| name | cli-creation |
| description | Build professional command-line interface (CLI) applications following industry best practices. Covers argument parsing, help text, output formatting, error handling, subcommands, configuration, and distribution across multiple languages. |
CLI Creation
Build professional, delightful command-line applications that follow established conventions and modern best practices.
When to Use This Skill
- Creating new CLI tools or utilities
- Adding command-line interfaces to existing applications
- Refactoring CLI programs for better usability
- User asks to "build a CLI", "create a command-line tool", or "add CLI commands"
Core Philosophy
Human-First Design
Design for humans first, machines second. The CLI is a text-based UI, not just a scripting interface.
Simple Parts That Work Together
Follow UNIX philosophy: small programs with clean interfaces that compose via pipes and standard I/O.
Consistency
Follow existing patterns. Terminal conventions are muscle memory—don't break expectations.
Ease of Discovery
Help users learn. Provide comprehensive help, examples, and suggestions.
Robustness
Handle errors gracefully. Be responsive. Show progress for long operations.
Project Structure
mycli/
├── src/
│ ├── main.{ts,py,go,rs} # Entry point
│ ├── commands/ # Subcommands
│ │ ├── init.{ts,py,go,rs}
│ │ └── run.{ts,py,go,rs}
│ ├── utils/
│ │ ├── output.{ts,py,go,rs} # Colors, formatting
│ │ └── config.{ts,py,go,rs} # Configuration handling
│ └── types.{ts,py,go,rs}
├── tests/
├── README.md
└── package.json / pyproject.toml / go.mod / Cargo.toml
Recommended Libraries
| Language | Library | Notes |
|---|
| Node.js | oclif, Commander | oclif for complex CLIs, Commander for simple |
| Python | Typer, Click | Typer uses type hints, Click is battle-tested |
| Go | Cobra, urfave/cli | Cobra powers kubectl, Hugo, GitHub CLI |
| Rust | clap | Derive macros for type-safe parsing |
| Deno | parseArgs | Built-in standard library |
Arguments and Flags
Terminology
- Arguments (args): Positional parameters (
cp source dest)
- Flags: Named parameters (
--verbose, -f file.txt)
Best Practices
mycli --input file.txt --output result.json
mycli file.txt result.json
mycli -v
mycli --verbose
-h, --help
-v, --version
-q, --quiet
-f, --force
-n, --dry-run
-o, --output
--json
--no-color
Flag Conventions
import { Command } from 'commander';
const program = new Command()
.name('mycli')
.description('A delightful CLI tool')
.version('1.0.0')
.option('-v, --verbose', 'Enable verbose output')
.option('-c, --config <path>', 'Config file path')
.option('--dry-run', 'Preview changes without applying');
import typer
app = typer.Typer()
@app.command()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
config: str = typer.Option(None, "--config", "-c", help="Config file path"),
dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview changes"),
):
"""A delightful CLI tool."""
pass
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "A delightful CLI tool",
}
func init() {
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output")
rootCmd.PersistentFlags().StringP("config", "c", "", "Config file path")
rootCmd.Flags().Bool("dry-run", false, "Preview changes")
}
Help Text
Display Help When Asked
Respond to -h, --help, and help subcommand:
$ mycli --help
A delightful CLI tool for managing widgets
Usage: mycli [command] [options]
Commands:
init Initialize a new project
build Build the project
deploy Deploy to production
Options:
-v, --verbose Enable verbose output
-c, --config Path to config file
-h, --help Show this help
--version Show version
Examples:
$ mycli init my-project
$ mycli build --verbose
$ mycli deploy --dry-run
Run 'mycli <command> --help' for more information on a command.
Help Text Principles
- Lead with examples - Users learn from examples first
- Show common flags first - Most-used options at the top
- Be concise by default - Full help on
--help, brief on no args
- Suggest next steps - Tell users what to run next
- Link to documentation - Include URLs to web docs
Output
Human-Readable by Default
if [ -t 1 ]; then
else
fi
Machine-Readable with --json
$ mycli status
✓ Connected to server
✓ 3 widgets deployed
✓ Last sync: 2 minutes ago
$ mycli status --json
{"connected": true, "widgets": 3, "lastSync": "2025-01-24T10:30:00Z"}
Color Guidelines
import chalk from 'chalk';
console.log(chalk.green('✓'), 'Success: Widget deployed');
console.log(chalk.yellow('⚠'), 'Warning: Deprecated API');
console.log(chalk.red('✗'), 'Error: Connection failed');
if (process.env.NO_COLOR || !process.stdout.isTTY) {
chalk.level = 0;
}
Progress Indicators
from tqdm import tqdm
import time
for item in tqdm(items, desc="Processing"):
process(item)
Errors
Write Errors for Humans
Error: ENOENT
✗ Error: Cannot find config file 'widget.yaml'
The file doesn't exist at the expected location.
To fix this, either:
• Run 'mycli init' to create a new config
• Specify a path with --config <path>
Error Handling Pattern
try {
await deploy();
} catch (error) {
if (error.code === 'ENOENT') {
console.error(chalk.red('✗'), `File not found: ${error.path}`);
console.error('\n Run "mycli init" to create the required files.\n');
process.exit(1);
}
if (error.code === 'ECONNREFUSED') {
console.error(chalk.red('✗'), 'Cannot connect to server');
console.error(`\n Check that the server is running at ${serverUrl}\n`);
process.exit(1);
}
console.error(chalk.red('✗'), 'Unexpected error:', error.message);
console.error('\n Please report this issue:');
console.error(' https://github.com/org/mycli/issues\n');
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
Subcommands
Git-Style Subcommands
mycli <command> [subcommand] [options]
mycli config get theme
mycli config set theme dark
mycli widget list
mycli widget create --name "My Widget"
Subcommand Consistency
mycli config get
mycli config set
mycli widget list
mycli widget create
mycli get config
mycli list widgets
Configuration
Configuration Precedence (highest to lowest)
- Command-line flags
- Environment variables
- Project-level config (
.myclirc, mycli.config.js)
- User-level config (
~/.config/mycli/config.yaml)
- System-wide config (
/etc/mycli/config.yaml)
XDG Base Directory Spec
import os from 'os';
import path from 'path';
const configDir = process.env.XDG_CONFIG_HOME
|| path.join(os.homedir(), '.config');
const configPath = path.join(configDir, 'mycli', 'config.yaml');
Environment Variables
MYCLI_API_KEY=xxx
MYCLI_DEBUG=1
MYCLI_NO_COLOR=1
NO_COLOR
DEBUG
EDITOR
Interactivity
Only Prompt in TTY
import { stdin, stdout } from 'process';
if (stdin.isTTY && stdout.isTTY) {
const answer = await prompt('Continue? [y/N]');
} else {
if (!options.force) {
console.error('Use --force to confirm in non-interactive mode');
process.exit(1);
}
}
Confirm Dangerous Operations
const confirm = await prompt('Delete file.txt? [y/N]');
const confirm = await prompt('Delete 47 files? Type "yes" to confirm:');
if (confirm !== 'yes') process.exit(1);
const confirm = await prompt('Delete production database? Type "prod-db" to confirm:');
if (confirm !== 'prod-db') process.exit(1);
if (options.force) {
}
Signals and Exit Codes
Exit Codes
process.exit(0);
process.exit(1);
process.exit(2);
Handle Ctrl-C Gracefully
process.on('SIGINT', async () => {
console.log('\n\nInterrupted. Cleaning up...');
await cleanup();
process.exit(130);
});
let interrupted = false;
process.on('SIGINT', () => {
if (interrupted) {
console.log('\nForce quitting...');
process.exit(1);
}
interrupted = true;
console.log('\nGracefully stopping... (press Ctrl+C again to force)');
gracefulShutdown();
});
Distribution
Single Binary is Best
- Go: Compiles to single binary by default
- Rust: Compiles to single binary by default
- Node.js: Use pkg or nexe
- Python: Use PyInstaller or shiv
Package Managers
npm install -g mycli
brew install mycli
pip install mycli
go install github.com/org/mycli@latest
Make Uninstall Easy
Document how to remove your tool at the bottom of install instructions.
Checklist
Essential (Must Have)
Recommended (Should Have)
Nice to Have
Anti-Patterns to Avoid
mycli --flag subcommand
mycli subcommand --flag
mycli dostuff
mycli update
mycli upgrade
mycli --old-flag
mycli --password=secret
mycli --password-file=/path/to/secret
echo "secret" | mycli --password-stdin
Resources