src/skills/cli-framework-cli-commander/SKILL.md
Node.js CLI development with Commander.js and @clack/prompts - command structure, interactive prompts, wizard state machines, config hierarchies, exit codes, cancellation handling
npx skillsauth add agents-inc/skills cli-framework-cli-commanderInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Commander.js for command structure and option parsing. Use @clack/prompts for interactive UX (spinners, selects, confirms). Always handle Ctrl+C cancellation with
p.isCancel(). Use named exit code constants. UseparseAsync()for async actions. Structure commands in separate files. Resolve config with precedence: flag > env > project > global > default.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST handle SIGINT (Ctrl+C) gracefully and exit with appropriate codes)
(You MUST use p.isCancel() to detect cancellation in ALL @clack/prompts calls and handle gracefully)
(You MUST use named constants for ALL exit codes - NEVER use magic numbers like process.exit(1))
(You MUST use parseAsync() for async actions to properly propagate errors)
(You MUST stop spinners before any console output or error display)
</critical_requirements>
Auto-detection: Commander.js, commander, @clack/prompts, picocolors, p.spinner, p.select, p.confirm, p.text, p.isCancel, p.tasks, p.multiselect, p.group, process.exit, exit codes, SIGINT handling, interactive prompts, wizard state machine, config hierarchy, CLI error handling, parseAsync, subcommand
When to use:
When NOT to use:
Key patterns covered:
p.isCancel()) on every promptDetailed Resources:
User experience first. CLI tools should be intuitive, provide helpful feedback, and fail gracefully. Users should always know what's happening (spinners), what went wrong (clear errors), and how to fix it (actionable messages).
Consistency across commands. Every command follows the same patterns: options at top, spinner feedback, success/error messaging, and proper exit codes. This makes the CLI predictable and learnable.
Graceful degradation. Always handle cancellation (Ctrl+C), invalid input, and errors. Never leave users in an unknown state. Stop spinners before displaying errors.
When to use Commander.js:
When to use @clack/prompts:
Register commands, handle SIGINT, use parseAsync() for async error propagation. See examples/core.md for full implementation.
// Handle Ctrl+C gracefully
process.on("SIGINT", () => {
console.log(pc.yellow("\nCancelled"));
process.exit(EXIT_CODES.CANCELLED);
});
// Use parseAsync for proper async error handling
await program.parseAsync(process.argv);
Define all exit codes as named constants. Never use magic numbers. See examples/core.md for the full constant definition.
export const EXIT_CODES = {
SUCCESS: 0,
ERROR: 1,
INVALID_ARGS: 2,
CANCELLED: 4,
VALIDATION_ERROR: 7,
} as const;
// GOOD: Named constant
process.exit(EXIT_CODES.VALIDATION_ERROR);
// BAD: Magic number
process.exit(1); // What does 1 mean?
Structure commands with typed options, descriptions for help text, and global option access. See examples/core.md for full implementation.
export const initCommand = new Command("init")
.description("Initialize the project")
.option("--source <url>", "Source URL")
.option("-f, --force", "Overwrite existing files", false)
.action(async (options, command) => {
const globalOpts = command.optsWithGlobals();
// ...
});
Every @clack/prompts call must be followed by p.isCancel(). See examples/core.md for spinner, select, confirm, and text patterns.
const result = await p.select({
message: "Select a framework:",
options: [
{ value: "react", label: "React", hint: "recommended" },
{ value: "vue", label: "Vue" },
],
});
// CRITICAL: Always check for cancellation
if (p.isCancel(result)) {
p.cancel("Setup cancelled");
process.exit(EXIT_CODES.CANCELLED);
}
Group related commands under parent commands. See examples/core.md for full implementation.
export const configCommand = new Command("config").description(
"Manage configuration",
);
configCommand
.command("show")
.description("Show current effective configuration")
.action(async () => {
/* ... */
});
configCommand
.command("set")
.argument("<key>", "Configuration key")
.argument("<value>", "Configuration value")
.action(async (key, value) => {
/* ... */
});
Complex multi-step flows with back navigation. See examples/wizard-patterns.md for full state machine implementation.
const state = createInitialState();
while (true) {
switch (state.currentStep) {
case "approach": {
const result = await stepApproach(state);
if (p.isCancel(result)) return null;
pushHistory(state);
state.currentStep = "selection";
break;
}
case "selection": {
const result = await stepSelection(state);
if (result === BACK_VALUE) {
state.currentStep = popHistory(state) || "approach";
break;
}
// ...
}
}
}
Resolve config values with clear precedence: flag > env > project > global > default. See examples/wizard-patterns.md for full implementation.
export async function resolveSource(
flagValue?: string,
projectDir?: string,
): Promise<ResolvedConfig> {
if (flagValue !== undefined)
return { source: flagValue, sourceOrigin: "flag" };
const envValue = process.env[SOURCE_ENV_VAR];
if (envValue) return { source: envValue, sourceOrigin: "env" };
// ... project config, global config, default
}
Preview operations without executing. See examples/wizard-patterns.md for full implementation.
export async function executeWithDryRun(
dryRun: boolean,
operations: Array<{ description: string; execute: () => Promise<void> }>,
): Promise<void> {
if (dryRun) {
for (const op of operations) {
console.log(pc.yellow(`[dry-run] Would: ${op.description}`));
}
return;
}
// Execute for real with spinner feedback
}
</patterns>
<decision_framework>
Is it a single operation?
├─ YES → Single command with options
└─ NO → Are operations related?
├─ YES → Subcommands under parent (config show, config set)
└─ NO → Separate top-level commands
Does user need to provide input?
├─ NO → Use options/flags only
└─ YES → Is it a simple yes/no?
├─ YES → p.confirm()
└─ NO → Is it choosing from options?
├─ YES → p.select() or p.multiselect()
└─ NO → Is it free-form text?
└─ YES → p.text() with validation
Is operation quick (< 500ms)?
├─ YES → No spinner needed
└─ NO → Use p.spinner() with:
├─ start("Descriptive message...")
├─ stop("Success with result info")
└─ Error: stop first, then p.log.error()
Check in order, first defined wins:
1. --flag (CLI argument)
2. ENV_VAR (environment variable)
3. ./.myapp/config.yaml (project config)
4. ~/.myapp/config.yaml (global config)
5. DEFAULT_VALUE (hardcoded default)
</decision_framework>
<red_flags>
High Priority Issues:
p.isCancel() checks after prompts — causes undefined behavior on Ctrl+C.parse() instead of .parseAsync() with async actions — swallows errors silentlyMedium Priority Issues:
--help descriptions for optionsCommon Mistakes:
process.exit() after p.cancel() — execution continues past cancellationprogram.parse() then trying to catch errors — parseAsync() required for async error propagationGotchas & Edge Cases:
--my-option to myOption in camelCase automaticallyoptsWithGlobals() needed to access parent command options (not just opts())console.log / p.log outputprocess.exit() in async context may not wait for pending I/O — use await before exit-triggering operationsallowExcessArguments to false — extra positional args are now errors.isCancelled property and .cancel() / .error() methods for richer feedback</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST handle SIGINT (Ctrl+C) gracefully and exit with appropriate codes)
(You MUST use p.isCancel() to detect cancellation in ALL @clack/prompts calls and handle gracefully)
(You MUST use named constants for ALL exit codes - NEVER use magic numbers like process.exit(1))
(You MUST use parseAsync() for async actions to properly propagate errors)
(You MUST stop spinners before any console output or error display)
Failure to follow these rules will result in poor UX, orphaned processes, and debugging nightmares.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety