ReviewOS

stacks/clapp

Restore prefix-scoped wildcard listeners (cli.on('namespace:*', ...)) — emits only generic command:* today

#1810
Open glennmichael123 opened this 8 days ago ·
8 days ago

Background

When an unknown command is parsed, clapp emits only the generic `command:*` event:

```js // dist/index.js:1250-1256 if (!this.matchedCommand && this.args[0]) { this.emit('command:'); const hasWildcardListener = this.listenerCount('command:') > 0; if (!hasWildcardListener) { this.showCommandNotFound(this.args[0]); } } ```

This means `cli.on('<prefix>:', handler)` is dead — the literal event `<prefix>:` is never emitted. Historically (CAC v6-style behavior) listeners like `cli.on('migrate:*', ...)` would fire for any unknown subcommand of `migrate`, letting commands provide their own "did-you-mean" or contextual error path.

This blocks Stacks framework default templates (see stacksjs/stacks#893): `app/Commands/Inspire.ts` and `app/Listeners/Console.ts` both register `cli.on('inspire:*', ...)` expecting it to fire on `buddy inspire:bogus` — it doesn't. The user just sees the generic "Command not found" output.

Proposed behavior

When emitting for an unknown command:

```js const arg = this.args[0] this.emit('command:*', arg)

if (typeof arg === 'string' && arg.includes(':')) { const prefix = arg.slice(0, arg.indexOf(':')) this.emit(`${prefix}:*`, arg) }

const hasWildcardListener = this.listenerCount('command:') > 0 || (typeof arg === 'string' && arg.includes(':') && this.listenerCount(`${arg.slice(0, arg.indexOf(':'))}:`) > 0)

if (!hasWildcardListener) { this.showCommandNotFound(this.args[0]) } ```

Two small changes from current:

  1. Pass the unknown-command string as a payload to `command:*` (today's emit has no payload, which is also a footgun — handlers can't see which command was unknown without reading `cli.args`).
  2. When the unknown command contains a colon, also emit `${prefix}:*` with the same payload, and count those listeners toward the "someone handled it" check so we don't show the default not-found message.

Acceptance

  • `cli.on('foo:*', handler)` fires for unknown `foo:bar` / `foo:baz` commands; handler receives the unknown command string
  • `cli.on('command:*', handler)` still fires (backward compatible — it's the generic catchall)
  • Default "Command not found" only prints when no wildcard listener (generic OR prefix-scoped) is registered
  • Tests covering: prefix wildcard fires, generic wildcard fires, both registered (both fire), neither registered (not-found prints)

Once this lands, the Stacks-side fix on #893 is a no-op — the existing templates start working.

Sign in to comment on this issue.