diff --git a/README.md b/README.md index 30cfe13..27d6471 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,78 @@ infrastructure: Each test creates a throwaway wallet in `/tmp` so runs are fully isolated and never touch real funds. Skip with `SKIP_INTEGRATION=1` when offline. +## UX — canonical conventions (issue #32) + +Every `sphere` command follows the same input/output rules so muscle memory +transfers across the surface. + +### Asset input — ` ` (two positional tokens) + +```bash +sphere payments send @bob 100.5 UCT +sphere invoice create --target @alice --asset 1000000 UCT +sphere invoice create --target @alice --asset 1000000 UCT --asset 500000 USDU +sphere invoice return --recipient @bob --asset 100000 UCT +sphere swap propose --to @bob --offer 10 UCT --want 5 USDU +``` + +`--asset`, `--offer`, and `--want` each consume the **next two argv tokens** +(amount, then coin). No quoted compound form (`--asset "10 UCT"`) is accepted +— that legacy shape has been removed. + +### Output — human-readable by default, `--json` for scripts + +```bash +sphere invoice status # labelled block, easy to read +sphere invoice status --json # raw JSON (machine-parseable) +``` + +`--json` is a global flag that any command honours. + +### Help — `--help` works for every command and subcommand + +```bash +sphere --help # top-level usage summary +sphere help # same +sphere help send # detailed help for one command +sphere wallet --help # namespace-level help +sphere wallet create --help # sub-subcommand help +sphere invoice deliver --help +``` + +On invalid input (missing flag, bad value, unknown subcommand) the CLI prints +`Error: ` followed by the full help block for the closest command — +never a one-line `Usage:` stub. + +## Shell completion + +`sphere completions ` emits a completion script. Pick the +install path that matches your environment: + +```bash +# Bash, no-sudo (recommended) +mkdir -p ~/.local/share/bash-completion/completions +sphere completions bash > ~/.local/share/bash-completion/completions/sphere + +# Bash, system-wide (requires sudo) +sudo sh -c "sphere completions bash > /etc/bash_completion.d/sphere" + +# Zsh +mkdir -p ~/.zsh/completions +sphere completions zsh > ~/.zsh/completions/_sphere +# Add to ~/.zshrc: +# fpath=(~/.zsh/completions $fpath) +# autoload -Uz compinit && compinit + +# Fish +sphere completions fish > ~/.config/fish/completions/sphere.fish +``` + +Re-source your shell rc (`source ~/.bashrc`) or open a new terminal to pick +up the completions. The completion script is regenerated from the same +command table the CLI dispatcher uses; an internal test +(`legacy-cli-ux.test.ts`) keeps the two in lockstep. + ## Design principles 1. **DM-native.** All controller → manager and controller → tenant traffic goes diff --git a/src/index.ts b/src/index.ts index 59cfde8..e568e19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,9 @@ const LEGACY_NAMESPACES = new Set([ // sphere-sdk → @unicity-sphere/cli extraction. `wallet init` / `wallet // status` map to the same legacy backing case via buildLegacyArgv. 'init', 'status', 'clear', + // Issue #32 Pass C: route `sphere help ` to the legacy dispatcher + // so the COMMAND_HELP registry powers detailed per-command help. + 'help', ]); // Phase 4 namespaces — DM-native, not yet implemented. @@ -90,6 +93,11 @@ export function buildLegacyArgv(namespace: string, tail: string[] = process.argv case 'status': return ['status', ...tail]; case 'clear': return ['clear', ...tail]; + // Issue #32: `sphere help ` and `sphere help ` — + // the legacy dispatcher recognises 'help' as a top-level command + // and prints the COMMAND_HELP entry for the rest of the tail. + case 'help': return ['help', ...tail]; + // faucet → legacy 'topup' case 'faucet': return ['topup', ...tail]; @@ -158,6 +166,12 @@ export function createCli(): Command { .description(`${name} commands (legacy bridge — phase 2)`); sub.allowUnknownOption(true); + // Issue #32 Pass C: forward `--help` / `-h` to the legacy dispatcher + // so per-command help blocks (e.g. `sphere invoice create --help`, + // `sphere swap propose --help`) print the full COMMAND_HELP entry + // instead of commander's thin namespace stub. The top-level + // `sphere --help` still works via the root command. + sub.helpOption(false); sub.action(async () => { const legacyArgv = buildLegacyArgv(name); // Dynamic import keeps the legacy ~40-file dispatcher out of the hot diff --git a/src/legacy/legacy-cli-ux.test.ts b/src/legacy/legacy-cli-ux.test.ts new file mode 100644 index 0000000..f0dfe38 --- /dev/null +++ b/src/legacy/legacy-cli-ux.test.ts @@ -0,0 +1,129 @@ +/** + * Issue #32 — UX consistency guard. + * + * These tests don't exercise SDK behaviour; they inspect the static + * surface of `legacy-cli.ts` (regex over the source) to keep the + * canonical UX invariants from drifting back to legacy patterns. + * + * Invariants enforced: + * 1. No `console.log(JSON.stringify(...))` in the dispatch body — + * output must route through `formatOutput()` so `--json` works. + * 2. No `console.error('Usage: …'); process.exit(…)` pattern — + * validation failures must call `failWithHelp(...)` so the full + * help block prints (Pass E). + * 3. No `parseAssetArg(` calls — asset input is canonical + * `consumeAssetPair(args, idx)` everywhere (Pass B). + * 4. Every entry in the COMMAND_HELP registry appears in the shell + * completion list (or is registered as a sub-subcommand of a + * completion entry that lists `subcommands`). + * + * If any of these regress, the regex will fire and the test will + * report the offending file:line. The CLI must stay consistent. + */ + +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const SOURCE_PATH = path.resolve(__dirname, 'legacy-cli.ts'); +const SOURCE = fs.readFileSync(SOURCE_PATH, 'utf8'); +const LINES = SOURCE.split('\n'); + +/** Return `{ line, text }` for each line that matches `re`. */ +function findMatches(re: RegExp): Array<{ line: number; text: string }> { + const out: Array<{ line: number; text: string }> = []; + for (let i = 0; i < LINES.length; i++) { + if (re.test(LINES[i])) out.push({ line: i + 1, text: LINES[i].trim() }); + } + return out; +} + +describe('issue #32 — UX consistency', () => { + it('no console.log(JSON.stringify(...)) in dispatch body — must use formatOutput()', () => { + // The implementation of formatOutput() itself legitimately calls + // console.log(JSON.stringify(...)) — that's the `--json` branch. + // Locate that function so we can exclude its body from the scan. + const formatStart = LINES.findIndex(l => /^function formatOutput\(/.test(l)); + expect(formatStart, 'formatOutput() helper definition not found').toBeGreaterThan(-1); + // Helper is small — closing brace at column 0 within ~20 lines. + let formatEnd = formatStart; + for (let i = formatStart + 1; i < Math.min(LINES.length, formatStart + 40); i++) { + if (/^\}/.test(LINES[i])) { formatEnd = i; break; } + } + const matches = findMatches(/console\.log\(\s*JSON\.stringify/); + const offenders = matches.filter(m => { + // Outside the formatOutput body + const inFormatOutput = m.line > formatStart && m.line <= formatEnd + 1; + return !inFormatOutput; + }); + expect(offenders, `Found ${offenders.length} stray console.log(JSON.stringify(...)) sites:\n${ + offenders.map(o => ` L${o.line}: ${o.text}`).join('\n') + }\nReplace with formatOutput(value, '', '