bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Typed command grammar for the BotForge CLI.
//!
//! Execution remains in the command-specific application modules, while this tree is the
//! authoritative syntax and constraint boundary shared by ordinary execution and help requests.

use clap::{Command as ClapCommand, CommandFactory, Parser, Subcommand, error::ErrorKind};

#[derive(Debug, Parser)]
#[command(
    name = "bot-forge",
    version,
    about = "Configurable Rust tool installer",
    override_usage = "bot-forge [OPTIONS] <COMMAND>",
    no_binary_name = true,
    disable_help_flag = true,
    disable_help_subcommand = true,
    disable_version_flag = true
)]
pub(crate) struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Execute an installation plan.
    Install(InstallArgs),
    /// Create or explain an installation plan.
    Plan(PlanArgs),
    /// Show managed installation status.
    Status(StatusArgs),
    /// Resume or abandon an unfinished transaction.
    Resume(ResumeArgs),
    /// Remove a managed installation.
    Remove(RemoveArgs),
    /// Create, validate, and inspect configuration.
    Config(ConfigArgs),
    /// Inspect or reclaim the cache.
    Cache(CacheArgs),
    /// Run system diagnostics.
    Doctor(DoctorArgs),
    /// Manage APT mirrors.
    AptMirror(AptMirrorArgs),
    /// Generate protocol artifacts or documentation.
    Generate(GenerateArgs),
    /// Print this message or the help of the given subcommand(s).
    Help(HelpArgs),
}

#[derive(Debug, clap::Args)]
/// Shared configuration and selection for planning commands.
struct CommonPlanArgs {
    /// Installation profile; defaults to standard when omitted.
    #[arg(value_name = "PROFILE")]
    profile: Option<String>,
    /// Configuration file; defaults to bot-forge.toml.
    #[arg(long, value_name = "FILE", help_heading = "Configuration")]
    config: Option<String>,
    /// Apply a configuration overlay file; repeat as needed.
    #[arg(long, value_name = "FILE", help_heading = "Configuration")]
    overlay: Vec<String>,
    /// Include only a component; repeat as needed.
    #[arg(long, value_name = "NAME", help_heading = "Selection")]
    only: Vec<String>,
    /// Exclude a component; repeat as needed.
    #[arg(long, value_name = "NAME", help_heading = "Selection")]
    exclude: Vec<String>,
}

#[derive(Debug, clap::Args)]
struct InstallArgs {
    #[command(flatten)]
    common: CommonPlanArgs,
    /// Skip confirmation prompts.
    #[arg(short = 'y', long, help_heading = "Execution")]
    yes: bool,
    /// Output format: human, json, or jsonl.
    #[arg(long, value_enum, conflicts_with = "quiet", help_heading = "Output")]
    format: Option<InstallFormat>,
    /// Suppress human output.
    #[arg(short = 'q', long, conflicts_with = "format", help_heading = "Output")]
    quiet: bool,
}

#[derive(Debug, clap::Args)]
struct PlanArgs {
    #[command(flatten)]
    common: CommonPlanArgs,
    /// Output format: human or json.
    #[arg(long, value_enum, help_heading = "Output")]
    format: Option<OutputFormat>,
    /// Explain plan decisions.
    #[arg(long, help_heading = "Output")]
    why: bool,
}

#[derive(Debug, clap::Args)]
struct StatusArgs {
    profile: Option<String>,
    #[arg(long)]
    config: Option<String>,
    #[arg(long)]
    overlay: Vec<String>,
    /// Output format: human or json.
    #[arg(long, value_enum, help_heading = "Output")]
    format: Option<OutputFormat>,
}

#[derive(Debug, clap::Args)]
struct ResumeArgs {
    profile: Option<String>,
    #[arg(long, conflicts_with = "abandon")]
    run: Option<String>,
    #[arg(
        long,
        conflicts_with_all = ["run", "profile", "config", "overlay", "only", "exclude"]
    )]
    abandon: Option<String>,
    #[arg(long)]
    config: Option<String>,
    #[arg(long)]
    overlay: Vec<String>,
    #[arg(long)]
    only: Vec<String>,
    #[arg(long)]
    exclude: Vec<String>,
}

#[derive(Debug, clap::Args)]
struct RemoveArgs {
    name: String,
    #[arg(long, value_enum)]
    kind: Option<RemoveKind>,
    #[arg(long, conflicts_with = "yes")]
    dry_run: bool,
    #[arg(short = 'y', long, conflicts_with = "dry_run")]
    yes: bool,
}

#[derive(Debug, clap::Args)]
struct ConfigArgs {
    #[command(subcommand)]
    command: ConfigCommand,
}

