Agent skill
cli-development
CLI development patterns with Commander.js, argument parsing, and user experience best practices. Use when creating commands, handling options, formatting output, or building CLI tools.
Install this agent skill to your Project
npx add-skill https://github.com/pwarnock/liaison-toolkit/tree/main/.skills/cli-development
Metadata
Additional technical details for this skill
- author
- liaison-toolkit
- version
- 1.0
- keywords
- cli, commander.js, argument-parsing, user-experience, terminal
SKILL.md
CLI Development
Patterns and best practices for building command-line interfaces using Commander.js, with focus on argument parsing, user experience, and output formatting.
When to use this skill
Use this skill when:
- Creating new CLI commands or subcommands
- Adding options, arguments, or flags to commands
- Parsing and validating user input
- Formatting CLI output (spinners, colors, tables)
- Handling errors and exit codes
- Writing help text and usage documentation
Commander.js Command Patterns
Basic Command Structure
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
export function createMyCommand(): Command {
const command = new Command('mycommand');
command
.description('Brief description of what this command does')
.option('-f, --force', 'Force operation without confirmation')
.option('-o, --output <path>', 'Output file path')
.action(handleMyCommand);
return command;
}
async function handleMyCommand(options: any): Promise<void> {
const spinner = ora('Processing...').start();
try {
// Command logic here
spinner.succeed(chalk.green('✅ Operation completed'));
} catch (error) {
spinner.fail(chalk.red(`❌ Failed: ${error instanceof Error ? error.message : String(error)}`));
process.exit(1);
}
}
Subcommands Pattern
From packages/liaison/src/commands/skill.ts:
export function createSkillCommand(): Command {
const command = new Command('skill');
command.description('Manage Agent Skills');
// Subcommand: liaison skill init
command
.command('init')
.description('Initialize Agent Skills in this project')
.option('--global', 'Initialize globally (~/.skills/)')
.option('--copy', 'Copy instead of symlink (Windows compatibility)')
.option('--location <path>', 'Custom skills location')
.action(initSkills);
// Subcommand: liaison skill create <name>
command
.command('create <name>')
.description('Create a new skill')
.option('--description <text>', 'Skill description')
.option('--template <type>', 'Skill template (workflow, library, qa, deployment)', 'workflow')
.option('--location <path>', 'Create skill at custom location')
.action(createSkill);
return command;
}
Argument and Option Patterns
Required Arguments
command
.command('create <name>') // Required argument
.action((name: string, options: any) => {
console.log(`Creating: ${name}`);
});
Optional Arguments
command
.command('validate [path]') // Optional argument (square brackets)
.action((path?: string) => {
const targetPath = path || '.skills';
});
Options with Values
command
.option('-o, --output <path>', 'Output file path') // Required value
.option('-t, --template <type>', 'Template type', 'workflow') // With default
.option('--format [fmt]', 'Output format', 'table') // Optional value
Boolean Flags
command
.option('-f, --force', 'Force operation')
.option('--no-cache', 'Disable caching') // Boolean negation
Input Validation
Validate Arguments
async function createSkill(name: string, options: any): Promise<void> {
const spinner = ora('Creating skill...').start();
// Validate skill name format
if (!name.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) {
spinner.fail(chalk.red('Invalid skill name. Use lowercase alphanumeric with hyphens only.'));
process.exit(1);
}
// Check if already exists
try {
await fs.access(skillPath);
spinner.fail(chalk.red(`Skill "${name}" already exists at ${skillPath}`));
process.exit(1);
} catch {
// Good, doesn't exist yet
}
// Proceed with creation...
}
Validate Options
async function listSkills(options: any): Promise<void> {
const validFormats = ['table', 'json', 'xml'];
if (options.format && !validFormats.includes(options.format)) {
console.error(chalk.red(`Invalid format: ${options.format}`));
console.error(chalk.yellow(`Valid formats: ${validFormats.join(', ')}`));
process.exit(1);
}
// Proceed...
}
Output Formatting
Using Spinners (ora)
import ora from 'ora';
const spinner = ora('Loading...').start();
// Update spinner text
spinner.text = 'Processing items...';
// Success
spinner.succeed(chalk.green('✅ Operation completed'));
// Warning
spinner.warn(chalk.yellow('⚠️ Warning message'));
// Failure
spinner.fail(chalk.red('❌ Operation failed'));
// Stop without status
spinner.stop();
Using Colors (chalk)
import chalk from 'chalk';
console.log(chalk.green('Success message'));
console.log(chalk.yellow('Warning message'));
console.log(chalk.red('Error message'));
console.log(chalk.blue('Info message'));
console.log(chalk.cyan('Highlight text'));
console.log(chalk.bold('Bold text'));
Table Output
From packages/liaison/src/commands/skill.ts:252-260:
// Format list as table
console.log(chalk.bold('\nAvailable Skills:\n'));
const table = skills
.map(skill =>
` ${chalk.cyan(skill.name.padEnd(30))} ${skill.description.substring(0, 60)}`
)
.join('\n');
console.log(table);
console.log(`\n Total: ${chalk.green(skills.length)} skill(s)\n`);
JSON Output
if (options.format === 'json') {
console.log(JSON.stringify(result, null, 2));
return;
}
Error Handling
Graceful Error Messages
try {
await performOperation();
} catch (error) {
console.error(chalk.red(`\n❌ Operation failed:\n`));
console.error(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));
if (options.verbose && error instanceof Error && error.stack) {
console.error(chalk.dim('\nStack trace:'));
console.error(chalk.dim(error.stack));
}
process.exit(1);
}
Exit Codes
// Success
process.exit(0);
// General error
process.exit(1);
// Invalid usage
process.exit(2);
// Validation error
process.exit(3);
Help Text and Documentation
Command Description
command
.description('Create a new skill') // Brief one-liner
.usage('[options] <name>') // Usage pattern
.addHelpText('after', `
Examples:
$ liaison skill create my-skill
$ liaison skill create my-skill --template library
$ liaison skill create my-skill --description "My custom skill"
`);
Custom Help
command.addHelpCommand(false); // Disable default help command
command.on('--help', () => {
console.log('');
console.log('Additional Information:');
console.log(' This command creates a new skill following the Agent Skills standard');
console.log(' Learn more: https://agentskills.io');
});
Common Patterns
Confirmation Prompts
import readline from 'readline';
async function confirmAction(message: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise(resolve => {
rl.question(`${message} (y/N): `, answer => {
rl.close();
resolve(answer.toLowerCase() === 'y');
});
});
}
// Usage
if (!options.force && await confirmAction('Delete all data?')) {
await deleteData();
}
Progress Indicators
for (let i = 0; i < items.length; i++) {
spinner.text = `Processing item ${i + 1} of ${items.length}...`;
await processItem(items[i]);
}
Verbose Mode
function log(message: string, verbose: boolean = false): void {
if (verbose) {
console.log(chalk.dim(`[DEBUG] ${message}`));
}
}
// Usage
command.option('-v, --verbose', 'Enable verbose output');
async function myAction(options: any): Promise<void> {
log('Starting operation...', options.verbose);
// ...
}
Verification
After implementing CLI features:
- Help text is clear and shows examples
- All options have descriptions
- Input validation provides helpful error messages
- Output is formatted consistently (colors, spinners)
- Exit codes are appropriate (0 for success, non-zero for errors)
- Error messages are user-friendly (no raw stack traces)
- Commands work with
--helpflag - Progress feedback for long operations
Examples from liaison-toolkit
Example 1: Skill Create Command
// packages/liaison/src/commands/skill.ts:168-226
async function createSkill(name: string, options: any): Promise<void> {
const spinner = ora('Creating skill...').start();
try {
// Validate skill name
if (!name.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) {
spinner.fail(chalk.red('Invalid skill name. Use lowercase alphanumeric with hyphens only.'));
process.exit(1);
}
const skillsDir = options.location || '.skills';
const skillPath = join(skillsDir, name);
// Check if skill already exists
try {
await fs.access(skillPath);
spinner.fail(chalk.red(`Skill "${name}" already exists at ${skillPath}`));
process.exit(1);
} catch {
// Good, doesn't exist yet
}
// Create skill directory
spinner.text = 'Creating skill directory...';
await fs.mkdir(skillPath, { recursive: true });
// Create subdirectories
await fs.mkdir(join(skillPath, 'references'), { recursive: true });
await fs.mkdir(join(skillPath, 'scripts'), { recursive: true });
await fs.mkdir(join(skillPath, 'assets'), { recursive: true });
// Generate and write SKILL.md
spinner.text = 'Creating SKILL.md...';
const skillContent = generateSkillTemplate(
name,
options.description || `Skill: ${name}`,
options.template,
);
await fs.writeFile(join(skillPath, 'SKILL.md'), skillContent);
spinner.succeed(chalk.green(`✅ Skill "${name}" created successfully`));
console.log(chalk.blue('\n📝 Next steps:'));
console.log(` 1. Edit: ${chalk.cyan(`${skillPath}/SKILL.md`)}`);
console.log(` 2. Add references: ${chalk.cyan(`${skillPath}/references/`)}`);
console.log(` 3. Validate: ${chalk.cyan(`liaison skill validate ${skillPath}`)}`);
} catch (error) {
spinner.fail(
chalk.red(
`Failed to create skill: ${error instanceof Error ? error.message : String(error)}`,
),
);
process.exit(1);
}
}
Example 2: Skill List Command
// packages/liaison/src/commands/skill.ts:232-269
async function listSkills(options: any): Promise<void> {
const spinner = ora('Discovering skills...').start();
try {
const locations = options.location ? [options.location] : ['.skills'];
const skills = await discoverSkills({ locations });
spinner.stop();
if (skills.length === 0) {
console.log(chalk.yellow('No skills found. Run: liaison skill create <name>'));
return;
}
if (options.format === 'json') {
console.log(JSON.stringify(skills, null, 2));
} else if (options.format === 'xml') {
console.log(generateAvailableSkillsXml(skills));
} else {
// Table format
console.log(chalk.bold('\nAvailable Skills:\n'));
const table = skills
.map(
(skill) =>
` ${chalk.cyan(skill.name.padEnd(30))} ${skill.description.substring(0, 60)}`,
)
.join('\n');
console.log(table);
console.log(`\n Total: ${chalk.green(skills.length)} skill(s)\n`);
}
} catch (error) {
spinner.fail(
chalk.red(
`Failed to list skills: ${error instanceof Error ? error.message : String(error)}`,
),
);
process.exit(1);
}
}
Related Resources
- Commander.js Documentation
- chalk (Terminal colors)
- ora (Spinners)
- Node.js readline
- CLI UX best practices: 12 Factor CLI Apps
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
security-scanning
Security scanning with dependency audits, secret detection, static analysis, and vulnerability management. Use when checking for vulnerabilities, scanning dependencies, or enforcing security gates.
bun-development
Bun build system, development workflow, and TypeScript patterns. Use when working with Bun projects, setting up development environment, or managing dependencies with Bun package manager.
library-research
Research library documentation and API references using Context7 integration. Use when users ask about libraries, frameworks, packages, APIs, or need documentation for third-party code.
release-publishing
Release workflow with changesets, version bumping, changelog generation, and npm publishing. Use when preparing releases, managing versions, or publishing to npm.
liaison-workflows
Task management and workflow patterns using liaison. Use when managing tasks, creating workflows, or working with task-driven automation.
git-automation
Automate Git workflows with atomic commits, Beads integration, and real-time sync. Use when managing Git commits, branches, task tracking, or handling complex multi-file changes with proper validation.
Didn't find tool you were looking for?