From 422647e7898b8df67cd11f8fb060aec23dec84aa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 14 Oct 2026 22:20:15 +0200 Subject: feat(cli): meshbay-cli, a command-line client for members Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-cli/src/meshbay_cli/commands/login.py | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/meshbay-cli/src/meshbay_cli/commands/login.py (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/login.py') diff --git a/packages/meshbay-cli/src/meshbay_cli/commands/login.py b/packages/meshbay-cli/src/meshbay_cli/commands/login.py new file mode 100644 index 0000000..cbd16ef --- /dev/null +++ b/packages/meshbay-cli/src/meshbay_cli/commands/login.py @@ -0,0 +1,71 @@ +"""`login`: sign in with the passphrase, and remember the session.""" + +import contextlib +import json +import sys + +from ..client.account import Account +from ..client.groups import my_groups +from ..client.store import FILE_KEY_NOTICE, KEY_FILE +from ..errors import USAGE, CliError +from ..output import err, safe, sentence +from .context import Context +from .logout import sign_out_stored + +HELP = "sign in, and remember the session so later runs need no passphrase" + + +def add_parser(sub, parent) -> None: + p = sub.add_parser("login", parents=[parent], help=HELP, description=sentence(HELP) + ".") + p.add_argument("--no-remember", action="store_true", + help="check the credentials and store nothing") + + +async def run(ctx: Context) -> int: + username = ctx.username or ctx.stored_username + if not username: + if not sys.stdin.isatty(): + raise CliError(USAGE, "Name the account with --user.") + username = input("Username: ").strip() + ctx.username = username + if not username: + raise CliError(USAGE, "No username given.") + ctx.username = username + + notice = await ctx.hub.check_version() + remember = not ctx.args.no_remember + + # Signing in again as the same account keeps its device key, its node + # identities and its pins; another account replaces the store. + previous = ctx.store.peek() + account = Account(ctx.hub, username, ctx.passphrase) + if previous.get("hub") == ctx.hub.url and previous.get("username") == username: + with contextlib.suppress(CliError): + ctx.store.load() + account.state, account.secrets = ctx.store.state, ctx.store.secrets + elif remember and previous: + await sign_out_stored(ctx) + + await account.sign_in(remember=remember) + groups = await my_groups(account) + + if remember: + kind = await account.remember(ctx.store) + if kind == KEY_FILE: + err(FILE_KEY_NOTICE) + else: + await account.close() + + n = len(groups) + if ctx.args.json: + print(json.dumps({"username": username, "hub": ctx.hub.url, "groups": n, + "remembered": remember, + "key_store": ctx.store.peek().get("key_store") if remember + else None})) + else: + verb = "Signed in" if remember else "Credentials accepted" + print(f"{verb}: {safe(username)} on {ctx.hub.url} — " + f"{n} group{'s' if n != 1 else ''}.") + if notice: + err(notice) + return 0 -- cgit v1.2.3