#[derive(Debug, Subcommand)]
enum ConfigCommand {
    /// Create bot-forge.toml.
    Init {
        #[arg(long)]
        output: Option<String>,
        #[arg(short = 'f', long)]
        force: bool,
    },
    /// Validate configuration.
    Validate(ConfigFiles),
    /// Print effective configuration.
    Effective {
        #[command(flatten)]
        files: ConfigFiles,
        #[arg(short = 'v', long)]
        verbose: bool,
        #[arg(long, requires = "verbose")]
        show_sensitive: bool,
    },
    /// Explain configuration sources.
    Explain(ConfigFiles),
}

#[derive(Debug, clap::Args)]
struct ConfigFiles {
    #[arg(long)]
    config: Option<String>,
    #[arg(long)]
    overlay: Vec<String>,
}

#[derive(Debug, clap::Args)]
struct CacheArgs {
    #[command(subcommand)]
    command: CacheCommand,
}

#[derive(Debug, Subcommand)]
enum CacheCommand {
    /// Show cache status.
    Status {
        #[arg(long, value_enum)]
        format: Option<OutputFormat>,
    },
    /// Reclaim expired cache entries.
    Gc {
        #[arg(long, value_enum)]
        format: Option<OutputFormat>,
        #[arg(long)]
        max_age_days: Option<u64>,
        #[arg(long)]
        dry_run: bool,
    },
}

#[derive(Debug, clap::Args)]
struct DoctorArgs {
    #[arg(long)]
    config: Option<String>,
    #[arg(long, value_enum)]
    format: Option<OutputFormat>,
}

#[derive(Debug, clap::Args)]
struct AptMirrorArgs {
    #[command(subcommand)]
    command: AptMirrorCommand,
}

#[derive(Debug, Subcommand)]
enum AptMirrorCommand {
    /// Preview the configured mirror.
    Show(ConfigFiles),
    /// Validate the configured mirror.
    Check(ConfigFiles),
    /// Apply the configured mirror.
    Apply(AptMirrorWriteArgs),
    /// Restore the previous mirror.
    Restore(AptMirrorWriteArgs),
}

#[derive(Debug, clap::Args)]
struct AptMirrorWriteArgs {
    #[command(flatten)]
    files: ConfigFiles,
    /// Skip the confirmation prompt.
    #[arg(short = 'y', long)]
    yes: bool,
}

#[derive(Debug, clap::Args)]
struct GenerateArgs {
    #[arg(value_enum)]
    format: GenerateFormat,
}

#[derive(Debug, clap::Args)]
struct HelpArgs {
    command: Option<String>,
    subcommand: Option<String>,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum OutputFormat {
    Human,
    Json,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum InstallFormat {
    Human,
    Json,
    Jsonl,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum RemoveKind {
    Tool,
    Skill,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum GenerateFormat {
    /// Generate shell completion scripts.
    Completion,
    /// Generate a roff manual page.
    Man,
    /// Generate the configuration schema.
    Schema,
    /// Generate command metadata as JSON.
    Json,
    /// Generate command metadata as JSON Lines.
    Jsonl,
}

/// Validate syntax and constraints without taking ownership of execution or presentation.
pub(crate) fn validate(arguments: &[String]) -> Result<(), String> {
    if arguments.len() == 1 && matches!(arguments[0].as_str(), "-V" | "--version") {
        return Ok(());
    }
    let help_requested = arguments
        .iter()
        .any(|argument| matches!(argument.as_str(), "-h" | "--help"));
    if arguments.first().is_some_and(|argument| argument == "help")
        || arguments.get(1).is_some_and(|argument| argument == "help")
    {
        return Ok(());
    }
    let filtered = arguments
        .iter()
        .filter(|argument| !matches!(argument.as_str(), "-h" | "--help"))
        .cloned()
        .collect::<Vec<_>>();
    if filtered.is_empty() {
        return Ok(());
    }
    match Cli::try_parse_from(filtered) {
        Ok(_) => Ok(()),
        Err(error)
            if help_requested
                && matches!(
                    error.kind(),
                    ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
                        | ErrorKind::MissingRequiredArgument
                        | ErrorKind::MissingSubcommand
                ) =>
        {
            Ok(())
        }
        Err(error) if help_requested && error.kind() == ErrorKind::InvalidSubcommand => {
            Err("invalid help command path".to_string())
        }
        Err(error) => {
            let text = error.to_string();
            if text.contains("cannot be used multiple times") {
                let option = text
                    .split_once("the argument '")
                    .and_then(|(_, rest)| rest.split_once('\''))
                    .map_or("argument", |(option, _)| option);
                Err(format!("argument {option} cannot be used multiple times"))
            } else if text.contains("unexpected argument '--force'") {
                Err("unknown remove option: --force".to_string())
            } else {
                Err(format!("parse error: {text}"))
            }
        }
    }
}

/// Build the typed command tree used by Help and syntax validation.
pub(crate) fn command() -> ClapCommand {
    Cli::command()
        .name("bot-forge")
        .subcommand_required(false)
        .arg_required_else_help(false)
}