Skip to content

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

Description

@glennmichael123

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(':', handler)` is dead — the literal event `:` 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions