Claude-skill-registry cli-tool
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cli-tool" ~/.claude/skills/majiayu000-claude-skill-registry-cli-tool && rm -rf "$T"
manifest:
skills/data/cli-tool/SKILL.mdsource content
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 |
| Slow startup | Lazy init, avoid heavy deps |
| No config file | Support |
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