Browse Source

Replace lesspass-cli on npmjs with a lesspass on pypi

create a cli with python.
python will be installed by default on a lot of system.

 * use getpass python native module
   Fixes: https://github.com/lesspass/lesspass/issues/305

 * use lesspass python code generator from Maurits van der Schee
   Fixes: https://github.com/lesspass/lesspass/issues/328

tested with python2.7 and python3.6

breaking changes:

 * -c is associated with --copy flag to copy the password
 * -C is associated with the counter (previously -c)
pull/364/head
Guillaume Vincent 6 years ago
parent
commit
4280872d3c
24 changed files with 982 additions and 940 deletions
  1. +10
    -0
      README.md
  2. +115
    -0
      cli/.gitignore
  3. +1
    -0
      cli/lesspass/__init__.py
  4. +29
    -0
      cli/lesspass/cli.py
  5. +48
    -0
      cli/lesspass/clipboard.py
  6. +45
    -0
      cli/lesspass/core.py
  7. +72
    -0
      cli/lesspass/help.py
  8. +108
    -0
      cli/lesspass/password.py
  9. +17
    -0
      cli/lesspass/profile.py
  10. +68
    -0
      cli/lesspass/validator.py
  11. +1
    -0
      cli/lesspass/version.py
  12. +0
    -0
      cli/requirements.txt
  13. +27
    -0
      cli/setup.py
  14. +4
    -0
      cli/test-requirements.txt
  15. +0
    -0
      cli/tests/__init__.py
  16. +112
    -0
      cli/tests/test_cli.py
  17. +83
    -0
      cli/tests/test_password_generation.py
  18. +169
    -0
      cli/tests/test_profile.py
  19. +52
    -0
      cli/tests/test_validator.py
  20. +21
    -0
      cli/tox.ini
  21. +0
    -143
      packages/cli/index.js
  22. +0
    -25
      packages/cli/package.json
  23. +0
    -294
      packages/cli/test.js
  24. +0
    -478
      packages/cli/yarn.lock

+ 10
- 0
README.md View File

