You are working on the CLI shell — the entry point that wires Commander subcommands, prints the welcome screen, warns about missing Claude hooks, dispatches to handlers, and translates CliError into a clean exit. Also the public programmatic surface re-exported from src/index.js.
Domain purpose
This layer is what a user actually invokes (aspens …) and what programmatic consumers import. It owns argument parsing, top-level error handling, and the welcome UX. All real work lives in src/commands/*.js — the shell only routes.
Business rules / invariants
- Handlers must throw
CliError, never call process.exit(). The top-level handler in bin/cli.js:250 catches it, prints Error: <message> (unless logged: true) in red, and exits with err.exitCode (default 1). Plain Error falls through to the same printer but always exits 1.
logged: true means "I already printed a user-friendly message" — top level then exits silently with the given code. Use it when the handler rendered a clack outro or multi-line failure already.
checkMissingHooks(repoPath) runs before doc sync, add, and customize — warns (does not throw) when .claude/skills/ exists but .claude/hooks/skill-activation-prompt.sh or .claude/skills/skill-rules.json is absent. Skipped entirely when .claude/skills/ is missing (nothing to activate).
- No-command invocation shows
showWelcome() — listing essential commands, generate/sync, Claude add-ons, utilities, options, typical workflow, and target notes. Adding a new subcommand requires updating this screen too.
- Template counts in the welcome are filesystem-derived —
countTemplates(subdir) reads src/templates/{agents,commands,hooks} and filters dotfiles; returns '?' on read failure (never throws).
- Version comes from
package.json at runtime via readFileSync; falls back to '0.0.0' silently if parse/read fails. Do not hardcode.
- Numeric option parsers throw
InvalidArgumentError (Commander-native) — parsePositiveInt rejects ≤0/NaN; parseCommits additionally caps at 50.
- Signal handlers exit with conventional codes — SIGINT→130, SIGTERM→143. Used to clean up spawned
claude -p / codex exec children.
Non-obvious behaviors
- Action wrappers chain
checkMissingHooks before the handler for doc sync, add, customize — done inline via arrow (args, options) => { checkMissingHooks(resolve(path)); return handler(...) }. Don't move this into the handler — the warning should fire even if the handler later fails or short-circuits.
program.parseAsync() is required (not .parse()) — handlers are async; .catch() on the returned promise is the only place plain errors are surfaced.
src/index.js is the public programmatic API — only re-exports scanRepo, runClaude, loadPrompt, parseFileOutput, writeSkillFiles, buildContext, buildBaseContext, buildDomainContext, analyzeImpact. Adding/removing a re-export is a breaking change for embedders; treat it as such.
Critical files
bin/cli.js — Commander setup, option parsers, welcome screen, signal handlers, top-level CliError catch.
src/lib/errors.js — CliError class with exitCode and logged options (plus optional cause).
src/index.js — Stable programmatic surface for library consumers.
Critical Rules
- New subcommand → register on
program (or the doc subgroup) and add it to showWelcome() so users discover it.
- Never swallow a handler error in the action wrapper — let it bubble to
program.parseAsync().catch().
- When a handler renders its own failure UX (clack/picocolors), throw
new CliError(msg, { logged: true, exitCode }) so the top level does not double-print.
Last Updated: 2026-05-11
1---2name: cli-shell3description: Top-level Commander wiring, welcome screen, missing-hook warning, CliError exit handling, and the public programmatic API surface4---56You are working on the **CLI shell** — the entry point that wires Commander subcommands, prints the welcome screen, warns about missing Claude hooks, dispatches to handlers, and translates `CliError` into a clean exit. Also the public programmatic surface re-exported from `src/index.js`.78## Domain purpose9This layer is what a user actually invokes (`aspens …`) and what programmatic consumers import. It owns argument parsing, top-level error handling, and the welcome UX. All real work lives in `src/commands/*.js` — the shell only routes.1011## Business rules / invariants12- **Handlers must throw `CliError`, never call `process.exit()`.** The top-level handler in `bin/cli.js:250` catches it, prints `Error: <message>` (unless `logged: true`) in red, and exits with `err.exitCode` (default 1). Plain `Error` falls through to the same printer but always exits 1.13- **`logged: true` means "I already printed a user-friendly message"** — top level then exits silently with the given code. Use it when the handler rendered a clack `outro` or multi-line failure already.14- **`checkMissingHooks(repoPath)` runs before `doc sync`, `add`, and `customize`** — warns (does not throw) when `.claude/skills/` exists but `.claude/hooks/skill-activation-prompt.sh` or `.claude/skills/skill-rules.json` is absent. Skipped entirely when `.claude/skills/` is missing (nothing to activate).15- **No-command invocation shows `showWelcome()`** — listing essential commands, generate/sync, Claude add-ons, utilities, options, typical workflow, and target notes. Adding a new subcommand requires updating this screen too.16- **Template counts in the welcome are filesystem-derived** — `countTemplates(subdir)` reads `src/templates/{agents,commands,hooks}` and filters dotfiles; returns `'?'` on read failure (never throws).17- **Version comes from `package.json`** at runtime via `readFileSync`; falls back to `'0.0.0'` silently if parse/read fails. Do not hardcode.18- **Numeric option parsers throw `InvalidArgumentError`** (Commander-native) — `parsePositiveInt` rejects ≤0/NaN; `parseCommits` additionally caps at 50.19- **Signal handlers exit with conventional codes** — SIGINT→130, SIGTERM→143. Used to clean up spawned `claude -p` / `codex exec` children.2021## Non-obvious behaviors22- **Action wrappers chain `checkMissingHooks` before the handler** for `doc sync`, `add`, `customize` — done inline via arrow `(args, options) => { checkMissingHooks(resolve(path)); return handler(...) }`. Don't move this into the handler — the warning should fire even if the handler later fails or short-circuits.23- **`program.parseAsync()` is required** (not `.parse()`) — handlers are async; `.catch()` on the returned promise is the only place plain errors are surfaced.24- **`src/index.js` is the public programmatic API** — only re-exports `scanRepo`, `runClaude`, `loadPrompt`, `parseFileOutput`, `writeSkillFiles`, `buildContext`, `buildBaseContext`, `buildDomainContext`, `analyzeImpact`. Adding/removing a re-export is a breaking change for embedders; treat it as such.2526## Critical files27- `bin/cli.js` — Commander setup, option parsers, welcome screen, signal handlers, top-level `CliError` catch.28- `src/lib/errors.js` — `CliError` class with `exitCode` and `logged` options (plus optional `cause`).29- `src/index.js` — Stable programmatic surface for library consumers.3031## Critical Rules32- New subcommand → register on `program` (or the `doc` subgroup) **and** add it to `showWelcome()` so users discover it.33- Never swallow a handler error in the action wrapper — let it bubble to `program.parseAsync().catch()`.34- When a handler renders its own failure UX (clack/picocolors), throw `new CliError(msg, { logged: true, exitCode })` so the top level does not double-print.3536---37**Last Updated:** 2026-05-11