|
| 1 | +# Proposal: a command-descriptor registry for `stash` help + a `manifest --json` |
| 2 | + |
| 3 | +**Status:** proposal / for discussion |
| 4 | +**Area:** `packages/cli` |
| 5 | +**Motivated by:** the docs V2 CLI reference, which is generated from the CLI |
| 6 | +(cipherstash/docs#45). Today it parses `stash --help`; this proposal gives it — |
| 7 | +and agents — a real structured source, and makes per-command help consistent. |
| 8 | + |
| 9 | +## Problem |
| 10 | + |
| 11 | +1. **The help text drifts from the implementation.** The top-level help is a |
| 12 | + hand-written template string, `HELP`, in |
| 13 | + [`packages/cli/src/bin/main.ts`](../../packages/cli/src/bin/main.ts), |
| 14 | + maintained separately from the command modules in `packages/cli/src/commands/*`. |
| 15 | + Nothing ties the two together, so the help lags real behaviour. (Concretely: |
| 16 | + the published `stash@0.16.0 --help` still lists the `db` group while `main.ts` |
| 17 | + on `main` has already moved install/upgrade/status to `eql` — the docs |
| 18 | + generated from the published `--help` were a whole command surface behind.) |
| 19 | + |
| 20 | +2. **Per-command help is inconsistent.** `auth` implements its own `--help` |
| 21 | + ([`commands/auth/index.ts`](../../packages/cli/src/commands/auth/index.ts)), |
| 22 | + but `stash eql install --help`, `stash db push --help`, etc. fall through to |
| 23 | + the top-level help. There is no per-command help for most commands. |
| 24 | + |
| 25 | +3. **No machine-readable output.** Docs have to scrape `--help`, and agents |
| 26 | + running `npx stash` have no authoritative, versioned command surface to read. |
| 27 | + The `--help` is also thin: no per-command args, no per-command examples, |
| 28 | + `auth`/`encrypt` subcommands undetailed. |
| 29 | + |
| 30 | +## Goals |
| 31 | + |
| 32 | +- One source of truth for command metadata; help and docs can't drift from it. |
| 33 | +- Consistent `stash <command> --help` for every command. |
| 34 | +- `stash manifest --json` — a structured, versioned command surface for the docs |
| 35 | + generator and for agents. |
| 36 | +- Room for rich per-command **long descriptions** and **examples** |
| 37 | + (GitHub-CLI / cobra model), so `--help` and the docs read well. |
| 38 | + |
| 39 | +## Design: a command-descriptor registry |
| 40 | + |
| 41 | +Replace the hand-written `HELP` string with a registry of descriptors — one per |
| 42 | +command — and render everything (top-level help, per-command help, JSON) from it. |
| 43 | +Command *handlers* (`commands/*`) are unchanged; the descriptor just carries |
| 44 | +metadata and points at the handler. |
| 45 | + |
| 46 | +```ts |
| 47 | +interface Flag { |
| 48 | + name: string; // "--supabase" |
| 49 | + value?: string; // "<path>" for value-taking flags |
| 50 | + description: string; |
| 51 | + appliesTo?: string[]; // for shared groups: subcommands this flag applies to |
| 52 | + default?: string; |
| 53 | + env?: string; // e.g. "DATABASE_URL" |
| 54 | +} |
| 55 | + |
| 56 | +interface CommandDescriptor { |
| 57 | + name: string; // "eql install", "auth login" |
| 58 | + group: string; // "Setup & workflow" | "Database" | "EQL" | ... |
| 59 | + summary: string; // one line (today's Commands: text) |
| 60 | + long?: string; // rich multi-paragraph help (cobra `Long`) |
| 61 | + examples?: string[]; // curated, per command (cobra `Example`) |
| 62 | + flags?: Flag[]; |
| 63 | + hidden?: boolean; // deprecated aliases (the old `db install`, …) |
| 64 | + run: (args: string[], flags: Record<string, unknown>) => Promise<void>; |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +Three renderers over the registry: |
| 69 | + |
| 70 | +- `renderTopLevelHelp(registry)` → today's `stash --help` (kills the `HELP` string). |
| 71 | +- `renderCommandHelp(registry, "eql install")` → per-command help, for **every** |
| 72 | + command, uniformly. `auth`'s bespoke `HELP` block goes away. |
| 73 | +- `emitManifest(registry)` → `stash manifest --json` (schema below). |
| 74 | + |
| 75 | +Dispatch already routes `argv` to a handler; it now looks the handler up in the |
| 76 | +registry, and a missing/`--help` arg prints `renderCommandHelp`. |
| 77 | + |
| 78 | +## `stash manifest --json` — the contract |
| 79 | + |
| 80 | +This is the exact shape the docs generator (cipherstash/docs#45) already targets, |
| 81 | +so once this lands the docs swap `--help` parsing for: |
| 82 | + |
| 83 | +``` |
| 84 | +npx stash@<latest> manifest --json |
| 85 | +``` |
| 86 | + |
| 87 | +```jsonc |
| 88 | +{ |
| 89 | + "name": "stash", |
| 90 | + "version": "0.17.0", |
| 91 | + "groups": [ |
| 92 | + { |
| 93 | + "title": "Auth", |
| 94 | + "commands": [ |
| 95 | + { |
| 96 | + "name": "auth login", |
| 97 | + "summary": "Authenticate with CipherStash", |
| 98 | + "long": "Runs the OAuth 2.0 device authorization flow: pick a region, approve in the browser (the URL is printed so it works headless/over SSH), then your device is bound to the workspace's default keyset…", |
| 99 | + "examples": ["stash auth login", "stash auth login --supabase"], |
| 100 | + "flags": [ |
| 101 | + { "name": "--supabase", "description": "Track Supabase as the referrer" } |
| 102 | + ] |
| 103 | + } |
| 104 | + ] |
| 105 | + } |
| 106 | + ] |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +`version` comes from the CLI's own `package.json`, so a page generated from the |
| 111 | +manifest is always stamped with the exact version it describes. |
| 112 | + |
| 113 | +## Worked example: `auth` |
| 114 | + |
| 115 | +Today `auth` is described in **two** places — a line in `main.ts`'s `HELP` and |
| 116 | +the `HELP` string in `commands/auth/index.ts` — and the device-code-flow detail |
| 117 | +(region → verification URL → poll → bind device to the default keyset) lives only |
| 118 | +in code. As a descriptor it's declared once: |
| 119 | + |
| 120 | +```ts |
| 121 | +{ |
| 122 | + name: "auth login", |
| 123 | + group: "Auth", |
| 124 | + summary: "Authenticate with CipherStash", |
| 125 | + long: [ |
| 126 | + "Runs the OAuth 2.0 device authorization flow:", |
| 127 | + "1. Pick a region for your workspace.", |
| 128 | + "2. Approve in the browser — the URL is printed, so it works over SSH/headless.", |
| 129 | + "3. The CLI polls until you approve, then stores a short-lived token.", |
| 130 | + "4. Your device is bound to the workspace's default keyset, so later", |
| 131 | + " commands authenticate without a fresh login.", |
| 132 | + ].join("\n"), |
| 133 | + examples: ["stash auth login", "stash auth login --supabase"], |
| 134 | + flags: [ |
| 135 | + { name: "--supabase", description: "Track Supabase as the referrer" }, |
| 136 | + { name: "--drizzle", description: "Track Drizzle as the referrer" }, |
| 137 | + ], |
| 138 | + run: authLogin, |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +That one declaration now feeds `stash auth --help`, `stash auth login --help`, |
| 143 | +the top-level help line, the JSON manifest, and the generated docs page. |
| 144 | + |
| 145 | +## How the docs consume it |
| 146 | + |
| 147 | +- `cipherstash/docs` runs `npx stash@<latest> manifest --json` in `prebuild`, |
| 148 | + renders one versioned page per command, and stamps `verifiedAgainst.cli`. |
| 149 | + (Prototype today parses `--help`; swapping to the manifest deletes the parser |
| 150 | + and keeps the page format identical — see cipherstash/docs#45.) |
| 151 | +- **Content split (GitHub-CLI model):** per-command *reference* — summary, flags, |
| 152 | + `long`, examples — lives here in the CLI and is generated. *Cross-command* |
| 153 | + narrative (the profile / workspace model, switching) stays as a hand-written |
| 154 | + docs concept page the command pages link to; it isn't any one command's help. |
| 155 | + |
| 156 | +## Prior art |
| 157 | + |
| 158 | +GitHub CLI (cobra): each command carries `Short` / `Long` / `Example` / flags; |
| 159 | +`gh help <cmd>` and the generated cli.github.com/manual both come from those, |
| 160 | +while conceptual pages (docs.github.com "About …") are separate. This proposal is |
| 161 | +the same split for `stash`. |
| 162 | + |
| 163 | +## Migration (incremental, non-breaking) |
| 164 | + |
| 165 | +1. Add the descriptor type + registry and `stash manifest --json`, populated from |
| 166 | + the existing help data. Additive; nothing changes for users. **Unblocks docs.** |
| 167 | +2. Render the top-level `--help` from the registry; delete the `HELP` string. |
| 168 | +3. Add `renderCommandHelp`; route `stash <cmd> --help` to it; remove `auth`'s |
| 169 | + bespoke help. Do it group by group (Setup → EQL/DB → Encrypt → …). |
| 170 | +4. Backfill `long` + `examples` per command as the reference content matures. |
| 171 | + |
| 172 | +## Open questions |
| 173 | + |
| 174 | +- Adopt a tiny declarative arg layer, or keep the hand-rolled parser and only add |
| 175 | + the metadata registry? (Registry-only is the smaller change.) |
| 176 | +- Where do `long` / `examples` strings live — inline in the descriptor, or in |
| 177 | + co-located `.md`/`.ts` files per command to keep `main.ts` lean? |
| 178 | +- Policy on `long` length — keep it reference-grade, push deeper concepts to docs. |
0 commit comments