ReviewOS

stacks/stx

fix(cli): `bundle`, `pwa` and `images` are registered but rejected by the hand-maintained `knownCommands` gate

#1881
Closed glennmichael123 opened this 14 days ago · 1 comment
14 days ago

Summary

packages/stx/bin/cli.ts validates process.argv[1] against a hardcoded knownCommands array before CAC parses anything. That array has drifted from the actual cli.command(...) registrations, so three shipped commands are unreachable: bundle, pwa and images. Each is fully implemented, documented with .example() lines, and then refused by the gate with a "did you mean" suggestion for something else.

Repro (local HEAD)

$ stx bundle dist/ --text
❌ Unknown command: bundle
💡 Did you mean: build?

$ stx pwa
❌ Unknown command: pwa
💡 Did you mean: dev?

$ stx images
❌ Unknown command or path: images

All three appear in stx --help output as registered CAC commands.

Evidence in stx

packages/stx/bin/cli.ts:2755-2758

const knownCommands = [
  'docs', 'iconify', 'dev', 'a11y', 'build', 'components:build', 'start', 'build:native', 'compile', 'test', 'init', 'new',
  'format', 'perf', 'debug', 'doctor', 'status', 'watch', 'analyze', 'version', 'deploy',
  'interactive', 'i', 'story', 'story:build', 'story:test', 'screenshot'
]

:2772 if (!knownCommands.includes(command)) → :2800 console.error('❌ Unknown command: …') → process.exit(1), all before CAC sees argv.

Diffing that array against every .command('…') in the same file:

  • registered but missing from the gate: bundle (:1415), images (:1288), pwa (:566)
  • in the gate but not registered: i, new

bundle is a complete implementation — :1416-1423 declares -o/--output, --json, --text, --markdown, --no-open, --no-gzip, --threshold <size> and --top <n>, and the action imports analyze, formatBytes, parseSize from ../src/bundle-analyzer.

Impact (real app)

analyticshq ships dist/dashboard.html at 182,245 bytes and dist/login.html at 169,987 bytes with no size measurement anywhere in CI. Its scaffolded export-size.yml:26-29 is a fully commented-out antfu/export-size-action step pointing at ~50 framework-internal directories that do not exist in the app, so the job runs checkout + setup-bun on every PR and does nothing.

The command that would have replaced it — stx bundle dist/ --text --threshold 500KB, exit non-zero over budget — was in the CLI the app already invokes for stx build, and could not be run.

Prior art

CAC (and commander, yargs, oclif) already reject unknown commands from the registry they built. A second hand-maintained list of command names is a redundant source of truth that can only ever drift — the same failure shape as #1819, where a hand-maintained regex of runtime globals went stale.

Ask

  1. Delete knownCommands and derive the set from the CAC instance (cli.commands.map(c => c.name)), so registration is the only source of truth.
  2. Keep the stx <path> → stx dev <path> shorthand and the Levenshtein suggestion, both driven off that derived set.
  3. Add a test that asserts every .command() name is accepted by the gate, so this cannot drift again.
  4. Meanwhile, add bundle, images and pwa to the array and drop the two entries that are not registered.

Related: #1781, #1819

14 days ago

Fixed in 730d4f8.

bundle    stx/0.2.155     (was: ❌ Unknown command: bundle    → "Did you mean: build?")
pwa       stx/0.2.155     (was: ❌ Unknown command: pwa       → "Did you mean: dev?")
images    stx/0.2.155     (was: ❌ Unknown command or path: images)
new       stx/0.2.155     (alias of init — still works)
i         stx/0.2.155     (alias of interactive — still works)
nonsense  ❌ Unknown command or path: nonsense

The gate is derived from the registrations now, so it cannot drift again.

One correction to ask 4

new and i are unregistered strays — drop them

They are not strays, they are aliases: .alias('new') on init and .alias('i') on interactive. Dropping them would break both, and — more relevant to ask 1 — deriving the gate from cli.commands.map(c => c.name) has the same hole, because alias names are not command names.

So the derivation unions aliasNames in:

const knownCommands = cli.commands.flatMap(c => [c.name, ...(c.aliasNames ?? [])])

There is a test asserting new and i still resolve, specifically because the obvious version of this fix silently breaks two working commands while fixing three.

Note

The two slot content over caller signals failures currently on main are not from this change — they reproduce at the parent commit without it. Flagging so nobody attributes them here; they look like they belong to the router/signals work landing in parallel.

Sign in to comment on this issue.