Skip to content

Commit 74e6bfc

Browse files
platypiiphilcunliffeclaude
authored
Collapse top-level CLI help into group rows with registry-backed sub help (#284)
* Collapse top-level CLI help into group rows with registry-backed sub help One row per command group at the top level; 'hyp <group> --help' now lists each subcommand with its registered summary. Dispatch intercepts a leading --help for every command (group table or usage + optional long help), so command bodies stay help-free. Documented in LLP 0009. Also includes dependency bumps in package.json (@aws-sdk, squirreling). * Restore --bin in daemon install help under central --help interception Central `--help` interception (this PR) renders the registration `usage` for a leaf command instead of running the command body. `runDaemonInstall` still supports `--bin <path>`, and its now-bypassed self-help documented it, but the registration `usage` omitted it, so `hyp daemon install --help` silently dropped the flag. Add `--bin <path>` to the registration usage so the primary help path matches what the command accepts, plus a regression test asserting the flag stays documented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Phillip Cunliffe <phillip.cunliffe@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6877fe1 commit 74e6bfc

12 files changed

Lines changed: 458 additions & 163 deletions

hypaware-plugin-kernel-types.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,12 @@ export interface CommandRegistration {
626626
plugin?: PluginName
627627
summary: string
628628
usage: string
629+
/**
630+
* Optional longer help text (may be multi-line). Rendered by the core
631+
* help renderer after the usage line for `hyp <name> --help`; the
632+
* summary stays one line for command listings.
633+
*/
634+
help?: string
629635
aliases?: string[]
630636
hidden?: boolean
631637
run(argv: string[], ctx: CommandRunContext): Promise<number>

llp/0009-cli-registry.spec.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,44 @@ help by being omitted from the manifest (it is still registered imperatively in
5454
`activate`). Discovery is best-effort: if it fails, help degrades to core
5555
commands rather than failing.
5656

57+
### Layered help
58+
59+
`hyp --help` renders **one row per top-level command token**, sorted; a
60+
command with subcommands appears only as its group (`query`, `daemon`,
61+
`plugin`, ...), never as individual `query sql` / `daemon install` rows. The
62+
per-subcommand summaries move down a level into group help (`hyp query
63+
--help`), which lists each direct child with its registered summary. Both
64+
levels read the same registry, so they cannot drift.
65+
66+
Where a group row's summary comes from:
67+
68+
- A registered **bare command** (`query`, `daemon`, `backfill`, or a plugin's
69+
bare `vector`) speaks for its group; its summary should name the headline
70+
subcommands (`Manage the HypAware daemon (install, start, stop, status,
71+
...)`).
72+
- A group with **no bare command** (plugin namespaces like `graph`) gets a
73+
synthesized `Subcommands: a, b, c` summary, so the row still says where to
74+
go next.
75+
76+
Core groups whose bare command exists only for help (`query`, `daemon`,
77+
`sink`, `remote`, `config`, `plugin`, `agents`, `skills`) are built by one
78+
registry-backed factory (`makeGroupCommand`): no args or `--help` renders the
79+
group's subcommand table; any other token is an unknown-subcommand error
80+
listing the registered children. Command bodies do not hand-maintain
81+
subcommand lists.
82+
83+
### Central help interception
84+
85+
Dispatch owns `--help` for every registered command: when the argv right
86+
after a matched command name is `--help`/`-h`, core renders registry-backed
87+
help instead of running the command (the group table when the command has
88+
visible children, otherwise `summary` + `usage` + the optional long `help`
89+
text on the registration). Commands therefore stay help-free; a command
90+
needing more than one line of explanation sets `CommandRegistration.help`
91+
(multi-line allowed) rather than printing its own. The interception still
92+
runs inside the `command.run` span, so help usage stays visible in command
93+
analytics under the real command name.
94+
5795
## No cross-plugin option injection
5896

5997
Plugins may **not** inject options into commands they do not own. This keeps

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@
3939
"smoke": "node ./hypaware-core/smoke/index.js"
4040
},
4141
"dependencies": {
42-
"@aws-sdk/client-s3": "3.1079.0",
43-
"@aws-sdk/credential-provider-ini": "3.972.60",
42+
"@aws-sdk/client-s3": "3.1081.0",
43+
"@aws-sdk/credential-provider-ini": "3.972.62",
4444
"hyparquet": "1.26.2",
4545
"hyparquet-compressors": "1.1.1",
4646
"icebird": "0.8.13",
47-
"squirreling": "0.13.0"
47+
"squirreling": "0.14.0"
4848
},
4949
"optionalDependencies": {
5050
"hyparquet-writer": "0.16.1",

src/core/cli/core_commands.js

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// @ts-check
22

33
import { runBackfill, runBackfillList, runBackfillPlan } from '../commands/backfill.js'
4-
import { runRemoteAdd, runRemoteHelp, runRemoteList, runRemoteLogin, runRemoteRemove } from './remote_commands.js'
4+
import { runRemoteAdd, runRemoteList, runRemoteLogin, runRemoteRemove } from './remote_commands.js'
55
import { CORE_VERBS } from './core_verbs.js'
66
import { verbToCommand } from './verb_command.js'
7+
import { makeGroupCommand } from './group_help.js'
78
import { runStatus } from '../commands/status.js'
89
import {
9-
runQuery,
1010
runQueryMaintain,
1111
runQueryRefresh,
1212
runQuerySchema,
@@ -22,9 +22,8 @@ import {
2222
runPluginRemove,
2323
runPluginUpdate,
2424
} from '../commands/plugin.js'
25-
import { runConfig, runConfigValidate } from '../commands/config.js'
25+
import { runConfigValidate } from '../commands/config.js'
2626
import {
27-
runDaemonHelp,
2827
runDaemonInstall,
2928
runDaemonRestart,
3029
runDaemonRun,
@@ -35,7 +34,7 @@ import {
3534
} from '../commands/daemon.js'
3635
import { runMcp } from '../commands/mcp.js'
3736
import { runSmoke, runVersion } from '../commands/misc.js'
38-
import { runSinkForce, runSinkHelp, runSinkMaintain } from '../commands/sink.js'
37+
import { runSinkForce, runSinkMaintain } from '../commands/sink.js'
3938
import { runInit } from '../commands/init.js'
4039
import { runJoin, runLeave } from '../commands/central.js'
4140
import {
@@ -66,7 +65,7 @@ import {
6665
* @param {CommandRegistryExtended} registry
6766
*/
6867
export function registerCoreCommands(registry) {
69-
for (const cmd of buildCoreCommands()) {
68+
for (const cmd of buildCoreCommands(registry)) {
7069
registry.register(cmd)
7170
}
7271
// Project the intrinsic core verbs (query_sql) as CLI commands here too,
@@ -78,21 +77,23 @@ export function registerCoreCommands(registry) {
7877
}
7978
}
8079

81-
/** @returns {CommandRegistration[]} */
82-
function buildCoreCommands() {
80+
/**
81+
* @param {CommandRegistryExtended} registry
82+
* @returns {CommandRegistration[]}
83+
*/
84+
function buildCoreCommands(registry) {
8385
return [
8486
{
8587
name: 'status',
8688
summary: 'Show kernel status (active plugins, sources, sinks, cache)',
8789
usage: 'hyp status [--json]',
8890
run: runStatus,
8991
},
90-
{
92+
makeGroupCommand({
93+
registry,
9194
name: 'query',
92-
summary: 'Query the local cache (see subcommands: schema, status, sql, refresh, maintain)',
93-
usage: 'hyp query <subcommand> [args...]',
94-
run: runQuery,
95-
},
95+
summary: 'Query the local cache (sql, schema, status, ...)',
96+
}),
9697
{
9798
name: 'query schema',
9899
summary: 'Print the schema for a dataset',
@@ -119,7 +120,7 @@ function buildCoreCommands() {
119120
},
120121
{
121122
name: 'backfill',
122-
summary: 'Import client history from registered backfill providers',
123+
summary: 'Import client history from backfill providers',
123124
usage: 'hyp backfill [provider...] [--since <iso>] [--until <iso>] [--retention-days <n>] [--dry-run] [--json]',
124125
run: runBackfill,
125126
},
@@ -135,6 +136,11 @@ function buildCoreCommands() {
135136
usage: 'hyp backfill plan [provider...] [--retention-days <n>] [--json]',
136137
run: runBackfillPlan,
137138
},
139+
makeGroupCommand({
140+
registry,
141+
name: 'plugin',
142+
summary: 'Manage plugins (install, list, update, remove, ...)',
143+
}),
138144
{
139145
name: 'plugin install',
140146
summary: 'Install a plugin from name, git URL, or local directory',
@@ -183,12 +189,11 @@ function buildCoreCommands() {
183189
usage: 'hyp plugin new <name> [--kind source|sink|dataset] [--dir <path>]',
184190
run: runPluginNew,
185191
},
186-
{
192+
makeGroupCommand({
193+
registry,
187194
name: 'config',
188-
summary: 'Inspect or operate on the HypAware config (subcommand: validate)',
189-
usage: 'hyp config <subcommand> [args...]',
190-
run: runConfig,
191-
},
195+
summary: 'Inspect or validate the HypAware config',
196+
}),
192197
{
193198
name: 'config validate',
194199
summary: 'Load and cross-validate the active config file',
@@ -205,12 +210,14 @@ function buildCoreCommands() {
205210
name: 'join',
206211
summary: 'Join a centrally-managed fleet (write seed config + install daemon)',
207212
usage: 'hyp join <url> [token] [--token-file <path>] [--bin <path>] [--no-daemon]',
213+
help: 'Token sources (pick one): positional argument, --token-file, or stdin.\nA bare argv token lands in shell history; scripts should prefer\n--token-file or stdin.',
208214
run: runJoin,
209215
},
210216
{
211217
name: 'leave',
212218
summary: 'Leave the centrally-managed fleet (stop forwarding + config pull, undo org-driven attaches)',
213219
usage: 'hyp leave',
220+
help: 'Disconnects this machine from its central server: stops forwarding and\nconfig pull, undoes org-driven client attaches, and removes the forward\ncredential. Keeps query sessions, the local config, and the daemon service.',
214221
run: runLeave,
215222
},
216223
{
@@ -228,39 +235,54 @@ function buildCoreCommands() {
228235
},
229236
{
230237
name: 'ignore',
231-
summary:
232-
'Write a .hypignore so HypAware never records this folder subtree, or --local-only to withhold from forwarding only (--check reports status)',
238+
summary: 'Exclude a folder subtree from recording or forwarding',
233239
usage: 'hyp ignore [path] [--check] [--json] [--local-only]',
240+
help: [
241+
'Writes a .hypignore so HypAware never records this folder subtree.',
242+
'With --local-only, keeps recording locally but withholds the subtree',
243+
'from forwarding. With --check, reports the current ignore status',
244+
'without writing anything.',
245+
].join('\n'),
234246
run: runIgnore,
235247
},
236248
{
237249
name: 'unignore',
238-
summary: 'Remove the governing .hypignore (or --local-only list entry) so recording/forwarding resumes',
250+
summary: 'Resume recording for a previously ignored folder',
239251
usage: 'hyp unignore [path] [--local-only]',
252+
help: 'Removes the governing .hypignore (or the --local-only list entry)\nso recording/forwarding resumes.',
240253
run: runUnignore,
241254
},
255+
makeGroupCommand({
256+
registry,
257+
name: 'skills',
258+
summary: 'Manage skills for AI clients',
259+
}),
242260
{
243261
name: 'skills install',
244262
summary: 'Install registered skills into AI client directories',
245263
usage: 'hyp skills install [--client <name>]',
246264
run: runSkillsInstall,
247265
},
266+
makeGroupCommand({
267+
registry,
268+
name: 'agents',
269+
summary: 'Manage subagents for AI clients',
270+
}),
248271
{
249272
name: 'agents install',
250273
summary: 'Install registered subagents into AI client directories',
251274
usage: 'hyp agents install [--client <name>]',
252275
run: runAgentsInstall,
253276
},
254-
{
277+
makeGroupCommand({
278+
registry,
255279
name: 'daemon',
256-
summary: 'Manage the HypAware daemon (subcommands: install, uninstall, run, start, stop, restart, status)',
257-
usage: 'hyp daemon <subcommand> [args...]',
258-
run: runDaemonHelp,
259-
},
280+
summary: 'Manage the HypAware daemon (install, start, stop, status, ...)',
281+
}),
260282
{
261283
name: 'daemon install',
262284
summary: 'Install the persistent user service (launchd / systemd)',
263-
usage: 'hyp daemon install [--config <path>] [--dry-run [--json]]',
285+
usage: 'hyp daemon install [--config <path>] [--bin <path>] [--dry-run [--json]]',
264286
run: runDaemonInstall,
265287
},
266288
{
@@ -299,12 +321,11 @@ function buildCoreCommands() {
299321
usage: 'hyp daemon restart',
300322
run: runDaemonRestart,
301323
},
302-
{
324+
makeGroupCommand({
325+
registry,
303326
name: 'sink',
304-
summary: 'Manage sink instances (subcommands: force, maintain)',
305-
usage: 'hyp sink <subcommand> [args...]',
306-
run: runSinkHelp,
307-
},
327+
summary: 'Manage sink instances (force, maintain)',
328+
}),
308329
{
309330
name: 'sink force',
310331
summary: 'Force the sink driver to fire a tick now (optionally for one instance)',
@@ -319,16 +340,15 @@ function buildCoreCommands() {
319340
},
320341
{
321342
name: 'mcp',
322-
summary: 'Serve this host\'s verbs as an MCP server over stdio (for AI clients)',
343+
summary: 'Serve this host\'s verbs as an MCP server for AI clients',
323344
usage: 'hyp mcp [--remote <target>]',
324345
run: runMcp,
325346
},
326-
{
347+
makeGroupCommand({
348+
registry,
327349
name: 'remote',
328-
summary: 'Manage remote MCP query targets and their tokens (subcommands: add, login, list, remove)',
329-
usage: 'hyp remote <subcommand> [args...]',
330-
run: runRemoteHelp,
331-
},
350+
summary: 'Manage remote MCP query targets and tokens',
351+
}),
332352
{
333353
name: 'remote add',
334354
summary: 'Register a remote MCP query target in local config',
@@ -339,6 +359,13 @@ function buildCoreCommands() {
339359
name: 'remote login',
340360
summary: 'Store the query-scoped token for a remote target (0600)',
341361
usage: 'hyp remote login <name> [--token-file <path>] [--no-forward] [--no-daemon]',
362+
help: [
363+
'Browser sign-in by default; --token-file/stdin for a static token,',
364+
'--org <name> to select an org, --no-browser to print the URL,',
365+
'--host <label> to override the forwarding host label (default: hostname),',
366+
'--no-forward to sign in for queries only (no fleet enrollment),',
367+
'--no-daemon to provision the sink without installing the service.',
368+
].join('\n'),
342369
run: runRemoteLogin,
343370
},
344371
{

0 commit comments

Comments
 (0)