diff --git a/src/config.rs b/src/config.rs index 2ab6953..c240a48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ // GNU Affero General Public License for more details. use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -63,6 +64,8 @@ pub struct Config { pub quiet: Option, /// Update configuration section pub update: Option, + /// Custom commands overrides + pub commands: Option>, } impl Config { @@ -129,6 +132,15 @@ impl Config { (Some(base), None) => Some(base), (None, None) => None, }, + commands: match (self.commands, other.commands) { + (Some(mut base), Some(over)) => { + base.extend(over); + Some(base) + } + (None, Some(over)) => Some(over), + (Some(base), None) => Some(base), + (None, None) => None, + }, } } @@ -200,6 +212,7 @@ mod tests { verbose: None, quiet: None, update: None, + commands: None, }; let override_config = Config { @@ -209,6 +222,7 @@ mod tests { verbose: Some(true), quiet: None, update: None, + commands: None, }; let merged = base.merge(override_config); @@ -218,6 +232,34 @@ mod tests { assert!(merged.get_verbose()); } + #[test] + fn test_merge_commands() { + let mut base_cmds = HashMap::new(); + base_cmds.insert("base".to_string(), "echo base".to_string()); + base_cmds.insert("both".to_string(), "echo base_both".to_string()); + + let base = Config { + commands: Some(base_cmds), + ..Default::default() + }; + + let mut override_cmds = HashMap::new(); + override_cmds.insert("over".to_string(), "echo over".to_string()); + override_cmds.insert("both".to_string(), "echo over_both".to_string()); + + let override_config = Config { + commands: Some(override_cmds), + ..Default::default() + }; + + let merged = base.merge(override_config); + let cmds = merged.commands.unwrap(); + + assert_eq!(cmds.get("base").unwrap(), "echo base"); + assert_eq!(cmds.get("over").unwrap(), "echo over"); + assert_eq!(cmds.get("both").unwrap(), "echo over_both"); + } + #[test] fn test_load_from_file() { let dir = tempdir().unwrap(); diff --git a/src/detectors/mod.rs b/src/detectors/mod.rs index 2d4184f..c252dd5 100644 --- a/src/detectors/mod.rs +++ b/src/detectors/mod.rs @@ -325,6 +325,12 @@ impl DetectedRunner { /// Check if this runner supports the given command. pub fn supports_command(&self, command: &str, working_dir: &Path) -> CommandSupport { + // First check if this is a custom command + if let Some(commands) = &self.custom_commands { + if commands.contains_key(command) { + return CommandSupport::Supported; + } + } self.validator.supports_command(working_dir, command) } diff --git a/src/main.rs b/src/main.rs index 3983c98..ee23013 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use clap::{CommandFactory, Parser}; use clap_complete::generate; use run_cli::cli::{Cli, Commands}; use run_cli::config::Config; +use run_cli::detectors::{DetectedRunner, Ecosystem, UnknownValidator}; use run_cli::error::exit_codes; use run_cli::output; use run_cli::runner::{check_conflicts, execute, search_runners, select_runner}; @@ -20,6 +21,7 @@ use run_cli::update; use std::env; use std::io; use std::process; +use std::sync::Arc; fn main() { // Check for internal update flag (used by background updater) @@ -94,20 +96,80 @@ fn main() { }; // Search for runners - let (runners, working_dir) = match search_runners( + let search_result = search_runners( ¤t_dir, max_levels, &ignore_list, verbose, - ) { + ); + + // Prepare to inject custom commands + // Filter empty commands + let valid_config_commands: Option> = + config.commands.as_ref().map(|cmds| { + cmds.iter() + .filter(|(_, cmd)| !cmd.trim().is_empty()) + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + }); + + let has_valid_commands = valid_config_commands + .as_ref() + .map_or(false, |c| !c.is_empty()); + + let (mut runners, working_dir) = match search_result { Ok(result) => result, Err(e) => { - output::error(&e.to_string()); - eprintln!("Hint: Use --levels=N to increase search depth or check if you're in the right directory."); - process::exit(e.exit_code()); + if has_valid_commands { + // If we have custom commands, we can proceed even without detected runners + (Vec::new(), current_dir.clone()) + } else { + output::error(&e.to_string()); + eprintln!("Hint: Use --levels=N to increase search depth or check if you're in the right directory."); + process::exit(e.exit_code()); + } } }; + // Inject custom commands from config + if let Some(valid_config_commands) = valid_config_commands { + if !valid_config_commands.is_empty() { + // Check if we already have a custom runner + if let Some(idx) = runners.iter().position(|r| r.ecosystem == Ecosystem::Custom) { + // Merge config commands into existing runner (local overrides global) + let mut merged_commands = valid_config_commands.clone(); + if let Some(existing_cmds) = &runners[idx].custom_commands { + merged_commands.extend(existing_cmds.clone()); + } + + // Update the runner + let old_runner = &runners[idx]; + let new_runner = DetectedRunner::with_custom_commands( + &old_runner.name, + &old_runner.detected_file, + old_runner.ecosystem, + old_runner.priority, + Arc::new(UnknownValidator), + merged_commands, + ); + runners[idx] = new_runner; + } else { + // Create new runner + let new_runner = DetectedRunner::with_custom_commands( + "custom", + "config.toml", + Ecosystem::Custom, + 0, + Arc::new(UnknownValidator), + valid_config_commands, + ); + runners.push(new_runner); + // Sort by priority (0 first) + runners.sort_by_key(|r| r.priority); + } + } + } + // Check for conflicts and select runner based on command support let runner = match check_conflicts(&runners, &working_dir, verbose) { Ok(_) => match select_runner(&runners, &command, &working_dir, verbose) {