Agent skill
cli-tool
Build command-line tools with argument parsing, subcommands, and nice UX. Use when: creating CLI, terminal app, command-line utility. Triggers: "cli", "command line", "terminal", "clap", "typer", "commander".
Install this agent skill to your Project
npx add-skill https://github.com/timequity/vibe-coder/tree/main/skills/cli-tool
SKILL.md
CLI Tool Development
Project Protection Setup
MANDATORY before writing any code:
# 1. Create .gitignore
cat >> .gitignore << 'EOF'
# Build
target/
node_modules/
__pycache__/
dist/
# Config with secrets
config.toml
*.key
credentials.json
# IDE
.idea/
.vscode/
.DS_Store
EOF
# 2. Setup pre-commit hooks
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
EOF
pre-commit install
Stack Options
| Language | Framework | Best For |
|---|---|---|
| Rust | clap (derive) | Fast binaries, type safety |
| Python | typer / click | Rapid development |
| Node | commander / yargs | JS ecosystem |
Quick Start
Rust (clap derive)
# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"
use clap::Parser;
#[derive(Parser)]
#[command(name = "mytool")]
#[command(about = "A sample CLI tool")]
struct Cli {
/// Input file
input: String,
/// Output file
#[arg(short, long, default_value = "output.txt")]
output: String,
/// Verbose output
#[arg(short, long)]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if cli.verbose {
println!("Input: {}", cli.input);
println!("Output: {}", cli.output);
}
// Do work...
Ok(())
}
Python (typer)
# requirements.txt
typer[all]>=0.9
import typer
app = typer.Typer()
@app.command()
def main(
input: str = typer.Argument(..., help="Input file"),
output: str = typer.Option("output.txt", "--output", "-o", help="Output file"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
):
"""A sample CLI tool."""
if verbose:
typer.echo(f"Input: {input}")
typer.echo(f"Output: {output}")
if __name__ == "__main__":
app()
Node (commander)
// package.json: "commander": "^12"
import { program } from 'commander';
program
.name('mytool')
.description('A sample CLI tool')
.argument('<input>', 'Input file')
.option('-o, --output <file>', 'Output file', 'output.txt')
.option('-v, --verbose', 'Verbose output')
.action((input, options) => {
if (options.verbose) {
console.log(`Input: ${input}`);
console.log(`Output: ${options.output}`);
}
});
program.parse();
Subcommands
Rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "mytool")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a new item
Add {
/// Item name
name: String,
},
/// List all items
List {
/// Show detailed info
#[arg(short, long)]
detailed: bool,
},
/// Remove an item
Remove {
/// Item ID
id: u32,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Add { name } => println!("Adding: {}", name),
Commands::List { detailed } => println!("Listing (detailed: {})", detailed),
Commands::Remove { id } => println!("Removing: {}", id),
}
}
Python
import typer
app = typer.Typer()
@app.command()
def add(name: str):
"""Add a new item."""
typer.echo(f"Adding: {name}")
@app.command()
def list(detailed: bool = typer.Option(False, "--detailed", "-d")):
"""List all items."""
typer.echo(f"Listing (detailed: {detailed})")
@app.command()
def remove(id: int):
"""Remove an item."""
typer.echo(f"Removing: {id}")
if __name__ == "__main__":
app()
Output Formats
Text / JSON / Table
use clap::ValueEnum;
use serde::Serialize;
#[derive(ValueEnum, Clone)]
enum OutputFormat {
Text,
Json,
Table,
}
#[derive(Serialize)]
struct Item {
id: u32,
name: String,
}
fn output(items: &[Item], format: OutputFormat) {
match format {
OutputFormat::Text => {
for item in items {
println!("{}: {}", item.id, item.name);
}
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(items).unwrap());
}
OutputFormat::Table => {
println!("{:<5} {}", "ID", "Name");
println!("{}", "-".repeat(20));
for item in items {
println!("{:<5} {}", item.id, item.name);
}
}
}
}
Python (rich)
from rich.console import Console
from rich.table import Table
import json
console = Console()
def output(items: list, format: str):
if format == "text":
for item in items:
console.print(f"{item['id']}: {item['name']}")
elif format == "json":
console.print_json(json.dumps(items))
elif format == "table":
table = Table()
table.add_column("ID")
table.add_column("Name")
for item in items:
table.add_row(str(item["id"]), item["name"])
console.print(table)
Progress Bars
Rust (indicatif)
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new(100);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap());
for i in 0..100 {
pb.set_position(i);
pb.set_message(format!("Processing item {}", i));
std::thread::sleep(std::time::Duration::from_millis(50));
}
pb.finish_with_message("Done!");
Python (rich)
from rich.progress import track
for item in track(range(100), description="Processing..."):
# Do work
pass
Config Files
Rust (config crate)
use config::{Config, File};
use serde::Deserialize;
#[derive(Deserialize)]
struct Settings {
api_key: String,
timeout: u64,
}
fn load_config() -> anyhow::Result<Settings> {
let settings = Config::builder()
.add_source(File::with_name("config.toml").required(false))
.add_source(config::Environment::with_prefix("MYTOOL"))
.build()?;
Ok(settings.try_deserialize()?)
}
# config.toml
api_key = "your-key"
timeout = 30
Python
import tomllib
from pathlib import Path
def load_config():
config_path = Path.home() / ".config" / "mytool" / "config.toml"
if config_path.exists():
return tomllib.loads(config_path.read_text())
return {}
Exit Codes
use std::process::ExitCode;
fn main() -> ExitCode {
match run() {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: {}", e);
ExitCode::FAILURE
}
}
}
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Misuse of command |
| 126 | Permission denied |
| 127 | Command not found |
Shell Completions
Rust (clap)
use clap::CommandFactory;
use clap_complete::{generate, Shell};
#[derive(Parser)]
struct Cli {
#[arg(long, value_enum)]
completions: Option<Shell>,
}
fn main() {
let cli = Cli::parse();
if let Some(shell) = cli.completions {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "mytool", &mut std::io::stdout());
return;
}
}
Usage:
# Generate completions
mytool --completions bash > ~/.local/share/bash-completion/completions/mytool
mytool --completions zsh > ~/.zfunc/_mytool
Interactive Prompts
Rust (dialoguer)
use dialoguer::{Confirm, Input, Select};
let name: String = Input::new()
.with_prompt("Your name")
.interact_text()?;
let proceed = Confirm::new()
.with_prompt("Continue?")
.interact()?;
let options = vec!["Option 1", "Option 2", "Option 3"];
let selection = Select::new()
.with_prompt("Choose")
.items(&options)
.interact()?;
Python (rich)
from rich.prompt import Prompt, Confirm
name = Prompt.ask("Your name")
proceed = Confirm.ask("Continue?")
Common Pitfalls
| Pitfall | Solution |
|---|---|
| No help text | Add descriptions to all args |
| Poor error messages | Use anyhow/thiserror with context |
| No colors in pipes | Detect TTY, use --color=always |
| Slow startup | Lazy init, avoid heavy deps |
| No config file | Support ~/.config/tool/config.toml |
Testing
Rust (clap)
#[cfg(test)]
mod tests {
use super::*;
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn test_cli_help() {
Command::cargo_bin("mytool")
.unwrap()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Usage"));
}
#[test]
fn test_cli_version() {
Command::cargo_bin("mytool")
.unwrap()
.arg("--version")
.assert()
.success();
}
#[test]
fn test_add_command() {
Command::cargo_bin("mytool")
.unwrap()
.args(["add", "test-item"])
.assert()
.success()
.stdout(predicate::str::contains("Added"));
}
#[test]
fn test_invalid_input_fails() {
Command::cargo_bin("mytool")
.unwrap()
.args(["add"]) // Missing required arg
.assert()
.failure();
}
}
Python (typer)
from typer.testing import CliRunner
from myapp import app
runner = CliRunner()
def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Usage" in result.output
def test_add_command():
result = runner.invoke(app, ["add", "test-item"])
assert result.exit_code == 0
assert "Added" in result.output
def test_invalid_input():
result = runner.invoke(app, ["add"]) # Missing arg
assert result.exit_code != 0
Node (commander)
import { describe, it, expect } from 'vitest';
import { execSync } from 'child_process';
describe('CLI', () => {
it('shows help', () => {
const output = execSync('node dist/cli.js --help').toString();
expect(output).toContain('Usage');
});
it('adds item', () => {
const output = execSync('node dist/cli.js add test-item').toString();
expect(output).toContain('Added');
});
});
TDD Workflow
1. Task[tdd-test-writer]: "Create 'add' subcommand"
→ Writes assert_cmd test
→ cargo test → FAILS (RED)
2. Task[rust-developer]: "Implement 'add' subcommand"
→ Implements minimal code
→ cargo test → PASSES (GREEN)
3. Repeat for each subcommand
4. Task[code-reviewer]: "Review CLI implementation"
→ Checks error messages, exit codes, edge cases
Security Checklist
- No secrets in default config
- Config file permissions checked (600 for sensitive)
- Input sanitized before shell execution
- No command injection in subprocesses
- Secure temp file handling
- Credentials stored in OS keyring (if needed)
Project Structure
mytool/
├── src/
│ ├── main.rs
│ ├── cli.rs # Argument definitions
│ ├── commands/ # Subcommand implementations
│ │ ├── mod.rs
│ │ ├── add.rs
│ │ └── list.rs
│ └── config.rs
├── tests/
│ └── cli_tests.rs # Integration tests
├── Cargo.toml
├── config.example.toml
└── README.md
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
mvp-help
Help and documentation for Idea to MVP plugin. Use when: user asks about building MVPs, vibe coding, or available commands. Triggers: "help", "what can you do", "mvp help", "how to build".
verification-gate
Hidden quality gate that runs before showing "Done!" to user - ensures all tests pass, build succeeds, and requirements met before claiming completion
brainstorming
Refine ideas into detailed designs through Socratic dialogue. Use when: user has rough idea, needs to clarify requirements, explore approaches. Triggers: "brainstorm", "discuss idea", "I'm thinking about", "what if", "help me think through", "explore options", "/brainstorm".
subagent-creator
Guide for creating effective subagents (custom agents). Use when users want to create a new subagent that can be dispatched via Task tool for autonomous work. Covers frontmatter fields (name, description, tools, model, permissionMode, skills), prompt design, and when to use subagents vs skills.
backend-rust
Modern Rust backend with Axum, SQLx, tokio + CI/CD automation. Use when: building Rust APIs, high-performance services, or needing build/test/lint/audit automation. Triggers: "axum", "rust backend", "rust api", "sqlx", "tokio", "cargo build", "cargo test", "clippy", "rustfmt", "cargo-audit", "cross-compile", "rust ci", "release build", "rust security", "shuttle", "actix".
test-driven-development
Write failing test first, then minimal code to pass. Red-Green-Refactor cycle. Use when: implementing features, fixing bugs, refactoring code. Triggers: "implement", "add feature", "fix bug", "tdd", "test first", "write tests", "test-driven".
Didn't find tool you were looking for?