""" The man page, generated from the parser: `python -m meshbay_cli.manpage`. Every action and option comes from `build_parser()`, so the page cannot list an option the program lacks or miss one it has; the prose the parser cannot carry (remote paths, the store, exit codes, environment) is written here. The packages build it at build time and install it in /usr/share/man/man1. """ import argparse import sys from . import __version__, errors from .__main__ import ACTIONS, build_parser EXIT_CODES = { 0: "Success.", errors.USAGE: "Usage error: an unknown option, a missing argument. Also: an action " "that asks for confirmation, run with no terminal and without " "\\fB\\-\\-yes\\fR, or not confirmed.", errors.AUTH: "Sign-in refused, no stored session and no passphrase to sign in " "with, or the account is locked out.", errors.NOT_FOUND: "A group, an invitation, a path, a device or a pattern that " "matches nothing; an invitation link no longer valid; a linking " "code no device is waiting with, or several are.", errors.PARTIAL: "Some items failed; the others were done. Each failure is " "named on stderr.", errors.NODE: "No node serving the group could be reached, or the node refused " "the connection. Also: this machine is waiting for device " "approval (the code is on stderr), or the node asks for an " "invitation's code.", errors.NODE_KEY: "A node presented another key than the one pinned at first " "contact. Nothing was sent. See \\fBtrust\\fR.", errors.VERSION: "This client, or the protocol it speaks, is too old or too new " "for the hub or the node.", errors.REFUSED: "Refused before anything was sent: a name already taken or not " "accepted by the node, a folder read-only or unavailable, leaving a " "group you own, revoking this machine.", errors.INTERRUPTED: "Interrupted (Ctrl+C). A download resumes where it stopped " "on the next run.", } DESCRIPTION = r""" .B meshbay-cli is a member's view of their MeshBay groups from a terminal: sign in, join and leave groups, list them and their folders, download and upload files, and approve this account's other devices. It talks to the hub only to sign in and to find a group's nodes; files go directly between this machine and the node, end-to-end encrypted, never through the hub. .PP Run with no argument at all, it opens the interactive mode (below). Global options are accepted before the action and after it. .PP Administering a node is not this program's business: see .BR meshbay-node (1). """ PATHS = r""" A remote path names a group, then a root the node shares, then folders and a file: .PP .RS .EX group[@owner]/root/folder/file .EE .RE .PP .B @owner is needed only when two of your groups share a name; group names compare without regard to case. A group name may itself contain .B / or .BR @ : type them as .B %2F and .B %40 (and a literal .B % as .BR %25 ). The GROUP column of .B meshbay-cli groups prints each group in this form. A link copied from the application's address bar (containing .BR #/ ) is accepted as it is. .PP .B get matches the wildcards .BR * , .B ? and .B [...] against the group's index, not against local files: quote them. """ INTERACTIVE = r""" .B meshbay-cli with no argument opens two panes: this machine's folders on the left, your groups on the right, and the transfers below. A sign-in screen comes first when no session is stored. .TP .B Tab switch pane .TP .B Enter open a folder or a group .TP .B Backspace go up .TP .B Space select an entry (several can be) .TP .B F5 copy the selection, or the entry under the cursor, to the other pane's folder; a name already there is asked about once for the whole batch .TP .B / filter by name; .B Esc clears the filter .TP .B s cycle the sort: name, size, date .TP .B r refresh .TP .B q quit .PP The right pane follows the node: a file added by someone else appears there. """ SESSION = r""" .B login signs in with the account's passphrase and, unless .B \-\-no\-remember is given, keeps the session so that later runs need no passphrase. The passphrase is read from, in order: .BR \-\-password\-file , .BR \-\-password , .BR MESHBAY_PASSWORD , then a prompt when there is a terminal. .B \-\-password puts it in the process list and the shell history; prefer .B \-\-password\-file \- (stdin). .PP The session and this machine's keys are kept encrypted in the data directory (see FILES), under a key held by the system's key storage (Secret Service, macOS Keychain, Windows Credential Manager). Where there is none, such as a server with no desktop session, the key is kept in a file only your user can read, like an SSH key without a passphrase; .B login says so once. .B logout ends the session on the hub, retires this machine's device key and erases everything stored. .PP The first time this machine opens a group whose node holds no copy of your keys (the application's "Browser access" turned off), it asks to be linked as a new device and prints a code. Approve that code from the application on a device already linked (the group's Settings), or with .B meshbay-cli devices approve .I GROUP CODE on a machine already linked; with a terminal, .B meshbay-cli waits and goes on by itself, otherwise it exits with status 5 and the next run goes on once the code is approved. .PP Each node's key is pinned at first contact. A node presenting another key is refused, with status 6, until .B meshbay-cli trust reset .I NODE forgets the pin; do that only for a key change the node's operator announced. """ MEMBERSHIP = r""" Joining has two halves. The hub makes the account a member: .B join accepts an invitation listed by .BR invitations , or redeems an invitation link pasted as it was sent. Then the group's node admits it: a key pair is made on this machine, the node wraps the group key for it, and it is kept (see FILES), so .B join needs a saved session. A link carries the code for the node it names, and is sent to that node only. An invitation's code is given with .BR \-\-code , or typed when the node asks for it; a node that cannot be reached gets it at the next connection. .PP .B leave ends a membership on the hub. The node keeps what it holds (the key it pinned, its copy of your keys, the files you uploaded) until its operator removes them, and the group key already served still opens what it opened until the operator replaces it. The owner of a group cannot leave it. .PP .B devices lists this account's devices on the node serving a group, approves a new one by the code it shows, and revokes one. Exactly one waiting device must match a code: a code that matches several, which somebody who saw it on your screen could cause, approves none. .PP .B browser\-access on makes this machine leave, on each node it opens a group on, a copy of its keys sealed under your passphrase, so that a browser can open the group; .B off withdraws that copy. Never set, nothing is written or withdrawn. It is this machine's setting, as Browser access is the desktop application's: the hub keeps a copy for browsers to read, and that copy never turns it on here. """ FILES = r""" .TP .I $XDG_DATA_HOME/meshbay\-cli/ \fR(default \fI~/.local/share/meshbay\-cli/\fR) On Windows, .IR %LOCALAPPDATA%\emeshbay\-cli\e . Holds .B state.json (the hub, the account name, pinned node keys, the browser access setting), .B secrets.enc (the session, the keys, invitation codes waiting for their node; encrypted), .B store.key only where the system has no key storage, and .B interactive.log (the interactive mode's log). .TP \fIname\fB.meshbay\-part\fR, \fIname\fB.meshbay\-part.json\fR A download in progress, next to where the file lands. A later .B get of the same file resumes from it; the finished file replaces it. """ ENVIRONMENT = r""" .TP .B MESHBAY_HUB The hub, when .B \-\-hub is not given (default: the one signed in to, else https://meshbay.org). .TP .B MESHBAY_USER The account name, when .B \-u is not given. .TP .B MESHBAY_PASSWORD The passphrase, when neither .B \-\-password\-file nor .B \-\-password is given. .TP .B MESHBAY_CLI_HOME The data directory, instead of the default (see FILES). .TP .B NO_COLOR The interactive mode draws without colour. """ EXAMPLES = r""" .EX meshbay\-cli login \-u alice meshbay\-cli invitations meshbay\-cli join Family@bob \-\-code K7P2\-9WQX meshbay\-cli join 'https://meshbay.org/#/invite?v=1&g=...' meshbay\-cli groups meshbay\-cli ls \-l Family/photos/2024 meshbay\-cli get \-R Family/photos/2024 \-o ~/Pictures/ meshbay\-cli get 'Family/music/*.flac' meshbay\-cli put \-R ~/scans Family/documents tar c notes | meshbay\-cli put \- Family/documents/notes.tar meshbay\-cli ls \-\-json \-R / | jq '.[] | .path' meshbay\-cli devices approve Family@bob ZX4P\-7QRT meshbay\-cli leave \-\-yes Club@carol .EE """ def _roff(text: str) -> str: """ Plain text into roff: no control line, no escape, no hyphen taken for a dash, and anything past ASCII named, which troff reads whatever the locale. """ text = text.replace("\\", "\\e").replace("-", "\\-") text = "".join(c if ord(c) < 128 else f"\\[u{ord(c):04X}]" for c in text) return "\n".join("\\&" + line if line[:1] in (".", "'") else line for line in text.splitlines()) def _option_lines(action: argparse.Action) -> str: if action.option_strings: names = ", ".join(f"\\fB{_roff(s)}\\fR" for s in action.option_strings) if action.choices: names += " " + "|".join(f"\\fB{_roff(c)}\\fR" for c in action.choices) elif action.nargs != 0: names += f" \\fI{_roff(action.metavar or action.dest.upper())}\\fR" else: names = f"\\fI{_roff(action.metavar or action.dest.upper())}\\fR" if action.choices: names = " | ".join(f"\\fB{_roff(c)}\\fR" for c in action.choices) help_text = action.help or "" return f".TP\n{names}\n{_roff(help_text)}\n" def _own_options(parser: argparse.ArgumentParser) -> list[argparse.Action]: """The action's own arguments; the global ones are listed once, above.""" late = {s for a in _globals(build_parser()) for s in a.option_strings} return [a for a in parser._actions if not isinstance(a, argparse._HelpAction) and not (set(a.option_strings) & late)] def _globals(parser: argparse.ArgumentParser) -> list[argparse.Action]: return [a for a in parser._actions if a.option_strings and not isinstance(a, argparse._SubParsersAction)] def _subparsers(parser: argparse.ArgumentParser) -> dict[str, argparse.ArgumentParser]: sub = next(a for a in parser._actions if isinstance(a, argparse._SubParsersAction)) return dict(sub.choices) def _synopsis(name: str, parser: argparse.ArgumentParser) -> str: """`meshbay-cli ls [-l] ... [path]`, without the global options.""" parts = [] own = _own_options(parser) for a in sorted(own, key=lambda a: not a.option_strings): # options first if a.option_strings: s = a.option_strings[0] if a.choices: s += " " + "|".join(a.choices) elif a.nargs != 0: s += f" {a.metavar or a.dest.upper()}" parts.append(f"[{s}]") elif a.choices: parts.append(" | ".join(a.choices)) else: meta = a.metavar or a.dest.upper() parts.append({"?": f"[{meta}]", "+": f"{meta}...", "*": f"[{meta}...]"} .get(a.nargs, meta)) return _roff(" ".join([name] + parts)) def render() -> str: parser = build_parser() out = [ f'.TH MESHBAY\\-CLI 1 "" "meshbay\\-cli {__version__}" "User Commands"', ".nh", ".ad l", ".SH NAME", "meshbay\\-cli \\- a member's MeshBay groups and files, from a terminal", ".SH SYNOPSIS", ".B meshbay\\-cli", ".br", ".B meshbay\\-cli", "[\\fIglobal options\\fR] \\fIaction\\fR [\\fIaction options\\fR] " "[\\fIarguments\\fR]", ".SH DESCRIPTION", DESCRIPTION.strip(), ".SH GLOBAL OPTIONS", ] out += [_option_lines(a).rstrip() for a in _globals(parser)] out.append(".SH ACTIONS") subs = _subparsers(parser) for name in ACTIONS: sp = subs[name] out += [f".SS {_roff(name)}", f".B {_synopsis(name, sp)}", ".PP", _roff(sp.description or "")] out += [_option_lines(a).rstrip() for a in _own_options(sp)] out += [".SH REMOTE PATHS", PATHS.strip(), ".SH INTERACTIVE MODE", INTERACTIVE.strip(), ".SH SESSION, DEVICES AND NODE KEYS", SESSION.strip(), ".SH GROUPS: JOINING, LEAVING, DEVICES", MEMBERSHIP.strip(), ".SH EXIT STATUS"] for code, text in sorted(EXIT_CODES.items()): out += [".TP", f".B {code}", text] out += [".SH ENVIRONMENT", ENVIRONMENT.strip(), ".SH FILES", FILES.strip(), ".SH EXAMPLES", EXAMPLES.strip(), ".SH SEE ALSO", ".BR meshbay\\-node (1)", ""] return "\n".join(out) def main() -> None: sys.stdout.write(render()) if __name__ == "__main__": main()