Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.

Expand Down
111 changes: 79 additions & 32 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,27 @@ fn load_config() -> Result<Config> {
serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e))
}

fn submit_cli_id_from_env() -> Option<String> {
std::env::var("POPCORN_SUBMITTER_ID")
.ok()
.filter(|v| !v.trim().is_empty())
}

fn resolve_cli_id() -> Result<String> {
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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"]
Expand All @@ -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 } => {
Expand All @@ -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(
Expand All @@ -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<String>,
old_submitter: Option<String>,
}

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");
}
}
Loading