项目文件夹

文件
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

1 行
16 KiB
JSON

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
{"content": "---\nname: rust-cli-builder\ndescription: Plan and build production-ready Rust CLI tools using clap for argument parsing, with subcommands, config file support, colored output, and proper error handling. Uses interview-driven planning to clarify commands, input/output formats, and distribution strategy before writing any code.\ntags: [rust, cli, clap, terminal, command-line, devtools]\n---\n\n# Rust CLI Tool Builder\n\n## When to use\n\nUse this skill when you need to:\n\n- Scaffold a new Rust CLI tool from scratch with clap\n- Add subcommands to an existing CLI application\n- Implement config file loading (TOML/JSON/YAML)\n- Set up proper error handling with anyhow/thiserror\n- Add colored and formatted terminal output\n- Structure a CLI project for distribution via cargo install or GitHub releases\n\n## Phase 1: Explore (Plan Mode)\n\nEnter plan mode. Before writing any code, explore the existing project:\n\n### If extending an existing project\n- Find `Cargo.toml` and check current dependencies (clap version, serde, tokio, etc.)\n- Locate the CLI entry point (`src/main.rs` or `src/cli.rs`)\n- Check if clap is using derive macros or builder pattern\n- Identify existing subcommand structure\n- Look for existing error types, config structs, and output formatting\n- Check if there's a `src/lib.rs` separating library logic from CLI\n\n### If starting from scratch\n- Check the workspace for any existing Rust projects or workspace `Cargo.toml`\n- Look for a `.cargo/config.toml` with custom settings\n- Check for `rust-toolchain.toml` to know the target Rust edition\n\n## Phase 2: Interview (AskUserQuestion)\n\nUse AskUserQuestion to clarify requirements. Ask in rounds.\n\n### Round 1: Tool purpose and commands\n\n```\nQuestion: \"What kind of CLI tool are you building?\"\nHeader: \"Tool type\"\nOptions:\n - \"Single command (like ripgrep, curl)\" — One main action with flags and arguments\n - \"Multi-command (like git, cargo)\" — Multiple subcommands under one binary\n - \"Interactive REPL (like psql)\" — Persistent session with a prompt loop\n - \"Pipeline tool (like jq, sed)\" — Reads stdin, transforms, writes stdout\n\nQuestion: \"What will the tool operate on?\"\nHeader: \"Input\"\nOptions:\n - \"Files/directories\" — Read, process, or generate files\n - \"Network/API\" — HTTP requests, TCP connections, API calls\n - \"System resources\" — Processes, hardware info, OS config\n - \"Data streams (stdin/stdout)\" — Pipe-friendly text/binary processing\n```\n\n### Round 2: Subcommands (if multi-command)\n\n```\nQuestion: \"Describe the subcommands you need (e.g., 'init', 'build', 'deploy')\"\nHeader: \"Commands\"\nOptions:\n - \"2-3 subcommands (I'll describe them)\" — Small focused tool\n - \"4-8 subcommands with groups\" — Medium tool, may need command groups\n - \"I have a rough list, help me design the API\" — Collaborative command design\n```\n\n### Round 3: Configuration and output\n\n```\nQuestion: \"How should the tool be configured?\"\nHeader: \"Config\"\nOptions:\n - \"CLI flags only (Recommended)\" — All config via command-line arguments\n - \"Config file (TOML)\" — Load defaults from ~/.config/toolname/config.toml\n - \"Config file + CLI overrides\" — Config file for defaults, flags override specific values\n - \"Environment variables + flags\" — Env vars for secrets, flags for everything else\n\nQuestion: \"What output format does the tool need?\"\nHeader: \"Output\"\nOptions:\n - \"Human-readable (colored text)\" — Pretty terminal output with colors and formatting\n - \"Machine-readable (JSON)\" — Structured output for piping to other tools\n - \"Both (--format flag)\" — Default human, --json or --format=json for machines\n - \"Minimal (exit codes only)\" — Success/failure via exit code, errors to stderr\n```\n\n### Round 4: Async and error handling\n\n```\nQuestion: \"Does the tool need async operations?\"\nHeader: \"Async\"\nOptions:\n - \"No — synchronous is fine (Recommended)\" — File I/O, computation, simple operations\n - \"Yes — tokio (network I/O)\" — HTTP requests, concurrent connections, async file I/O\n - \"Yes — tokio multi-threaded\" — Heavy parallelism, multiple concurrent tasks\n\nQuestion: \"How should errors be presented to users?\"\nHeader: \"Errors\"\nOptions:\n - \"Simple messages (anyhow) (Recommended)\" — Human-readable error chains, good for most CLIs\n - \"Typed errors (thiserror)\" — Custom error enum with specific variants for each failure\n - \"Both (thiserror for lib, anyhow for bin)\" — Library code is typed, CLI wraps with anyhow\n```\n\n## Phase 3: Plan (ExitPlanMode)\n\nWrite a concrete implementation plan covering:\n\n1. **Project structure** — `Cargo.toml` dependencies, `src/` file layout\n2. **CLI definition** — clap derive structs for all commands, args, and flags\n3. **Config loading** — config file format and merge strategy with CLI args\n4. **Core logic** — main functions for each subcommand, separated from CLI layer\n5. **Error types** — error enum or anyhow usage, user-facing error messages\n6. **Output formatting** — colored output, JSON mode, progress indicators\n7. **Tests** — unit tests for core logic, integration tests for CLI behavior\n\nPresent via ExitPlanMode for user approval.\n\n## Phase 4: Execute\n\nAfter approval, implement following this order:\n\n### Step 1: Project setup (Cargo.toml)\n\n```toml\n[package]\nname = \"toolname\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"Short description of the tool\"\n\n[dependencies]\nclap = { version = \"4\", features = [\"derive\", \"env\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nanyhow = \"1\"\n# Add based on interview:\n# thiserror = \"2\" # if typed errors\n# tokio = { version = \"1\", features = [\"full\"] } # if async\n# serde_json = \"1\" # if JSON output\n# toml = \"0.8\" # if TOML config\n# colored = \"2\" # if colored output\n# indicatif = \"0.17\" # if progress bars\n# dirs = \"5\" # if config file (~/.config/)\n```\n\n### Step 2: CLI definition with clap derive\n\n```rust\nuse clap::{Parser, Subcommand};\n\n/// Short one-line description of the tool\n#[derive(Parser, Debug)]\n#[command(name = \"toolname\", version, about, long_about = None)]\npub struct Cli {\n /// Increase verbosity (-v, -vv, -vvv)\n #[arg(short, long, action = clap::ArgAction::Count, global = true)]\n pub verbose: u8,\n\n /// Output format\n #[arg(long, default_value = \"text\", global = true)]\n pub format: OutputFormat,\n\n /// Path to config file\n #[arg(long, global = true)]\n pub config: Option<std::path::PathBuf>,\n\n #[command(subcommand)]\n pub command: Commands,\n}\n\n#[derive(Subcommand, Debug)]\npub enum Commands {\n /// Initialize a new project\n Init {\n /// Project name\n name: String,\n\n /// Template to use\n #[arg(short, long, default_value = \"default\")]\n template: String,\n },\n\n /// Build the project\n Build {\n /// Build in release mode\n #[arg(short, long)]\n release: bool,\n\n /// Target directory\n #[arg(short, long)]\n output: Option<std::path::PathBuf>,\n },\n\n /// Show project status\n Status,\n}\n\n#[derive(clap::ValueEnum, Clone, Debug)]\npub enum OutputFormat {\n Text,\n Json,\n}\n```\n\n### Step 3: Error handling\n\n```rust\n// With anyhow (simple approach):\nuse anyhow::{Context, Result};\n\nfn load_config(path: &Path) -> Result<Config> {\n let content = std::fs::read_to_string(path)\n .with_context(|| format!(\"Failed to read config file: {}\", path.display()))?;\n let config: Config = toml::from_str(&content)\n .context(\"Invalid TOML in config file\")?;\n Ok(config)\n}\n\n// With thiserror (typed approach):\nuse thiserror::Error;\n\n#[derive(Error, Debug)]\npub enum AppError {\n #[error(\"Config file not found: {path}\")]\n ConfigNotFound { path: std::path::PathBuf },\n\n #[error(\"Invalid config: {0}\")]\n InvalidConfig(#[from] toml::de::Error),\n\n #[error(\"Network error: {0}\")]\n Network(#[from] reqwest::Error),\n\n #[error(\"{0}\")]\n Custom(String),\n}\n```\n\n### Step 4: Config file loading\n\n```rust\nuse serde::Deserialize;\nuse std::path::{Path, PathBuf};\n\n#[derive(Deserialize, Debug, Default)]\npub struct Config {\n pub default_template: Option<String>,\n pub output_dir: Option<PathBuf>,\n // ... fields from interview\n}\n\nimpl Config {\n pub fn load(explicit_path: Option<&Path>) -> anyhow::Result<Self> {\n let path = match explicit_path {\n Some(p) => p.to_path_buf(),\n None => Self::default_path(),\n };\n\n if !path.exists() {\n return Ok(Config::default());\n }\n\n let content = std::fs::read_to_string(&path)?;\n let config: Config = toml::from_str(&content)?;\n Ok(config)\n }\n\n fn default_path() -> PathBuf {\n dirs::config_dir()\n .unwrap_or_else(|| PathBuf::from(\".\"))\n .join(\"toolname\")\n .join(\"config.toml\")\n }\n}\n```\n\n### Step 5: Colored output and formatting\n\n```rust\nuse colored::Colorize;\n\npub struct Output {\n format: OutputFormat,\n verbose: u8,\n}\n\nimpl Output {\n pub fn new(format: OutputFormat, verbose: u8) -> Self {\n Self { format, verbose }\n }\n\n pub fn success(&self, msg: &str) {\n match self.format {\n OutputFormat::Text => eprintln!(\"{} {}\", \"✓\".green().bold(), msg),\n OutputFormat::Json => {} // JSON output goes to stdout only\n }\n }\n\n pub fn error(&self, msg: &str) {\n match self.format {\n OutputFormat::Text => eprintln!(\"{} {}\", \"✗\".red().bold(), msg),\n OutputFormat::Json => {\n let err = serde_json::json!({\"error\": msg});\n println!(\"{}\", serde_json::to_string(&err).unwrap());\n }\n }\n }\n\n pub fn info(&self, msg: &str) {\n if self.verbose >= 1 {\n match self.format {\n OutputFormat::Text => eprintln!(\"{} {}\", \"\".blue(), msg),\n OutputFormat::Json => {}\n }\n }\n }\n\n pub fn data<T: serde::Serialize>(&self, data: &T) {\n match self.format {\n OutputFormat::Text => {\n // Pretty print for humans — customize per subcommand\n println!(\"{:#?}\", data);\n }\n OutputFormat::Json => {\n println!(\"{}\", serde_json::to_string_pretty(data).unwrap());\n }\n }\n }\n}\n```\n\n### Step 6: Main entry point\n\n```rust\nuse clap::Parser;\n\nfn main() -> anyhow::Result<()> {\n let cli = Cli::parse();\n let config = Config::load(cli.config.as_deref())?;\n let output = Output::new(cli.format.clone(), cli.verbose);\n\n match cli.command {\n Commands::Init { name, template } => {\n cmd_init(&name, &template, &config, &output)?;\n }\n Commands::Build { release, output_dir } => {\n let dir = output_dir\n .or(config.output_dir.clone())\n .unwrap_or_else(|| PathBuf::from(\"./dist\"));\n cmd_build(release, &dir, &output)?;\n }\n Commands::Status => {\n cmd_status(&config, &output)?;\n }\n }\n\n Ok(())\n}\n\n// If async (tokio):\n// #[tokio::main]\n// async fn main() -> anyhow::Result<()> { ... }\n```\n\n### Step 7: Subcommand implementations\n\n```rust\nfn cmd_init(name: &str, template: &str, config: &Config, out: &Output) -> anyhow::Result<()> {\n let template = if template == \"default\" {\n config.default_template.as_deref().unwrap_or(\"default\")\n } else {\n template\n };\n\n out.info(&format!(\"Using template: {}\", template));\n\n let project_dir = Path::new(name);\n if project_dir.exists() {\n anyhow::bail!(\"Directory '{}' already exists\", name);\n }\n\n std::fs::create_dir_all(project_dir)?;\n // ... scaffold project files based on template\n\n out.success(&format!(\"Created project '{}' with template '{}'\", name, template));\n Ok(())\n}\n```\n\n### Step 8: Tests\n\n```rust\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_config_default() {\n let config = Config::default();\n assert!(config.default_template.is_none());\n }\n\n #[test]\n fn test_config_parse_toml() {\n let toml_str = r#\"\n default_template = \"react\"\n output_dir = \"./build\"\n \"#;\n let config: Config = toml::from_str(toml_str).unwrap();\n assert_eq!(config.default_template.unwrap(), \"react\");\n }\n}\n\n// Integration tests (tests/cli.rs):\nuse assert_cmd::Command;\nuse predicates::prelude::*;\n\n#[test]\nfn test_help_flag() {\n Command::cargo_bin(\"toolname\")\n .unwrap()\n .arg(\"--help\")\n .assert()\n .success()\n .stdout(predicate::str::contains(\"Usage:\"));\n}\n\n#[test]\nfn test_version_flag() {\n Command::cargo_bin(\"toolname\")\n .unwrap()\n .arg(\"--version\")\n .assert()\n .success();\n}\n\n#[test]\nfn test_init_creates_directory() {\n let dir = tempfile::tempdir().unwrap();\n let project_name = dir.path().join(\"test-project\");\n\n Command::cargo_bin(\"toolname\")\n .unwrap()\n .args([\"init\", project_name.to_str().unwrap()])\n .assert()\n .success();\n\n assert!(project_name.exists());\n}\n\n#[test]\nfn test_init_existing_directory_fails() {\n let dir = tempfile::tempdir().unwrap();\n\n Command::cargo_bin(\"toolname\")\n .unwrap()\n .args([\"init\", dir.path().to_str().unwrap()])\n .assert()\n .failure()\n .stderr(predicate::str::contains(\"already exists\"));\n}\n\n#[test]\nfn test_json_output_format() {\n Command::cargo_bin(\"toolname\")\n .unwrap()\n .args([\"--format\", \"json\", \"status\"])\n .assert()\n .success()\n .stdout(predicate::str::starts_with(\"{\"));\n}\n```\n\n## Project structure reference\n\n```\ntoolname/\n├── Cargo.toml\n├── src/\n│ ├── main.rs # Entry point, CLI parsing, command dispatch\n│ ├── cli.rs # Clap derive structs (Cli, Commands, Args)\n│ ├── config.rs # Config file loading and merging\n│ ├── output.rs # Output formatting (text/JSON/colored)\n│ ├── error.rs # Error types (if using thiserror)\n│ └── commands/\n│ ├── mod.rs\n│ ├── init.rs # Init subcommand logic\n│ ├── build.rs # Build subcommand logic\n│ └── status.rs # Status subcommand logic\n└── tests/\n └── cli.rs # Integration tests with assert_cmd\n```\n\n## Best practices\n\n### Separate CLI from logic\nKeep clap structs and argument parsing in `cli.rs`. Put business logic in `commands/`. This makes the core logic testable without invoking the CLI.\n\n### Use stderr for status, stdout for data\nHuman-readable messages (progress, success, errors) go to `stderr`. Machine-readable data goes to `stdout`. This lets users pipe output cleanly: `toolname status --format json | jq '.items'`.\n\n### Respect NO_COLOR\nCheck the `NO_COLOR` environment variable and disable colors when set:\n```rust\nif std::env::var(\"NO_COLOR\").is_ok() {\n colored::control::set_override(false);\n}\n```\n\n### Exit codes\nUse meaningful exit codes: 0 for success, 1 for general errors, 2 for usage errors (clap handles this automatically).\n\n### Dev dependencies for testing\n\n```toml\n[dev-dependencies]\nassert_cmd = \"2\"\npredicates = \"3\"\ntempfile = \"3\"\n```\n\n## Checklist before finishing\n\n- [ ] `clap` derive structs have doc comments (they become --help text)\n- [ ] All subcommands have short and long descriptions\n- [ ] Config file has sensible defaults and doesn't error when missing\n- [ ] `--format json` outputs valid, parseable JSON to stdout\n- [ ] Errors show context (file paths, what went wrong, how to fix it)\n- [ ] Integration tests verify CLI behavior end-to-end\n- [ ] `cargo clippy` passes with no warnings\n- [ ] `cargo fmt` has been run\n"}