Command Handlers & do/config¶
Each mcc <command> subcommand is implemented as a handler class in
src/lib/<command>-command-handler.js. bin/cli.js is a thin dispatcher: it
registers the Commander subcommand, lazily imports the handler's default export,
constructs it, and calls handle(...). See
system-overview.md for where this sits;
ADR-005 for the contract decision.
The handler contract¶
BaseCommandHandler (src/lib/base-command-handler.js) captures what handlers
genuinely share:
this.GENERATOR_ROOT/this.TEMPLATE_DIR(also named exports) — the generator package root and templates dir, computed once instead of copy-pasted.fail(message, { exit = true })— the standard failure convention:exit: truehard-exits (fatal),exit: falsesetsprocess.exitCode(soft, keep-going). Unifies the two error styles that existed across handlers.async handle()— abstract; throws if a subclass forgets to override it. The contract is "there is an asynchandle," not a fixed signature.
What is intentionally NOT uniform¶
handle(...)arity varies by command (see table). This is essential, not incidental — each command's inputs differ, andbin/cli.jsknows each shape at its call site. Do not add a translation layer to force uniformity.- Constructor options are per-command bags; the only rule is
super().
The eight handlers¶
| Command | Handler | handle signature |
Extends base? |
|---|---|---|---|
import |
ImportCommandHandler | handle(endpointArn) |
✓ |
update |
UpdateCommandHandler | handle() |
✓ |
regenerate |
RegenerateCommandHandler | handle() |
✓ |
bootstrap |
BootstrapCommandHandler | handle(args, options) |
— (incremental) |
architecture |
ArchitectureCommandHandler | handle(args, options) |
— |
prove |
ProveCommandHandler | handle(args, options) |
— |
mcp |
McpCommandHandler | handle(args, options) |
— |
secrets |
SecretsCommandHandler | handle(args, options) |
— |
The three that extend the base are the ones that shared the copy-pasted
GENERATOR_ROOT/TEMPLATE_DIR block. The other five don't need the path block
and may adopt the base incrementally; they are not forced to.
do/config parsing (shared)¶
update, regenerate, and the validate path all read a generated project's
do/config shell file. That parsing is a single utility,
src/lib/do-config.js:
parseDoConfig(configPath, { resolveShellDefaults })— returns a rawKEY → valuemap (ornullif the file is missing). WithresolveShellDefaults: trueit resolves${VAR:-default}→default(the validate path needs resolved values; update/regenerate do not).shellVarsToAnswers(shellVars)— maps shell KEYs to camelCase answer keys via the canonical frozenSHELL_VAR_TO_ANSWER(the superset union of the mappings the handlers previously each carried).
Before Wave 4 this logic existed in four places (update, regenerate, validate-runner, and a replica inside a test). It is now one module.
Adding a command handler¶
- Create
src/lib/<command>-command-handler.jswithexport default class <Command>CommandHandler extends BaseCommandHandler. - Call
super()in the constructor; take a command-specific options bag. - Implement
async handle(...)with whatever signature the command needs. - Use
this.fail(msg)/this.fail(msg, { exit: false })for errors, andthis.GENERATOR_ROOT/this.TEMPLATE_DIRfor paths. - Register the subcommand in
bin/cli.js(lazyimport()+new ...().handle). - If it reads
do/config, useparseDoConfig/shellVarsToAnswersfromsrc/lib/do-config.js— never re-implement the parser.