Skip to content

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

@glennmichael123

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions