diff --git a/README.md b/README.md index ef6bc6e..7a6069d 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Since we're effectively giving out GPUs for free we rely on either github or dis 1. Register the CLI (Discord recommended): `popcorn register discord` (or `popcorn register github`) 2. To ensure the above worked you can run `cat $HOME/.popcorn.yaml` which should print your client ID which is what will be sent to us on every request +3. To override the submitter ID for local proxy/testing flows, set `POPCORN_SUBMITTER_ID`. Commands that require `cli_id` use this value first, then fall back to `~/.popcorn.yaml`. Sometimes you'll get an error that you're already authenticated despite being unable to submit in which case you can run `popcorn reregister [discord|github]`. diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index e1b1033..004b2a8 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -40,6 +40,27 @@ fn load_config() -> Result { serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e)) } +fn submit_cli_id_from_env() -> Option { + std::env::var("POPCORN_SUBMITTER_ID") + .ok() + .filter(|v| !v.trim().is_empty()) +} + +fn resolve_cli_id() -> Result { + if let Some(cli_id) = submit_cli_id_from_env() { + return Ok(cli_id); + } + + let config = load_config()?; + config.cli_id.ok_or_else(|| { + anyhow!( + "cli_id not found in config file ({}). Please run 'popcorn-cli register' first.", + get_config_path() + .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) + ) + }) +} + #[derive(Parser, Debug)] #[command(author, version = env!("CLI_VERSION"), about, long_about = None)] /// Popcorn CLI for GPU Mode competitions. Run `popcorn setup` first in each project so agents use the correct workflow and templates. @@ -183,14 +204,7 @@ pub async fn execute(cli: Cli) -> Result<()> { output, no_tui, }) => { - let config = load_config()?; - let cli_id = config.cli_id.ok_or_else(|| { - anyhow!( - "cli_id not found in config file ({}). Please run 'popcorn-cli register' first.", - get_config_path() - .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) - ) - })?; + let cli_id = resolve_cli_id()?; // Use filepath from Submit command first, fallback to top-level filepath let final_filepath = filepath.or(cli.filepath); @@ -218,14 +232,7 @@ pub async fn execute(cli: Cli) -> Result<()> { } } Some(Commands::Join { code }) => { - let config = load_config()?; - let cli_id = config.cli_id.ok_or_else(|| { - anyhow!( - "cli_id not found in config file ({}). Please run `popcorn register` first.", - get_config_path() - .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) - ) - })?; + let cli_id = resolve_cli_id()?; let client = service::create_client(Some(cli_id))?; let result = service::join_with_invite(&client, &code).await?; let leaderboards = result["leaderboards"] @@ -242,14 +249,7 @@ pub async fn execute(cli: Cli) -> Result<()> { } Some(Commands::Admin { action }) => admin::handle_admin(action).await, Some(Commands::Submissions { action }) => { - let config = load_config()?; - let cli_id = config.cli_id.ok_or_else(|| { - anyhow!( - "cli_id not found in config file ({}). Please run `popcorn register` first.", - get_config_path() - .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) - ) - })?; + let cli_id = resolve_cli_id()?; match action { SubmissionsAction::List { leaderboard, limit } => { @@ -272,14 +272,7 @@ pub async fn execute(cli: Cli) -> Result<()> { // Handle the case where only a filepath is provided (for backward compatibility) if let Some(top_level_filepath) = cli.filepath { - let config = load_config()?; - let cli_id = config.cli_id.ok_or_else(|| { - anyhow!( - "cli_id not found in config file ({}). Please run `popcorn register` first.", - get_config_path() - .map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string()) - ) - })?; + let cli_id = resolve_cli_id()?; // Run TUI with only filepath, no other options submit::run_submit_tui( @@ -299,3 +292,57 @@ pub async fn execute(cli: Cli) -> Result<()> { } } } + +#[cfg(test)] +mod tests { + use super::resolve_cli_id; + use std::env; + use std::fs; + use std::sync::Mutex; + use tempfile::tempdir; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + struct EnvGuard { + old_home: Option, + old_submitter: Option, + } + + impl EnvGuard { + fn new() -> Self { + Self { + old_home: env::var("HOME").ok(), + old_submitter: env::var("POPCORN_SUBMITTER_ID").ok(), + } + } + } + + impl Drop for EnvGuard { + fn drop(&mut self) { + match &self.old_home { + Some(v) => env::set_var("HOME", v), + None => env::remove_var("HOME"), + } + match &self.old_submitter { + Some(v) => env::set_var("POPCORN_SUBMITTER_ID", v), + None => env::remove_var("POPCORN_SUBMITTER_ID"), + } + } + } + + #[test] + fn test_resolve_cli_id_prefers_env_over_config() { + let _lock = ENV_LOCK.lock().expect("Failed to lock env mutex"); + let _guard = EnvGuard::new(); + + let temp_home = tempdir().expect("Failed to create temp home dir"); + let config_path = temp_home.path().join(".popcorn.yaml"); + fs::write(config_path, "cli_id: config-cli-id\n").expect("Failed to write config"); + + env::set_var("HOME", temp_home.path()); + env::set_var("POPCORN_SUBMITTER_ID", "env-cli-id"); + + let cli_id = resolve_cli_id().expect("Expected cli_id resolution to succeed"); + assert_eq!(cli_id, "env-cli-id"); + } +}