@@ -16,6 +16,16 @@ LessPass open source password manager (https://lesspass.com)
* :fr: [Présentation de LessPass](https://www.youtube.com/watch?v=YbaRCHXk8Jo)


## CLI

Use pip to install LessPass cli:

python3 -m pip install --user lesspass

Usage

lesspass --help

## License

This project is licensed under the terms of the GNU GPLv3.


+ 115
- 0
cli/.gitignore View File

@@ -0,0 +1,115 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

+ 1
- 0
cli/lesspass/__init__.py View File

@@ -0,0 +1 @@
name = "lesspass"

+ 29
- 0
cli/lesspass/cli.py View File

@@ -0,0 +1,29 @@
import argparse
import os


def parse_args(args):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("site", nargs="?")
parser.add_argument("login", nargs="?")
parser.add_argument(
"master_password",
default=os.environ.get("LESSPASS_MASTER_PASSWORD", None),
nargs="?",
)
parser.add_argument("-l", "--lowercase", dest="l", action="store_true")
parser.add_argument("-u", "--uppercase", dest="u", action="store_true")
parser.add_argument("-d", "--digits", dest="d", action="store_true")
parser.add_argument("-s", "--symbols", dest="s", action="store_true")
parser.add_argument("--no-lowercase", dest="nl", action="store_true")
parser.add_argument("--no-uppercase", dest="nu", action="store_true")
parser.add_argument("--no-digits", dest="nd", action="store_true")
parser.add_argument("--no-symbols", dest="ns", action="store_true")
parser.add_argument("-L", "--length", default=16, type=int)
parser.add_argument("-C", "--counter", default=1, type=int)
parser.add_argument(
"-c", "--copy", "--clipboard", dest="clipboard", action="store_true"
)
parser.add_argument("-h", "--help", action="store_true")
parser.add_argument("-v", "--version", action="store_true")
return parser.parse_args(args)

+ 48
- 0
cli/lesspass/clipboard.py View File

@@ -0,0 +1,48 @@
import platform
import subprocess
import uuid


def _call(args):
return subprocess.call(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)


def _copy_available(command):
if platform.system() == "Windows":
return _call(["where", command]) == 0
return _call(["which", command]) == 0


def get_system_copy_command():
if platform.system() == "Windows" and _copy_available("clip"):
return "clip"

if platform.system() == "Darwin" and _copy_available("pbcopy"):
return "pbcopy"

for command in ["xsel", "xclip"]:
if _copy_available(command):
return command


def _popen(args, **kwargs):
return subprocess.Popen(args, stdin=subprocess.PIPE, encoding="utf8")


commands = {
"clip": ["clip"],
"pbcopy": ["pbcopy"],
"xsel": ["xsel", "--clipboard", "--input"],
"xclip": ["xclip", "-selection", "clipboard"],
}

def copy(text):
command = get_system_copy_command()
if command is None:
raise (Exception("No software available on your system to copy to clipboard"))
args = commands[command]
if platform.system() == "Windows":
p = _popen(args)
else:
p = _popen(args, close_fds=True)
p.communicate(input=text)

+ 45
- 0
cli/lesspass/core.py View File

@@ -0,0 +1,45 @@
import getpass
import platform
import sys
import traceback

from lesspass.version import __version__
from lesspass.cli import parse_args
from lesspass.help import print_help
from lesspass.validator import validate_args
from lesspass.profile import create_profile
from lesspass.password import generate_password
from lesspass.clipboard import copy


def main(args=sys.argv[1:]):
args = parse_args(args)
if args.version:
print(__version__)
sys.exit(0)
error, help_message = validate_args(args)
if args.help:
print_help(help_message, long=True)
sys.exit(0)
if error:
print_help(help_message)
sys.exit(0)
profile, master_password = create_profile(args)
if not master_password:
master_password = getpass.getpass("Master Password: ")
generated_password = generate_password(profile, master_password)
if args.clipboard:
try:
copy(generated_password)
print("Copied to clipboard")
except Exception as e:
print("Copy failed, we are sorry")
print("Can you send us an email at contact@lesspass.com\n")
print("-" * 80)
print("Object: [LessPass][cli] Copy issue on %s" % platform.system())
print("Hello,")
print("I got an issue with LessPass cli software.\n")
traceback.print_exc()
print("-" * 80)
else:
print(generated_password)

+ 72
- 0
cli/lesspass/help.py View File

@@ -0,0 +1,72 @@
usage = "Usage: lesspass SITE [LOGIN] [MASTER_PASSWORD] [OPTIONS]"


def get_short_help(help_message):
return "%s\nErrors:\n%s\nTry 'lesspass --help' for more information." % (
usage,
help_message,
)


def get_long_help():
return """Name:

LessPass - stateless password generator
Usage:

lesspass SITE [LOGIN] [MASTER_PASSWORD] [OPTIONS]

Arguments:

SITE site used in the password generation (required)
LOGIN login used in the password generation
default to '' if not provided
MASTER_PASSWORD master password used in password generation
default to LESSPASS_MASTER_PASSWORD env variable or prompt

Options:

-l, --lowercase add lowercase in password
-u, --uppercase add uppercase in password
-d, --digits add digits in password
-s, --symbols add symbols in password
-L, --length int (default 16)
-C, --counter int (default 1)
--no-lowercase remove lowercase from password
--no-uppercase remove uppercase from password
--no-digits remove digits from password
--no-symbols remove symbols from password
-c, --clipboard copy generated password to clipboard rather than displaying it.
Need pbcopy (OSX), xsel or xclip (Linux) or clip (Windows).

Examples:

# no symbols
lesspass site login masterpassword --no-symbols

# no symbols shortcut
lesspass site login masterpassword -lud

# only digits and length of 8
lesspass site login masterpassword -d -L8

# master password in env variable
LESSPASS_MASTER_PASSWORD="masterpassword" lesspass site login

Copyright:

Copyright © 2018 Guillaume Vincent <contact@lesspass.com>. License GPLv3: GNU GPL version 3 <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law."""


def print_help(help_message, long=False):
usage = "Usage: lesspass SITE [LOGIN] [MASTER_PASSWORD] [OPTIONS]"
short_help = "%s\nErrors:\n%s\nTry 'lesspass --help' for more information." % (
usage,
help_message,
)
if long:
print(get_long_help())
else:
print(short_help)

+ 108
- 0
cli/lesspass/password.py View File

@@ -0,0 +1,108 @@
# From an initial work from Maurits van der Schee
# https://github.com/mevdschee/lesspass.py
#
# Copyright (C) 2017 Maurits van der Schee
# Copyright (C) 2018 Guillaume Vincent
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


import hashlib
import binascii

CHARACTER_SUBSETS = {
"lowercase": "abcdefghijklmnopqrstuvwxyz",
"uppercase": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"digits": "0123456789",
"symbols": "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
}


def _calc_entropy(password_profile, master_password):
salt = (
password_profile["site"]
+ password_profile["login"]
+ hex(password_profile["counter"])[2:]
)
return binascii.hexlify(
hashlib.pbkdf2_hmac(
"sha256", master_password.encode("utf-8"), salt.encode("utf-8"), 100000, 32
)
)


def _get_set_of_characters(rules=None):
if rules is None:
return (
CHARACTER_SUBSETS["lowercase"]
+ CHARACTER_SUBSETS["uppercase"]
+ CHARACTER_SUBSETS["digits"]
+ CHARACTER_SUBSETS["symbols"]
)
set_of_chars = ""
for rule in rules:
set_of_chars += CHARACTER_SUBSETS[rule]
return set_of_chars


def _consume_entropy(generated_password, quotient, set_of_characters, max_length):
if len(generated_password) >= max_length:
return [generated_password, quotient]
quotient, remainder = divmod(quotient, len(set_of_characters))
generated_password += set_of_characters[remainder]
return _consume_entropy(generated_password, quotient, set_of_characters, max_length)


def _insert_string_pseudo_randomly(generated_password, entropy, string):
for letter in string:
quotient, remainder = divmod(entropy, len(generated_password))
generated_password = (
generated_password[:remainder] + letter + generated_password[remainder:]
)
entropy = quotient
return generated_password


def _get_one_char_per_rule(entropy, rules):
one_char_per_rules = ""
for rule in rules:
value, entropy = _consume_entropy("", entropy, CHARACTER_SUBSETS[rule], 1)
one_char_per_rules += value
return [one_char_per_rules, entropy]


def _get_configured_rules(password_profile):
rules = ["lowercase", "uppercase", "digits", "symbols"]
return [
rule for rule in rules if rule in password_profile and password_profile[rule]
]


def _render_password(entropy, password_profile):
rules = _get_configured_rules(password_profile)
set_of_characters = _get_set_of_characters(rules)
password, password_entropy = _consume_entropy(
"", int(entropy, 16), set_of_characters, password_profile["length"] - len(rules)
)
characters_to_add, character_entropy = _get_one_char_per_rule(
password_entropy, rules
)
return _insert_string_pseudo_randomly(
password, character_entropy, characters_to_add
)


def generate_password(password_profile, master_password):
entropy = _calc_entropy(password_profile, master_password)
return _render_password(entropy, password_profile)

+ 17
- 0
cli/lesspass/profile.py View File

@@ -0,0 +1,17 @@
def create_profile(args):
profile = {
"lowercase": False if args.nl else True,
"uppercase": False if args.nu else True,
"digits": False if args.nd else True,
"symbols": False if args.ns else True,
"length": args.length,
"counter": args.counter,
"site": args.site,
"login": args.login or "",
}
if args.l or args.u or args.d or args.s:
profile["lowercase"] = args.l
profile["uppercase"] = args.u
profile["digits"] = args.d
profile["symbols"] = args.s
return profile, args.master_password

+ 68
- 0
cli/lesspass/validator.py View File

@@ -0,0 +1,68 @@
from lesspass.clipboard import get_system_copy_command


class NoOppositeRules(object):
def __init__(self, args):
self.args = args
self.error_message = ""

def is_valid(self):
is_valid = True
if self.args.l and self.args.nl:
self.error_message += (
" * Can't have -l (--lowercase) and --no-lowercase at the same time"
)
is_valid = False
if self.args.u and self.args.nu:
self.error_message += (
" * Can't have -u (--uppercase) and --no-uppercase at the same time"
)
is_valid = False
if self.args.d and self.args.nd:
self.error_message += (
" * Can't have -d (--digits) and --no-digits at the same time"
)
is_valid = False
if self.args.s and self.args.ns:
self.error_message += (
" * Can't have -s (--symbols) and --no-symbols at the same time"
)
is_valid = False
return is_valid


class DefaultParameters(object):
def __init__(self, args):
self.args = args
self.error_message = ""

def is_valid(self):
is_valid = True
if not self.args.site:
self.error_message += " * SITE is a required argument"
is_valid = False
return is_valid


class ClipboardAvailable(object):
def __init__(self, args):
self.args = args
self.error_message = ""

def is_valid(self):
is_valid = True
if self.args.clipboard and not get_system_copy_command():
self.error_message += " * To use the option -c (--copy) you need pbcopy on OSX, xsel or xclip on Linux and clip on Windows"
is_valid = False
return is_valid


def validate_args(args):
rules = [NoOppositeRules(args), DefaultParameters(args), ClipboardAvailable(args)]
error = False
error_message = ""
for rule in rules:
if not rule.is_valid():
error = True
error_message += "%s\n" % rule.error_message
return error, error_message

+ 1
- 0
cli/lesspass/version.py View File

@@ -0,0 +1 @@
__version__ = '6.0.0'

+ 0
- 0
cli/requirements.txt View File


+ 27
- 0
cli/setup.py View File

@@ -0,0 +1,27 @@
import setuptools

from lesspass.version import __version__
from lesspass.help import get_long_help


setuptools.setup(
name='lesspass',
version=__version__,
packages=['lesspass'],
author='Guillaume Vincent',
author_email='contact@lesspass.com',
description='LessPass stateless password generator',
long_description=get_long_help(),
install_requires=[],
entry_points="""
[console_scripts]
lesspass=lesspass.core:main
""",
url='https://github.com/lesspass/lesspass',
license='GPL-3.0',
classifiers=[
"Programming Language :: Python",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
],
)

+ 4
- 0
cli/test-requirements.txt View File

@@ -0,0 +1,4 @@
tox
pytest
flake8
mock

+ 0
- 0
cli/tests/__init__.py View File


+ 112
- 0
cli/tests/test_cli.py View File

@@ -0,0 +1,112 @@
import unittest

from mock import patch

from lesspass.cli import parse_args


class TestParseArgs(unittest.TestCase):
def test_parse_args_version(self):
self.assertTrue(parse_args(["--version"]).version)
self.assertTrue(parse_args(["-v"]).version)

def test_parse_args_help(self):
self.assertTrue(parse_args(["--help"]).help)
self.assertTrue(parse_args(["-h"]).help)

def test_parse_args_site(self):
self.assertEqual(parse_args(["site"]).site, "site")

def test_parse_args_login(self):
self.assertEqual(parse_args(["site", "login"]).login, "login")

def test_parse_args_LESSPASS_MASTER_PASSWORD_env_variable(self):
with patch.dict("os.environ", {"LESSPASS_MASTER_PASSWORD": "password"}):
self.assertEqual(parse_args([]).master_password, "password")

def test_parse_args_master_password(self):
self.assertEqual(
parse_args(["site", "login", "masterpassword"]).master_password,
"masterpassword",
)

def test_parse_args_l(self):
self.assertTrue(parse_args(["site", "-l"]).l)
self.assertTrue(parse_args(["site", "--lowercase"]).l)

def test_parse_args_u(self):
self.assertTrue(parse_args(["site", "-u"]).u)
self.assertTrue(parse_args(["site", "--uppercase"]).u)

def test_parse_args_d(self):
self.assertTrue(parse_args(["site", "-d"]).d)
self.assertTrue(parse_args(["site", "--digits"]).d)

def test_parse_args_s(self):
self.assertTrue(parse_args(["site", "-s"]).s)
self.assertTrue(parse_args(["site", "--symbols"]).s)

def test_parse_args_lu(self):
args = parse_args(["site", "-lu"])
self.assertTrue(args.l)
self.assertTrue(args.u)
self.assertFalse(args.d)
self.assertFalse(args.s)

def test_parse_args_lud(self):
args = parse_args(["site", "-lud"])
self.assertTrue(args.l)
self.assertTrue(args.u)
self.assertTrue(args.d)
self.assertFalse(args.s)

def test_parse_args_luds(self):
args = parse_args(["site", "-luds"])
self.assertTrue(args.l)
self.assertTrue(args.u)
self.assertTrue(args.d)
self.assertTrue(args.s)

def test_parse_args_no_lowercase(self):
self.assertTrue(parse_args(["site", "--no-lowercase"]).nl)

def test_parse_args_no_uppercase(self):
self.assertTrue(parse_args(["site", "--no-uppercase"]).nu)

def test_parse_args_no_digits(self):
self.assertTrue(parse_args(["site", "--no-digits"]).nd)

def test_parse_args_no_symbols(self):
self.assertTrue(parse_args(["site", "--no-symbols"]).ns)

def test_parse_args_length_default(self):
self.assertEqual(parse_args(["site"]).length, 16)

def test_parse_args_length_long(self):
self.assertEqual(parse_args(["site", "--length", "8"]).length, 8)

def test_parse_args_length_short(self):
self.assertEqual(parse_args(["site", "-L6"]).length, 6)
self.assertEqual(parse_args(["site", "-L", "12"]).length, 12)

def test_parse_args_counter_default(self):
self.assertEqual(parse_args(["site"]).counter, 1)

def test_parse_args_counter_long(self):
self.assertEqual(parse_args(["site", "--counter", "2"]).counter, 2)

def test_parse_args_counter_short(self):
self.assertEqual(parse_args(["site", "-C99"]).counter, 99)
self.assertEqual(parse_args(["site", "-C", "100"]).counter, 100)

def test_parse_args_clipboard_default(self):
self.assertFalse(parse_args(["site"]).clipboard)

def test_parse_args_clipboard_long(self):
self.assertTrue(parse_args(["site", "--copy"]).clipboard)

def test_parse_args_clipboard_short(self):
self.assertTrue(parse_args(["site", "-c"]).clipboard)

def test_parse_args_clipboard_backward_compatibility(self):
self.assertTrue(parse_args(["site", "--clipboard"]).clipboard)

+ 83
- 0
cli/tests/test_password_generation.py View File

@@ -0,0 +1,83 @@
import unittest

from lesspass.password import generate_password


class TestPassword(unittest.TestCase):
def test_generate_password(self):
profile = {
"site": "example.org",
"login": "contact@example.org",
"lowercase": True,
"uppercase": True,
"digits": True,
"symbols": True,
"length": 16,
"counter": 1,
}
master_password = "password"
self.assertEqual(
generate_password(profile, master_password), "WHLpUL)e00[iHR+w"
)

def test_generate_password_2(self):
profile = {
"site": "example.org",
"login": "contact@example.org",
"lowercase": True,
"uppercase": True,
"digits": True,
"symbols": False,
"length": 14,
"counter": 2,
}
master_password = "password"
self.assertEqual(generate_password(profile, master_password), "MBAsB7b1Prt8Sl")

def test_generate_password_3(self):
profile = {
"site": "example.org",
"login": "contact@example.org",
"lowercase": False,
"uppercase": False,
"digits": True,
"symbols": False,
"length": 16,
"counter": 1,
}
master_password = "password"
self.assertEqual(
generate_password(profile, master_password), "8742368585200667"
)

def test_generate_password_4(self):
profile = {
"site": "example.org",
"login": "contact@example.org",
"lowercase": True,
"uppercase": True,
"digits": False,
"symbols": True,
"length": 16,
"counter": 1,
}
master_password = "password"
self.assertEqual(
generate_password(profile, master_password), "s>{F}RwkN/-fmM.X"
)

def test_generate_password_nrt_328(self):
profile = {
"site": "site",
"login": "login",
"lowercase": True,
"uppercase": True,
"digits": True,
"symbols": True,
"length": 16,
"counter": 10,
}
master_password = "test"
self.assertEqual(
generate_password(profile, master_password), "XFt0F*,r619:+}[."
)

+ 169
- 0
cli/tests/test_profile.py View File

@@ -0,0 +1,169 @@
import unittest

from lesspass.cli import parse_args
from lesspass.profile import create_profile


class TestProfile(unittest.TestCase):
def test_create_profile_default(self):
profile, master_password = create_profile(parse_args(["site", "login"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])
self.assertEqual(profile["length"], 16)
self.assertEqual(profile["counter"], 1)
self.assertEqual(profile["site"], "site")
self.assertEqual(profile["login"], "login")
self.assertIsNone(master_password)

def test_create_profile_login(self):
profile, _ = create_profile(parse_args(["site"]))
self.assertEqual(profile["login"], "")

def test_create_profile_length(self):
profile, _ = create_profile(parse_args(["site", "--length", "8"]))
self.assertEqual(profile["length"], 8)

def test_create_profile_counter(self):
profile, _ = create_profile(parse_args(["site", "--counter", "2"]))
self.assertEqual(profile["counter"], 2)

def test_create_profile_master_password(self):
_, master_password = create_profile(
parse_args(["site", "login", "master_password"])
)
self.assertEqual(master_password, "master_password")

def test_create_profile_l(self):
profile, _ = create_profile(parse_args(["site", "-l"]))
self.assertTrue(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_u(self):
profile, _ = create_profile(parse_args(["site", "-u"]))
self.assertFalse(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_d(self):
profile, _ = create_profile(parse_args(["site", "-d"]))
self.assertFalse(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_s(self):
profile, _ = create_profile(parse_args(["site", "-s"]))
self.assertFalse(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_lu(self):
profile, _ = create_profile(parse_args(["site", "-lu"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_ld(self):
profile, _ = create_profile(parse_args(["site", "-ld"]))
self.assertTrue(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_ls(self):
profile, _ = create_profile(parse_args(["site", "-ls"]))
self.assertTrue(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_ud(self):
profile, _ = create_profile(parse_args(["site", "-ud"]))
self.assertFalse(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_us(self):
profile, _ = create_profile(parse_args(["site", "-us"]))
self.assertFalse(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_ds(self):
profile, _ = create_profile(parse_args(["site", "-ds"]))
self.assertFalse(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_lud(self):
profile, _ = create_profile(parse_args(["site", "-lud"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertFalse(profile["symbols"])

def test_create_profile_lus(self):
profile, _ = create_profile(parse_args(["site", "-lus"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_uds(self):
profile, _ = create_profile(parse_args(["site", "-uds"]))
self.assertFalse(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_luds(self):
profile, _ = create_profile(parse_args(["site", "-luds"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_suld(self):
profile, _ = create_profile(parse_args(["site", "-suld"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_nl(self):
profile, _ = create_profile(parse_args(["site", "--no-lowercase"]))
self.assertFalse(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_nu(self):
profile, _ = create_profile(parse_args(["site", "--no-uppercase"]))
self.assertTrue(profile["lowercase"])
self.assertFalse(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_nd(self):
profile, _ = create_profile(parse_args(["site", "--no-digits"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertFalse(profile["digits"])
self.assertTrue(profile["symbols"])

def test_create_profile_ns(self):
profile, _ = create_profile(parse_args(["site", "--no-symbols"]))
self.assertTrue(profile["lowercase"])
self.assertTrue(profile["uppercase"])
self.assertTrue(profile["digits"])
self.assertFalse(profile["symbols"])

+ 52
- 0
cli/tests/test_validator.py View File

@@ -0,0 +1,52 @@
import unittest

from lesspass.cli import parse_args
from lesspass.validator import validate_args


class TestValidateArgs(unittest.TestCase):
def test_validate_args_no_opposite_rules_lowercase(self):
error, message = validate_args(parse_args(["site", "-l", "--no-lowercase"]))
self.assertTrue(error)
self.assertTrue(
"Can't have -l (--lowercase) and --no-lowercase at the same time" in message
)

def test_validate_args_no_opposite_rules_uppercase(self):
error, message = validate_args(parse_args(["site", "-u", "--no-uppercase"]))
self.assertTrue(error)
self.assertTrue(
"Can't have -u (--uppercase) and --no-uppercase at the same time" in message
)

def test_validate_args_no_opposite_rules_digits(self):
error, message = validate_args(parse_args(["site", "-d", "--no-digits"]))
self.assertTrue(error)
self.assertTrue(
"Can't have -d (--digits) and --no-digits at the same time" in message
)

def test_validate_args_no_opposite_rules_symbols(self):
error, message = validate_args(parse_args(["site", "-s", "--no-symbols"]))
self.assertTrue(error)
self.assertTrue(
"Can't have -s (--symbols) and --no-symbols at the same time" in message
)

def test_validate_args_concat_errors(self):
_, message = validate_args(
parse_args(["site", "-u", "--no-uppercase", "-l", "--no-lowercase"])
)
self.assertTrue(
"Can't have -l (--lowercase) and --no-lowercase at the same time" in message
)
self.assertTrue(
"Can't have -u (--uppercase) and --no-uppercase at the same time" in message
)

def test_validate_args_no_site(self):
error, message = validate_args(parse_args([]))
self.assertTrue(error)
self.assertTrue(
"SITE is a required argument" in message
)

+ 21
- 0
cli/tox.ini View File

@@ -0,0 +1,21 @@
[tox]
skipsdist = True
envlist = pep8,py2,py3

[testenv]
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
install_command = pip install -U {packages}
commands = pytest -v {posargs: tests}
whitelist_externals = sh
setenv =
PYTHONPATH = {toxinidir}

[testenv:pep8]
commands = flake8 --ignore=E501 --show-source src tests

[testenv:py2]
basepython = python2

[testenv:py3]
basepython = python3

+ 0
- 143
packages/cli/index.js View File

@@ -1,143 +0,0 @@
#!/usr/bin/env node
const clipboardy = require("clipboardy");
const meow = require("meow");
const { calcEntropy } = require("lesspass-entropy");
const renderPassword = require("lesspass-render-password");
const read = require("read");
const chalk = require("chalk");

const helpMessage = `
Usage
$ lesspass <site> <login> [masterPassword] [options]

Options
-l add lowercase in password
-u add uppercase in password
-d add digits in password
-s add symbols in password

--no-lowercase remove lowercase from password
--no-uppercase remove uppercase from password
--no-digits remove digits from password
--no-symbols remove symbols from password

--length, -L int (default 16)
--counter, -c int (default 1)

--clipboard, -C copy generated password to clipboard rather than displaying it.
Need pbcopy (OSX), xsel (Linux) or clip (Windows).

Examples
# no symbols
$ lesspass lesspass.com contact@lesspass.com password --no-symbols
OlfK63bmUhqrGODR

# no symbols shortcut
$ lesspass lesspass.com contact@lesspass.com password -lud
OlfK63bmUhqrGODR

# only digits and length of 8
$ lesspass lesspass.com contact@lesspass.com -d -L8
master password:
75837019`;

const cli = meow(helpMessage, {
flags: {
site: { type: "string" },
login: { type: "string" },
length: {
type: "string",
alias: "L"
},
counter: {
type: "string",
alias: "c"
},
clipboard: {
type: "boolean",
alias: "C"
},
l: { type: "boolean" },
u: { type: "boolean" },
d: { type: "boolean" },
s: { type: "boolean" }
}
});

function calcPassword(passwordProfile, masterPassword, copyToClipboard) {
calcEntropy(passwordProfile, masterPassword).then(entropy => {
const generatedPassword = renderPassword(entropy, passwordProfile.options);
if (copyToClipboard) {
clipboardy
.write(generatedPassword)
.then(() => {
console.log("Copied to clipboard");
process.exit();
})
.catch(err => {
console.error(chalk.red("Copy failed."));
console.error(err.message);
process.exit(1);
});
} else {
console.log(generatedPassword);
process.exit();
}
});
}

function hasNoShortOption(options) {
return !["l", "u", "d", "s"].some(
shortOption =>
typeof options[shortOption] !== "undefined" && options[shortOption]
);
}

function getOptionBoolean(options, optionString) {
const shortOption = optionString.substring(0, 1);
if (options[shortOption]) {
return true;
}
if (typeof options[optionString] === "undefined") {
return hasNoShortOption(options);
}
return options[optionString];
}

const site = cli.input[0];
if (typeof site === "undefined") {
console.log(chalk.red("site cannot be empty"));
console.log("type lesspass --help");
process.exit(-1);
}

const lowercase = getOptionBoolean(cli.flags, "lowercase");
const uppercase = getOptionBoolean(cli.flags, "uppercase");
const symbols = getOptionBoolean(cli.flags, "symbols");
const digits = getOptionBoolean(cli.flags, "digits");

const passwordProfile = {
site,
login: cli.input[1] || "",
options: {
counter: cli.flags.counter || 1,
length: cli.flags.length || 16,
lowercase,
uppercase,
digits,
symbols
}
};

const copyToClipboard = cli.flags.clipboard || false;
if (cli.input.length === 3) {
const masterPassword = cli.input[2];
calcPassword(passwordProfile, masterPassword, copyToClipboard);
} else {
read({ prompt: "master password: ", silent: true }, (er, password) => {
if (er && er.message === "canceled") {
process.exit();
}
calcPassword(passwordProfile, password, copyToClipboard);
});
}

+ 0
- 25
packages/cli/package.json View File

@@ -1,25 +0,0 @@
{
"name": "lesspass",
"version": "9.0.0",
"description": "LessPass cli",
"license": "GPL-3.0",
"author": {
"name": "Guillaume Vincent",
"email": "guillaume@oslab.fr",
"url": "https://guillaumevincent.com"
},
"files": [
"index.js"
],
"bin": {
"lesspass": "index.js"
},
"dependencies": {
"chalk": "2.3.1",
"clipboardy": "1.2.3",
"lesspass-entropy": "1.0.0",
"lesspass-render-password": "1.0.0",
"meow": "5.0.0",
"read": "1.0.7"
}
}

+ 0
- 294
packages/cli/test.js View File

@@ -1,294 +0,0 @@
const path = require("path");
const execa = require("execa");

const cliPath = path.resolve(__dirname, "index.js");

test("default options", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("\\g-A1-.OHEwrXjT#");
});

test("no login", async () => {
const command = `echo password | ${cliPath} "lesspass.com"`;
const { stdout } = await execa.shell(command);
expect(stdout).toBe("master password: 7Cw-APO5Co?G>W>u");
});

test("options can be before parameters", async () => {
const { stdout } = await execa(cliPath, [
"-C",
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("Copied to clipboard");
});

test("long options can be before parameters", async () => {
const { stdout } = await execa(cliPath, [
"--clipboard",
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("Copied to clipboard");
});

test("length", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--length=14"
]);
expect(stdout).toBe("=0\\A-.OHEKvwrX");
});

test("length shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-L=14"
]);
expect(stdout).toBe("=0\\A-.OHEKvwrX");
});

test("counter", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--counter=2"
]);
expect(stdout).toBe("Vf:F1'!I`8Y2`GBE");
});

test("counter shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-c=2"
]);
expect(stdout).toBe("Vf:F1'!I`8Y2`GBE");
});

test("no lowercase", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--no-lowercase"
]);
expect(stdout).toBe('JBG\\`3{+0["(E\\JJ');
});

test("no lowercase shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-uds"
]);
expect(stdout).toBe('JBG\\`3{+0["(E\\JJ');
});

test("only lowercase", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-l"
]);
expect(stdout).toBe("fmnujoqgcxmpffyh");
});

test("no uppercase", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--no-uppercase"
]);
expect(stdout).toBe('jbg\\`3{+0["(e\\jj');
});

test("no uppercase shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-lds"
]);
expect(stdout).toBe('jbg\\`3{+0["(e\\jj');
});

test("only uppercase", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-u"
]);
expect(stdout).toBe("FMNUJOQGCXMPFFYH");
});

test("no digits", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--no-digits"
]);
expect(stdout).toBe(";zkB#m]mNF$;J_Ej");
});

test("no digits shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-lus"
]);
expect(stdout).toBe(";zkB#m]mNF$;J_Ej");
});

