AgentSkillsCN

adding-commands

引导代理为该项目添加新的CLI子命令或xtask命令。当用户请求添加新命令、子命令或xtask,或在实现新的CLI功能时使用此功能。

SKILL.md
--- frontmatter
name: adding-commands
description: Guides agents through adding new CLI subcommands or xtask commands to this project. Use when the user asks to add a new command, subcommand, or xtask, or when implementing new CLI functionality.

Adding Commands to bito-lint

Use this skill when adding new CLI subcommands or xtask development commands.

Architecture Overview

This project uses a structured command pattern:

LayerLocationPurpose
CLI parsingcrates/bito-lint/src/lib.rsCli struct with clap derive
Command dispatchcrates/bito-lint/src/main.rsMatch on Commands enum
Command implcrates/bito-lint/src/commands/*.rsActual logic
Core librarycrates/bito-lint-core/Shared logic, config, errors
Build tasksxtask/Development/maintenance commands

Adding a CLI Command

Step 1: Define the command args

Create crates/bito-lint/src/commands/<name>.rs:

rust
//! <Name> command implementation

use clap::Args;
use tracing::{debug, info, instrument};
use bito_lint_core::Config;



/// Arguments for the `<name>` subcommand.
#[derive(Args)]
pub struct <Name>Args {
    /// Example flag
    #[arg(long)]
    pub verbose: bool,

    /// Example positional argument
    pub input: Option<String>,
}

#[instrument(skip(config), fields(command = "<name>"))]
pub fn cmd_<name>(args: <Name>Args, config: &Config) -> anyhow::Result<()> {
    debug!(?config.log_level, "executing <name> command");
info!(input = ?args.input, "processing");

    // Your implementation here

    Ok(())
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cmd_<name>_succeeds() {
        let args = <Name>Args { verbose: false, input: None };
let config = Config::default();
        assert!(cmd_<name>(args, &config).is_ok());
}
}

Step 2: Export from mod.rs

Add to crates/bito-lint/src/commands/mod.rs:

rust
pub mod <name>;

Step 3: Add to Commands enum

In crates/bito-lint/src/lib.rs, add the variant:

rust
use crate::commands::<name>::<Name>Args;

pub enum Commands {
    // ... existing commands ...

    /// Description of what <name> does
    <Name>(<Name>Args),
}

Step 4: Wire up in main.rs

In crates/bito-lint/src/main.rs, add the match arm:

rust
match cli.command {
    // ... existing arms ...
Commands::<Name>(args) => commands::<name>::cmd_<name>(args, &config),
}

Step 5: Add CLI integration tests

Add to crates/bito-lint/tests/cli.rs:

rust
#[test]
fn <name>_basic_usage() {
    Command::cargo_bin(env!("CARGO_PKG_NAME"))
        .unwrap()
        .arg("<name>")
        .assert()
        .success();
}

#[test]
fn <name>_with_flags() {
    Command::cargo_bin(env!("CARGO_PKG_NAME"))
        .unwrap()
        .args(["<name>", "--verbose"])
        .assert()
        .success();
}

Adding an xtask Command

xtask commands are for development/maintenance tasks, not runtime commands. They don't use the app's config or logging.

Step 1: Create the command file

Create xtask/src/commands/<name>.rs:

rust
//! <Name> xtask command

use clap::Args;
use std::process::Command;

use crate::workspace_root;

#[derive(Args, Debug)]
pub struct <Name>Args {
    /// Example option
    #[arg(long)]
    pub dry_run: bool,
}

pub fn cmd_<name>(args: <Name>Args) -> Result<(), String> {
    let root = workspace_root();

    if args.dry_run {
        println!("Would run <name> in {:?}", root);
        return Ok(());
    }

    // Run external command example:
    let status = Command::new("cargo")
        .current_dir(&root)
        .args(["check", "--all-targets"])
        .status()
        .map_err(|e| format!("Failed to run cargo: {}", e))?;

    if !status.success() {
        return Err("Command failed".into());
    }

    Ok(())
}

Step 2: Export and wire up

In xtask/src/commands/mod.rs:

rust
pub mod <name>;

In xtask/src/main.rs, add to Task enum:

rust
/// Description of what <name> does
<Name>(<Name>Args),

And add the match arm:

rust
Task::<Name>(args) => commands::<name>::cmd_<name>(args),

Observability Patterns

Tracing Macros

Use these tracing macros for structured logging:

rust
use tracing::{trace, debug, info, warn, error, instrument, span, Level};

// Simple messages
trace!("very detailed info");
debug!("debugging info");
info!("normal operation");
warn!("something unexpected");
error!("something failed");

// Structured fields
info!(user_id = %user.id, action = "login", "user logged in");
debug!(count = items.len(), "processing items");
error!(error = ?err, path = %file_path, "failed to read file");

// Field formatting:
// %value  - Display formatting
// ?value  - Debug formatting
// value   - direct (must impl Value trait)

Instrumenting Functions

Use #[instrument] to automatically create spans:

rust
#[instrument(skip(config), fields(command = "my_cmd"))]
pub fn cmd_my_command(args: MyArgs, config: &Config) -> anyhow::Result<()> {
    // Function body is automatically wrapped in a span
    // Arguments are recorded as span fields (except `config` which we skip)

    debug!("inside the span");

    Ok(())
}

Skip large or sensitive arguments:

rust
#[instrument(skip(password, large_data), fields(user = %username))]

Config Integration

Accessing Config in Commands

Config is loaded in main.rs and passed to commands:

rust
// In main.rs - config is already loaded
let config = ConfigLoader::new()
    .with_project_search(std::env::current_dir()?)
    .load()?;

// Pass to your command
Commands::MyCmd(args) => commands::my_cmd::cmd_my_cmd(args, &config),

Using Config Values

rust
pub fn cmd_my_cmd(args: MyArgs, config: &Config) -> anyhow::Result<()> {
    // Access config fields
    let log_level = config.log_level.as_str();
// Config fields are defined in:
// crates/bito-lint-core/src/config.rs
Ok(())
}

Adding New Config Fields

  1. Add field to Config struct in config.rs
  2. Add default in impl Default for Config
  3. Update example configs in config/bito-lint.{toml,yaml}.example

Error Handling Patterns

Use anyhow for Application Errors

rust
use anyhow::{Context, Result, bail, ensure};

pub fn cmd_example(args: ExampleArgs) -> Result<()> {
    // Add context to errors
    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("Failed to read {}", args.path.display()))?;

    // Early return with error
    if content.is_empty() {
        bail!("File is empty: {}", args.path.display());
    }

    // Assert with error
    ensure!(!content.is_empty(), "File must not be empty");

    Ok(())
}

Use thiserror for Library Errors

In crates/bito-lint-core/src/error.rs:

rust
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CoreError {
    #[error("configuration error: {message}")]
    Config { message: String },

    #[error("failed to process {path}")]
    Processing {
        path: String,
        #[source]
        source: std::io::Error,
    },
}

JSON Output Pattern

Commands that produce data should support --json for scripting:

rust
use serde::Serialize;

#[derive(Serialize)]
struct MyOutput {
    field: String,
    count: usize,
}

/// Arguments for `mycommand` subcommand
#[derive(Args)]
pub struct MyArgs {
    /// Output as JSON (command-level flag)
    #[arg(long)]
    pub json: bool,
}

pub fn cmd_my(args: MyArgs, global_json: bool) -> anyhow::Result<()> {
    let output = MyOutput { field: "value".into(), count: 42 };

    // Either --json flag works (global or command-specific)
    if args.json || global_json {
        println!("{}", serde_json::to_string_pretty(&output)?);
    } else {
        println!("Field: {}", output.field);
        println!("Count: {}", output.count);
    }
    Ok(())
}

Update main.rs to pass cli.json:

rust
Commands::My(args) => commands::my::cmd_my(args, cli.json),

Checklist for New Commands

  • Command file created in src/commands/
  • Exported in src/commands/mod.rs
  • Args struct added to lib.rs Commands enum
  • Match arm added in main.rs
  • Config passed if needed
  • #[instrument] added for tracing
  • Appropriate log levels used (debug for dev, info for user-visible)
  • Unit tests in command file
  • Integration tests in tests/cli.rs
  • Help text is clear and useful
  • JSON output supported if command produces data (accept global_json param)