use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell}; use colored::*; use anyhow::Result; use anvil::version::ANVIL_VERSION; mod commands { pub use anvil::commands::*; } #[derive(Parser)] #[command(name = "anvil")] #[command(author = "Eric Ratliff ")] #[command(version = ANVIL_VERSION)] #[command( about = "Arduino project generator and build tool - forges clean embedded projects", long_about = None )] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Create a new Arduino project New { /// Project name name: Option, /// Template to use (basic) #[arg(long, short = 't', value_name = "TEMPLATE")] template: Option, /// Board preset (uno, mega, nano, leonardo, micro) #[arg(long, short = 'b', value_name = "BOARD")] board: Option, /// List available templates #[arg(long, conflicts_with = "name")] list_templates: bool, /// List available board presets #[arg(long, conflicts_with = "name")] list_boards: bool, }, /// Check system health and diagnose issues Doctor { /// Automatically install missing tools #[arg(long)] fix: bool, }, /// Install arduino-cli and required cores Setup, /// List connected boards and serial ports Devices { /// Save a port to .anvil.local for this project #[arg(long, conflicts_with_all = ["get", "clear"])] set: bool, /// Show the saved port for this project #[arg(long, conflicts_with_all = ["set", "clear"])] get: bool, /// Remove .anvil.local (revert to auto-detect) #[arg(long, conflicts_with_all = ["set", "get"])] clear: bool, /// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set. port_or_dir: Option, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, /// Update project scripts to the latest version Refresh { /// Path to project directory (defaults to current directory) dir: Option, /// Overwrite managed files even if they have been modified #[arg(long)] force: bool, /// Override .anvilignore for a specific file (use with --force) #[arg(long, value_name = "PATH")] file: Option, /// Add a pattern to .anvilignore (protect a file from refresh) #[arg(long, value_name = "PATTERN", conflicts_with_all = ["unignore", "force"])] ignore: Option, /// Remove a pattern from .anvilignore (allow refresh to update it) #[arg(long, value_name = "PATTERN", conflicts_with_all = ["ignore", "force"])] unignore: Option, }, /// Manage board profiles in .anvil.toml Board { /// Board name (e.g. mega, nano) name: Option, /// Add a board to the project #[arg(long, conflicts_with_all = ["remove", "listall", "default"])] add: bool, /// Remove a board from the project #[arg(long, conflicts_with_all = ["add", "listall", "default"])] remove: bool, /// Set the default board #[arg(long, conflicts_with_all = ["add", "remove", "listall"])] default: bool, /// Browse all available boards #[arg(long, conflicts_with_all = ["add", "remove", "default"])] listall: bool, /// Board identifier (from anvil board --listall) #[arg(long, value_name = "ID")] id: Option, /// Baud rate override #[arg(long, value_name = "RATE")] baud: Option, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, /// Add a device library to the project Add { /// Library name (e.g. tmp36) name: String, /// Assign a pin during add (e.g. --pin A0) #[arg(long, value_name = "PIN")] pin: Option, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, /// Remove a device library from the project Remove { /// Library name (e.g. tmp36) name: String, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, /// List installed or available device libraries Lib { /// Show all available libraries (not just installed) #[arg(long)] available: bool, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, /// Generate shell completion scripts Completions { /// Shell to generate completions for (bash, zsh, fish, powershell) shell: Shell, }, /// View pin maps, assign pins, and audit wiring Pin { /// Capability filter (pwm, analog, spi, i2c, uart, interrupt) /// or pin/bus name for --assign / --remove name: Option, /// Pin number or alias for --assign (e.g. 13, A0, SDA) pin: Option, /// Assign a pin or bus group #[arg(long, conflicts_with_all = ["remove", "audit", "generate", "capabilities", "init_from"])] assign: bool, /// Remove a pin or bus assignment #[arg(long, conflicts_with_all = ["assign", "audit", "generate", "capabilities", "init_from"])] remove: bool, /// Show wiring audit report #[arg(long, conflicts_with_all = ["assign", "remove", "generate", "capabilities", "init_from"])] audit: bool, /// Show only the wiring checklist (with --audit) #[arg(long, requires = "audit")] brief: bool, /// Generate lib/hal/pins.h #[arg(long, conflicts_with_all = ["assign", "remove", "audit", "capabilities", "init_from"])] generate: bool, /// List capabilities supported by the board #[arg(long, conflicts_with_all = ["assign", "remove", "audit", "generate", "init_from"])] capabilities: bool, /// Copy pin assignments from another board #[arg(long, value_name = "BOARD", conflicts_with_all = ["assign", "remove", "audit", "generate", "capabilities"])] init_from: Option, /// Pin mode (input, output, input_pullup, pwm, analog) #[arg(long, value_name = "MODE")] mode: Option, /// SPI chip-select pin (with --assign spi) #[arg(long, value_name = "PIN")] cs: Option, /// Target board (defaults to project default) #[arg(long, short = 'b', value_name = "BOARD")] board: Option, /// Path to project directory (defaults to current directory) #[arg(long, short = 'd', value_name = "DIR")] dir: Option, }, } fn main() -> Result<()> { #[cfg(windows)] colored::control::set_virtual_terminal(true).ok(); let cli = Cli::parse(); // Completions output must be clean -- no banner let is_completions = matches!(cli.command, Commands::Completions { .. }); if !is_completions { print_banner(); } match cli.command { Commands::New { name, template, board, list_templates, list_boards } => { if list_boards { commands::new::list_boards() } else if list_templates { commands::new::list_templates() } else if let Some(project_name) = name { commands::new::create_project( &project_name, template.as_deref(), board.as_deref(), ) } else { anyhow::bail!( "Project name required.\n\ Usage: anvil new \n\ Usage: anvil new --board mega\n\ List boards: anvil new --list-boards\n\ List templates: anvil new --list-templates" ); } } Commands::Doctor { fix } => { commands::doctor::run_diagnostics(fix) } Commands::Setup => { commands::setup::run_setup() } Commands::Devices { set, get, clear, port_or_dir, dir } => { if set { commands::devices::set_port( port_or_dir.as_deref(), dir.as_deref(), ) } else if get { commands::devices::get_port( dir.as_deref().or(port_or_dir.as_deref()), ) } else if clear { commands::devices::clear_port( dir.as_deref().or(port_or_dir.as_deref()), ) } else { commands::devices::scan_devices() } } Commands::Refresh { dir, force, file, ignore, unignore } => { if let Some(pattern) = ignore { commands::refresh::add_ignore( dir.as_deref(), &pattern, ) } else if let Some(pattern) = unignore { commands::refresh::remove_ignore( dir.as_deref(), &pattern, ) } else { commands::refresh::run_refresh( dir.as_deref(), force, file.as_deref(), ) } } Commands::Board { name, add, remove, default, listall, id, baud, dir } => { if listall { commands::board::listall_boards(name.as_deref()) } else if add { let board_name = name.as_deref().ok_or_else(|| { anyhow::anyhow!( "Board name required.\n\ Usage: anvil board --add mega\n\ Browse available boards: anvil board --listall" ) })?; commands::board::add_board( board_name, id.as_deref(), baud, dir.as_deref(), ) } else if remove { let board_name = name.as_deref().ok_or_else(|| { anyhow::anyhow!( "Board name required.\n\ Usage: anvil board --remove mega" ) })?; commands::board::remove_board( board_name, dir.as_deref(), ) } else if default { let board_name = name.as_deref().ok_or_else(|| { anyhow::anyhow!( "Board name required.\n\ Usage: anvil board --default uno\n\ List boards: anvil board" ) })?; commands::board::set_default_board( board_name, dir.as_deref(), ) } else { commands::board::list_boards(dir.as_deref()) } } Commands::Add { name, pin, dir } => { commands::lib::add_library(&name, pin.as_deref(), dir.as_deref()) } Commands::Remove { name, dir } => { commands::lib::remove_library(&name, dir.as_deref()) } Commands::Lib { available, dir } => { if available { commands::lib::list_available() } else { commands::lib::list_libraries(dir.as_deref()) } } Commands::Completions { shell } => { let mut cmd = Cli::command(); generate(shell, &mut cmd, "anvil", &mut std::io::stdout()); Ok(()) } Commands::Pin { name, pin, assign, remove, audit, brief, generate, capabilities, init_from, mode, cs, board, dir, } => { if capabilities { commands::pin::show_capabilities( board.as_deref(), dir.as_deref(), ) } else if audit { commands::pin::audit_pins( board.as_deref(), brief, dir.as_deref(), ) } else if generate { commands::pin::generate_pins_header( board.as_deref(), dir.as_deref(), ) } else if remove { let pin_name = name.as_deref().ok_or_else(|| { anyhow::anyhow!( "Name required.\n\ Usage: anvil pin --remove red_led\n\ Audit current: anvil pin --audit" ) })?; commands::pin::remove_assignment( pin_name, board.as_deref(), dir.as_deref(), ) } else if let Some(ref source) = init_from { let target = board.as_deref().ok_or_else(|| { anyhow::anyhow!( "Target board required.\n\ Usage: anvil pin --init-from uno --board mega" ) })?; commands::pin::init_from( source, target, dir.as_deref(), ) } else if assign { let assign_name = name.as_deref().ok_or_else(|| { anyhow::anyhow!( "Name required.\n\ Usage: anvil pin --assign red_led 13\n\ Usage: anvil pin --assign spi --cs 10\n\ See pins: anvil pin" ) })?; // Check if this is a bus group assignment let is_bus = { let board_name = board.as_deref().unwrap_or("uno"); anvil::board::pinmap::find_pinmap_fuzzy(board_name) .map(|pm| pm.groups.iter().any(|g| { g.name == assign_name.to_lowercase() })) .unwrap_or(false) }; if is_bus { let mut user_pins: Vec<(&str, &str)> = Vec::new(); if let Some(ref cs_pin) = cs { user_pins.push(("cs", cs_pin.as_str())); } commands::pin::assign_bus( assign_name, &user_pins, board.as_deref(), dir.as_deref(), ) } else { let pin_str = pin.as_deref().ok_or_else(|| { anyhow::anyhow!( "Pin number required.\n\ Usage: anvil pin --assign {} 13\n\ See pins: anvil pin", assign_name ) })?; commands::pin::assign_pin( assign_name, pin_str, mode.as_deref(), board.as_deref(), dir.as_deref(), ) } } else { // Default: show pin map, optionally filtered commands::pin::show_pin_map( board.as_deref(), name.as_deref(), dir.as_deref(), ) } } } } fn print_banner() { println!( "{}", "================================================================" .bright_cyan() ); println!( "{}", format!(" Anvil - Arduino Build Tool v{}", ANVIL_VERSION) .bright_cyan() .bold() ); println!("{}", " Nexus Workshops LLC".bright_cyan()); println!( "{}", "================================================================" .bright_cyan() ); println!(); }