diff --git a/README.md b/README.md index c2d638e..3884234 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,15 @@ You can also type a repository keyword on the home screen (for example `ratatui` | `--no-folder` | Downloads files directly without creating a subfolder for the repo. | | `--token ` | Use a specific GitHub token for this run (doesn't save to settings). | +### Environment Variables + +`ghgrab` also accepts GitHub tokens from environment variables: + +- `GHGRAB_GITHUB_TOKEN` +- `GITHUB_TOKEN` + + + ### Agent Mode For scripts, agents, and other non-interactive workflows, `ghgrab` includes a machine-friendly `agent` command that prints a stable JSON envelope with `api_version`, `ok`, `command`, and either `data` or `error`. diff --git a/src/main.rs b/src/main.rs index 11aed23..bcb88ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,9 @@ use ghgrab::config::Config; use ghgrab::ui; +const GHGRAB_GITHUB_TOKEN_ENV: &str = "GHGRAB_GITHUB_TOKEN"; +const GITHUB_TOKEN_ENV: &str = "GITHUB_TOKEN"; + #[derive(Parser)] #[command(name = "ghgrab", version, about)] struct Cli { @@ -151,7 +154,7 @@ async fn main() -> Result<()> { }, Some(Commands::Agent { action }) => match action { AgentCommand::Tree { url, token } => { - let token = token.or(default_config.github_token.clone()); + let token = resolve_github_token(token, default_config.github_token.clone()); let result = agent::fetch_tree(&url, token).await; print_agent_json("tree", result)?; } @@ -165,7 +168,7 @@ async fn main() -> Result<()> { out, token, } => { - let token = token.or(default_config.github_token.clone()); + let token = resolve_github_token(token, default_config.github_token.clone()); let out = out.or(default_config.download_path.clone()); let selected_paths = build_download_request(paths, repo, subtree); let result = match selected_paths { @@ -183,7 +186,7 @@ async fn main() -> Result<()> { let download_path = default_config.download_path.clone(); - let token = cli.token.or(default_config.github_token.clone()); + let token = resolve_github_token(cli.token, default_config.github_token.clone()); let initial_icon_mode = default_config.icon_mode.unwrap_or(ui::IconMode::Emoji); let final_icon_mode = ui::run_tui( @@ -206,6 +209,30 @@ async fn main() -> Result<()> { Ok(()) } +fn resolve_github_token(cli_token: Option, config_token: Option) -> Option { + normalize_token(cli_token) + .or_else(resolve_github_token_from_env) + .or_else(|| normalize_token(config_token)) +} + +fn resolve_github_token_from_env() -> Option { + [GHGRAB_GITHUB_TOKEN_ENV, GITHUB_TOKEN_ENV] + .into_iter() + .find_map(|key| std::env::var(key).ok()) + .and_then(|token| normalize_token(Some(token))) +} + +fn normalize_token(token: Option) -> Option { + token.and_then(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + }) +} + fn build_download_request( paths: Vec, repo: bool,