Skip to content

Commit 7f67e11

Browse files
authored
Merge pull request #558 from cipherstash/docs/cli-help-and-manifest-proposal
docs(cli): proposal — command-descriptor registry for help + `stash manifest --json`
2 parents 93b2929 + 1ed5f4b commit 7f67e11

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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

Comments
 (0)