diff --git a/src/cli/init_cli.rs b/src/cli/init_cli.rs index bedb35b..454654d 100644 --- a/src/cli/init_cli.rs +++ b/src/cli/init_cli.rs @@ -60,7 +60,13 @@ use cull_gmail::{ClientConfig, Error, GmailClient, Result}; use lazy_regex::{Lazy, Regex, lazy_regex}; /// Parse configuration root path with h:, c:, r: prefixes. -fn parse_config_root(path_str: &str) -> PathBuf { +/// +/// Supports: +/// - `h:path` - Relative to home directory +/// - `c:path` - Relative to current directory +/// - `r:path` - Relative to filesystem root +/// - `path` - Use path as-is +pub fn parse_config_root(path_str: &str) -> PathBuf { static ROOT_CONFIG: Lazy = lazy_regex!(r"^(?P[hrc]):(?P.+)$"); if let Some(captures) = ROOT_CONFIG.captures(path_str) { diff --git a/src/cli/init_cli/tests.rs b/src/cli/init_cli/tests.rs index 96c4b9f..5eb6528 100644 --- a/src/cli/init_cli/tests.rs +++ b/src/cli/init_cli/tests.rs @@ -72,6 +72,7 @@ mod unit_tests { create_mock_credential_file(temp_dir.path()).unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -88,6 +89,7 @@ mod unit_tests { fn test_validate_credential_file_not_found() { let temp_dir = TempDir::new().unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -108,6 +110,7 @@ mod unit_tests { fs::write(&credential_path, "invalid json content").unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -131,6 +134,7 @@ mod unit_tests { let config_path = temp_dir.path().join("new-config"); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -183,6 +187,7 @@ mod unit_tests { fs::rename(temp_dir.path().join("credential.json"), &cred_path).unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -220,6 +225,7 @@ mod unit_tests { fs::write(config_path.join("cull-gmail.toml"), "existing config").unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -243,6 +249,7 @@ mod unit_tests { fs::write(config_path.join("rules.toml"), "existing rules").unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: true, @@ -275,6 +282,7 @@ mod unit_tests { fs::write(&test_file, "test content").unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, @@ -317,6 +325,7 @@ mod unit_tests { fs::write(&test_file, "test content").unwrap(); let init_cli = InitCli { + rules_dir: None, config_dir: "test".to_string(), credential_file: None, force: false, diff --git a/src/cli/main.rs b/src/cli/main.rs index d833fe4..29d1c52 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -127,6 +127,8 @@ use messages_cli::MessagesCli; use rules_cli::RulesCli; use token_cli::{TokenCli, restore_tokens_from_string}; +use std::path::PathBuf; + /// Main CLI application structure defining global options and subcommands. /// /// This struct represents the root of the command-line interface, providing @@ -304,8 +306,11 @@ async fn run(args: Cli) -> Result<()> { let mut client = GmailClient::new_with_config(client_config).await?; + // Get configured rules path + let rules_path = get_rules_path(&config)?; + let Some(sub_command) = args.sub_command else { - let rules = rules_cli::get_rules()?; + let rules = rules_cli::get_rules_from(rules_path.as_deref())?; let execute = config.get_bool("execute").unwrap_or(false); return run_rules(&mut client, rules, execute).await; }; @@ -317,7 +322,11 @@ async fn run(args: Cli) -> Result<()> { } SubCmds::Message(messages_cli) => messages_cli.run(&mut client).await, SubCmds::Labels(labels_cli) => labels_cli.run(client).await, - SubCmds::Rules(rules_cli) => rules_cli.run(&mut client).await, + SubCmds::Rules(rules_cli) => { + rules_cli + .run_with_rules_path(&mut client, rules_path.as_deref()) + .await + } SubCmds::Token(token_cli) => { // Token commands don't need an initialized client, just the config // We need to get a fresh client_config since the original was moved @@ -530,6 +539,33 @@ async fn run_rules(client: &mut GmailClient, rules: Rules, execute: bool) -> Res /// - Container deployments with injected token environment variables /// - CI/CD pipelines with stored token secrets /// - Ephemeral compute environments requiring periodic Gmail access +/// Gets the rules file path from configuration. +/// +/// Reads the `rules` configuration value and resolves it using path prefixes. +/// Supports h:, c:, r: prefixes for home, current, and root directories. +/// +/// # Arguments +/// +/// * `config` - Application configuration +/// +/// # Returns +/// +/// Returns the resolved rules file path, or None if using default location. +fn get_rules_path(config: &Config) -> Result> { + let rules_config = config + .get_string("rules") + .unwrap_or_else(|_| "rules.toml".to_string()); + + // If it's just "rules.toml" (the default), return None to use default location + if rules_config == "rules.toml" { + return Ok(None); + } + + // Otherwise, parse the path with prefix support + let path = init_cli::parse_config_root(&rules_config); + Ok(Some(path)) +} + fn restore_tokens_if_available(config: &Config, client_config: &ClientConfig) -> Result<()> { let token_env_var = config .get_string("token_cache_env") diff --git a/src/cli/rules_cli.rs b/src/cli/rules_cli.rs index 7876ec5..0e06d4b 100644 --- a/src/cli/rules_cli.rs +++ b/src/cli/rules_cli.rs @@ -263,7 +263,21 @@ impl RulesCli { /// - **Error isolation**: Subcommand errors don't affect rule loading /// - **State preservation**: Configuration errors don't corrupt existing rules pub async fn run(&self, client: &mut GmailClient) -> Result<()> { - let rules = get_rules()?; + self.run_with_rules_path(client, None).await + } + + /// Executes the rules command with an optional custom rules path. + /// + /// # Arguments + /// + /// * `client` - Mutable Gmail client for API operations + /// * `rules_path` - Optional path to rules file + pub async fn run_with_rules_path( + &self, + client: &mut GmailClient, + rules_path: Option<&Path>, + ) -> Result<()> { + let rules = get_rules_from(rules_path)?; match &self.sub_command { SubCmds::Config(config_cli) => config_cli.run(rules),