test("only digits", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-d"
]);
expect(stdout).toBe("7587019305478072");
});

test("no symbols", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--no-symbols"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("no symbols shortcut", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-lud"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("only symbols", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-s"
]);
expect(stdout).toBe("<\"]|'`%};'`>-'[,");
});

test("test space in password", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"my Master Password"
]);
expect(stdout).toBe("D1PBB34\\#fh!LY={");
});

test("doc 1", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"--no-symbols"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("doc 1 options before", async () => {
const { stdout } = await execa(cliPath, [
"--no-symbols",
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("doc 2", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-lud"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("doc 2 options before", async () => {
const { stdout } = await execa(cliPath, [
"-lud",
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("OlfK63bmUhqrGODR");
});

test("doc 3", async () => {
const { stdout } = await execa(cliPath, [
"lesspass.com",
"contact@lesspass.com",
"password",
"-d",
"-L8"
]);
expect(stdout).toBe("75837019");
});

test("doc 3 options before", async () => {
const { stdout } = await execa(cliPath, [
"-d",
"-L8",
"lesspass.com",
"contact@lesspass.com",
"password"
]);
expect(stdout).toBe("75837019");
});

test("doc 3 options before and after", async () => {
const { stdout } = await execa(cliPath, [
"-d",
"lesspass.com",
"contact@lesspass.com",
"password",
"-L8"
]);
expect(stdout).toBe("75837019");
});

test("nrt numbers should be considered as string not integers", async () => {
const p = execa(cliPath, ["example.org", "123", "password"]);
const p2 = execa(cliPath, ["example.org", "0123", "password"]);
const p3 = execa(cliPath, ["example.org", '"0123"', "password"]);
const p4 = execa(cliPath, ["example.org", "00123", "password"]);
return Promise.all([p, p2, p3, p4]).then(v => {
expect(v[0].stdout).toBe("sMb8}N&`J4wkF9q~");
expect(v[1].stdout).toBe("5,4SqhB2[=/h\\DZh");
expect(v[2].stdout).toBe("u0Fz)EOJ4i\\{{;a~");
expect(v[3].stdout).toBe('=}|O7hN0ZHdjQ{">');
});
});

+ 0
- 478
packages/cli/yarn.lock View File

@@ -1,478 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


ansi-styles@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"

arch@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e"
integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==

array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=

arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=

builtin-modules@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=

camelcase-keys@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77"
integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=
dependencies:
camelcase "^4.1.0"
map-obj "^2.0.0"
quick-lru "^1.0.0"

camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=

chalk@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
integrity sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==
dependencies:
ansi-styles "^3.2.0"
escape-string-regexp "^1.0.5"
supports-color "^5.2.0"

clipboardy@1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.3.tgz#0526361bf78724c1f20be248d428e365433c07ef"
integrity sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA==
dependencies:
arch "^2.1.0"
execa "^0.8.0"

color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"

color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=

cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
dependencies:
lru-cache "^4.0.1"
shebang-command "^1.2.0"
which "^1.2.9"

currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
dependencies:
array-find-index "^1.0.1"

decamelize-keys@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
dependencies:
decamelize "^1.1.0"
map-obj "^1.0.0"

decamelize@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=

error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
dependencies:
is-arrayish "^0.2.1"

escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=

execa@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=
dependencies:
cross-spawn "^5.0.1"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"

find-up@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
dependencies:
locate-path "^2.0.0"

get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=

graceful-fs@^4.1.2:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==

has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=

hosted-git-info@^2.1.4:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==

indent-string@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=

is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=

is-builtin-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
dependencies:
builtin-modules "^1.0.0"

is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=

is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=

isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=

json-parse-better-errors@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==

load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"

locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
dependencies:
p-locate "^2.0.0"
path-exists "^3.0.0"

loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
dependencies:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"

lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"

map-obj@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=

map-obj@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=

meow@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4"
integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==
dependencies:
camelcase-keys "^4.0.0"
decamelize-keys "^1.0.0"
loud-rejection "^1.0.0"
minimist-options "^3.0.1"
normalize-package-data "^2.3.4"
read-pkg-up "^3.0.0"
redent "^2.0.0"
trim-newlines "^2.0.0"
yargs-parser "^10.0.0"

minimist-options@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954"
integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==
dependencies:
arrify "^1.0.1"
is-plain-obj "^1.1.0"

mute-stream@~0.0.4:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=

normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
dependencies:
hosted-git-info "^2.1.4"
is-builtin-module "^1.0.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"

npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
dependencies:
path-key "^2.0.0"

p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=

p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
dependencies:
p-try "^1.0.0"

p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
dependencies:
p-limit "^1.1.0"

p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=

parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
dependencies:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"

path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=

path-key@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=

path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
dependencies:
pify "^3.0.0"

pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=

pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=

quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=

read-pkg-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
dependencies:
find-up "^2.0.0"
read-pkg "^3.0.0"

read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"

read@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=
dependencies:
mute-stream "~0.0.4"

redent@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"
integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=
dependencies:
indent-string "^3.0.0"
strip-indent "^2.0.0"

"semver@2 || 3 || 4 || 5":
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==

shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
dependencies:
shebang-regex "^1.0.0"

shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=

signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=

spdx-correct@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e"
integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==
dependencies:
spdx-expression-parse "^3.0.0"
spdx-license-ids "^3.0.0"

spdx-exceptions@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==

spdx-expression-parse@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"

spdx-license-ids@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2"
integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==

strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=

strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=

strip-indent@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=

supports-color@^5.2.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"

trim-newlines@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=

validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
dependencies:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"

which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"

yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=

yargs-parser@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
dependencies:
camelcase "^4.1.0"

Loading…
Cancel
Save