From 93921a3ae31b60e060983ed631370153409c8fbd Mon Sep 17 00:00:00 2001 From: "Zeko (Hermes)" Date: Fri, 5 Jun 2026 23:42:57 +0000 Subject: [PATCH] feat: tiered auth login, configurable CI, updated docs - Upgrade cora auth login to interactive tiered provider selection - Known providers: just API key needed - Custom provider: collect base_url, model, and API key - Store provider info alongside API key in auth.toml - Update cora auth status to display stored provider details - Fix CI action to read .cora.yaml from repo instead of hardcoded config - Update README Quick Start and Authentication sections - Update website getting-started docs with auth flow examples --- .github/actions/cora-review/action.yml | 26 ++- README.md | 91 ++++++++-- src/commands/auth.rs | 156 +++++++++++++++++- src/config/loader.rs | 131 +++++++++++++++ .../routes/docs/getting-started/+page.svelte | 118 ++++++++++--- 5 files changed, 465 insertions(+), 57 deletions(-) diff --git a/.github/actions/cora-review/action.yml b/.github/actions/cora-review/action.yml index 30ac653a..ad016bd8 100644 --- a/.github/actions/cora-review/action.yml +++ b/.github/actions/cora-review/action.yml @@ -109,14 +109,24 @@ runs: INPUT_BASE: ${{ inputs.base-branch }} INPUT_SEVERITY: ${{ inputs.severity }} run: | - # Set higher diff limit for CI (default 50K is too small for large PRs) - printf 'hook:\n max_diff_size: 200000\n' > .cora-ci.yaml - CORA_CONFIG=".cora-ci.yaml" cora review \ - --base "$INPUT_BASE" \ - --format sarif \ - --severity "$INPUT_SEVERITY" \ - --quiet \ - > cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$? + # Use project .cora.yaml if it exists; otherwise create a CI-friendly fallback + if [ ! -f .cora.yaml ]; then + printf 'hook:\n max_diff_size: 5242880\n' > .cora-ci.yaml + CORA_CONFIG=".cora-ci.yaml" cora review \ + --base "$INPUT_BASE" \ + --format sarif \ + --severity "$INPUT_SEVERITY" \ + --quiet \ + > cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$? + else + echo "Using project .cora.yaml for configuration" + cora review \ + --base "$INPUT_BASE" \ + --format sarif \ + --severity "$INPUT_SEVERITY" \ + --quiet \ + > cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$? + fi SARIF_BYTES=$(wc -c < cora-results.sarif) echo "Cora review complete ($SARIF_BYTES bytes, exit=$EXIT_CODE)" if [ -s cora-stderr.log ]; then diff --git a/README.md b/README.md index da1b49a5..8eace351 100644 --- a/README.md +++ b/README.md @@ -87,38 +87,43 @@ cargo install --path . ## 🚀 Quick Start -### 1. Set Your API Key +### 1. Install ```bash -export OPENAI_API_KEY="sk-..." -# or -export ANTHROPIC_API_KEY="sk-ant-..." +curl -fsSL https://raw.githubusercontent.com/codecoradev/cora-cli/main/install.sh | sh ``` -### 2. Initialize Config (Optional) +### 2. Set Up Authentication ```bash -cora init +cora auth login ``` -### 3. Review Staged Changes +Pick your provider, enter your API key — done. Cora stores it securely in `~/.cora/auth.toml` (never committed to git). -```bash -cora review --staged -``` +> **No API key yet?** Set any provider env var instead: `export OPENAI_API_KEY="..."` -### 4. Review the Last Commit +### 3. Review Your Code ```bash +# Review staged changes +cora review + +# Review vs a branch +cora review --base origin/main + +# Review the last commit cora review --commit HEAD ``` -### 5. Scan the Entire Project +### 4. Optional — Project Config ```bash -cora scan +cora init ``` +Creates `.cora.yaml` in your project root for custom settings (focus areas, ignore patterns, hook config). The CI action automatically reads this file. + ## 📖 Commands ### `cora review` @@ -324,16 +329,68 @@ output: ### Authentication -API keys can be provided via environment variable (`CORA_API_KEY`), provider-specific env vars (`OPENAI_API_KEY`, etc.), or stored in `~/.cora/auth.toml` (auto-created by `cora auth login`, permission `0600`). +API keys can be provided via environment variables, stored in `~/.cora/auth.toml` (auto-created by `cora auth login`, permission `0600`), or passed via CLI flags. + +#### Interactive Login (Recommended) ```bash -# Interactive login (stores key in ~/.cora/auth.toml) cora auth login +``` + +This starts an interactive setup that: + +1. **Lists known providers** — OpenAI, Anthropic, Groq, Ollama, Z.AI +2. **Lets you pick one** — known providers only need an API key +3. **Or choose "custom"** — enter your own base URL, model, and API key for any OpenAI-compatible endpoint -# Or set via environment variable -export CORA_API_KEY=sk-... +Example flow: + +``` +$ cora auth login + +🔑 Cora Auth Setup + Choose your LLM provider: + + [1] openai — https://api.openai.com/v1 (model: gpt-4o-mini) + [2] anthropic — https://api.anthropic.com/v1 (model: claude-3-haiku-20240307) + [3] groq — https://api.groq.com/openai/v1 (model: llama-3.1-8b-instant) + [4] ollama — http://localhost:11434/v1 (model: llama3.1) + [5] zai — https://api.z.ai/api/coding/paas/v4 (model: glm-5.1) + [6] custom — use any OpenAI-compatible endpoint + + Select provider [1-6]: 1 + + → Provider: openai + → Model: gpt-4o-mini + → Base URL: https://api.openai.com/v1 + + 🔑 Enter your API key: sk-... + +✅ API key saved to ~/.cora/auth.toml + Provider: openai | Model: gpt-4o-mini | Base: https://api.openai.com/v1 ``` +#### Check Auth Status + +```bash +cora auth status +``` + +Shows your configured provider, model, and key source. + +#### Environment Variables + +| Variable | Description | +|----------|-------------| +| `CORA_API_KEY` | API key (overrides all other sources) | +| `OPENAI_API_KEY` | OpenAI API key (auto-detected) | +| `ANTHROPIC_API_KEY` | Anthropic API key (auto-detected) | +| `GROQ_API_KEY` | Groq API key (auto-detected) | +| `ZAI_API_KEY` | Z.AI API key (auto-detected) | +| `CORA_PROVIDER` | Override provider name | +| `CORA_MODEL` | Override model | +| `CORA_BASE_URL` | Override API base URL | + ## 🔗 CI/CD Integration ### GitHub Actions diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 36548224..5d5259be 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -4,8 +4,9 @@ use anyhow::Result; use colored::Colorize; use crate::config::loader; +use crate::config::providers::PRESETS; -/// Execute `auth login` — prompt for API key and save it. +/// Execute `auth login` — interactive tiered provider selection and API key setup. pub fn execute_auth_login() -> Result<()> { // Check if already logged in let status = loader::auth_status()?; @@ -26,7 +27,97 @@ pub fn execute_auth_login() -> Result<()> { } } - print!("🔑 Enter your API key: "); + println!(); + println!("{}", "🔑 Cora Auth Setup".bold()); + println!("{}", " Choose your LLM provider:".dimmed()); + println!(); + + // List known providers + for (i, preset) in PRESETS.iter().enumerate() { + println!( + " {} {} — {} (model: {})", + format!("[{}]", i + 1).cyan().bold(), + preset.name.bold(), + preset.default_base_url.dimmed(), + preset.default_model.dimmed(), + ); + } + // Custom option + println!( + " {} {} — use any OpenAI-compatible endpoint", + format!("[{}]", PRESETS.len() + 1).cyan().bold(), + "custom".bold(), + ); + println!(); + + print!( + " {} ", + format!("Select provider [1-{}]:", PRESETS.len() + 1).bold() + ); + io::stdout().flush()?; + + let mut choice = String::new(); + io::stdin().read_line(&mut choice)?; + let choice_num: usize = choice + .trim() + .parse() + .map_err(|_| anyhow::anyhow!("Invalid choice — enter a number"))?; + + if choice_num == 0 || choice_num > PRESETS.len() + 1 { + anyhow::bail!( + "Invalid choice — pick a number between 1 and {}", + PRESETS.len() + 1 + ); + } + + let (provider, base_url, model) = if choice_num <= PRESETS.len() { + // Known provider — just need API key + let preset = &PRESETS[choice_num - 1]; + println!(); + println!( + " {} {} ({})", + "→".green(), + "Provider:".bold(), + preset.name.green() + ); + println!( + " {} {} ({})", + "→".green(), + "Model:".bold(), + preset.default_model.green() + ); + println!( + " {} {} ({})", + "→".green(), + "Base URL:".bold(), + preset.default_base_url.dimmed() + ); + println!(); + ( + preset.name.to_string(), + preset.default_base_url.to_string(), + preset.default_model.to_string(), + ) + } else { + // Custom provider — collect all details + println!(); + println!(" {} {}", "→".green(), "Custom provider setup".bold()); + println!(); + + let provider = prompt_input(" Provider name (e.g. my-llm):")?; + let base_url = prompt_input(" Base URL (e.g. https://api.example.com/v1):")?; + let model = prompt_input(" Default model (e.g. gpt-4o):")?; + + if provider.is_empty() || base_url.is_empty() || model.is_empty() { + anyhow::bail!("Provider name, base URL, and model are required for custom providers"); + } + + (provider, base_url, model) + }; + + // Collect API key + println!(); + print!(" 🔑 Enter your API key: "); io::stdout().flush()?; let mut key = String::new(); @@ -37,35 +128,80 @@ pub fn execute_auth_login() -> Result<()> { anyhow::bail!("API key cannot be empty"); } + // Save API key + provider info loader::save_api_key(&key)?; - println!("{} API key saved to ~/.cora/auth.toml", "✅".green().bold()); + loader::save_provider_info(&provider, &base_url, &model)?; + + println!(); + println!( + "{} API key saved to {}", + "✅".green().bold(), + "~/.cora/auth.toml".green() + ); + println!( + "{} Provider: {} | Model: {} | Base: {}", + " ".dimmed(), + provider.bold(), + model.bold(), + base_url.dimmed() + ); println!( "{}", " This file is local to your machine and not committed to git.".dimmed() ); + println!(); + println!( + "{} Run {} to verify, or {} to start reviewing.", + "Next:".bold(), + "cora auth status".cyan(), + "cora review".cyan() + ); Ok(()) } -/// Execute `auth status` — show whether an API key is configured. +/// Prompt for a non-empty string with label. +fn prompt_input(label: &str) -> Result { + print!("{} ", label); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) +} + +/// Execute `auth status` — show whether an API key is configured and which provider. pub fn execute_auth_status() -> Result<()> { let status = loader::auth_status()?; if status.has_key { println!("{} API key is configured.", "✅".green().bold()); println!(" Source: {}", status.source); + + // Show stored provider info if available + if let Some(info) = loader::load_provider_info()? { + println!(); + println!(" {} {}", "Provider:".bold(), info.provider.green()); + println!(" {} {}", "Model:".bold(), info.model.green()); + println!(" {} {}", "Base URL:".bold(), info.base_url.dimmed()); + } else { + println!( + " {} No provider info stored — run {} to set up", + "â„šī¸".yellow(), + "cora auth login".cyan() + ); + } } else { println!("{} No API key configured.", "❌".red().bold()); println!(" Set it via:"); + println!(" â€ĸ {} (interactive setup)", "cora auth login".cyan()); println!(" â€ĸ CORA_API_KEY environment variable"); - println!(" â€ĸ `cora auth login` command"); - println!(" â€ĸ `cora review --api-key ` flag"); + println!(" â€ĸ Provider-specific env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc."); } Ok(()) } -/// Execute `auth remove` — delete the stored API key. +/// Execute `auth remove` — delete the stored API key and provider info. pub fn execute_auth_remove() -> Result<()> { let status = loader::auth_status()?; if !status.has_key && std::env::var("CORA_API_KEY").is_err() { @@ -74,7 +210,11 @@ pub fn execute_auth_remove() -> Result<()> { } loader::remove_api_key()?; - println!("{} API key removed from local config.", "✅".green().bold()); + loader::remove_provider_info()?; + println!( + "{} API key and provider info removed from local config.", + "✅".green().bold() + ); println!( "{}", " If you set CORA_API_KEY in your shell, remove it there too.".dimmed() diff --git a/src/config/loader.rs b/src/config/loader.rs index ee5a2042..66b4e72d 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -478,3 +478,134 @@ pub struct AuthStatus { pub source: String, pub has_key: bool, } + +/// Stored provider information alongside the API key. +#[derive(Debug, Clone)] +pub struct ProviderInfo { + pub provider: String, + pub base_url: String, + pub model: String, +} + +/// Save provider info (name, base_url, model) to `~/.cora/auth.toml` +/// alongside the existing `api_key`. +pub fn save_provider_info( + provider: &str, + base_url: &str, + model: &str, +) -> std::result::Result<(), CoraError> { + let dir = cora_dir()?; + let path = dir.join(AUTH_FILENAME); + + // Read existing auth.toml or start fresh + let mut table = if path.is_file() { + let content = std::fs::read_to_string(&path) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + content + .parse::() + .unwrap_or_else(|_| toml::Table::new()) + } else { + toml::Table::new() + }; + + // Set provider info fields + table.insert( + "provider".to_string(), + toml::Value::String(provider.to_string()), + ); + table.insert( + "base_url".to_string(), + toml::Value::String(base_url.to_string()), + ); + table.insert("model".to_string(), toml::Value::String(model.to_string())); + + let content = table.to_string(); + std::fs::write(&path, content) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + + // Restrict permissions to owner only (0o600) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o600); + std::fs::set_permissions(&path, perms)?; + } + + debug!(provider = provider, "saved provider info"); + Ok(()) +} + +/// Load stored provider info from `~/.cora/auth.toml`. +/// Returns `None` if no provider info is stored. +pub fn load_provider_info() -> std::result::Result, CoraError> { + let dir = cora_dir()?; + let path = dir.join(AUTH_FILENAME); + if !path.is_file() { + return Ok(None); + } + + let content = std::fs::read_to_string(&path) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + + let table: toml::Table = content + .parse::() + .map_err(|e| CoraError::AuthError(format!("invalid TOML: {}", e)))?; + + let provider = table + .get("provider") + .and_then(toml::Value::as_str) + .unwrap_or(""); + let base_url = table + .get("base_url") + .and_then(toml::Value::as_str) + .unwrap_or(""); + let model = table + .get("model") + .and_then(toml::Value::as_str) + .unwrap_or(""); + + if provider.is_empty() { + return Ok(None); + } + + Ok(Some(ProviderInfo { + provider: provider.to_string(), + base_url: base_url.to_string(), + model: model.to_string(), + })) +} + +/// Remove stored provider info from `~/.cora/auth.toml` while keeping the api_key if present. +pub fn remove_provider_info() -> std::result::Result<(), CoraError> { + let dir = cora_dir()?; + let path = dir.join(AUTH_FILENAME); + if !path.is_file() { + return Ok(()); + } + + let content = std::fs::read_to_string(&path) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + + let mut table: toml::Table = content + .parse::() + .map_err(|e| CoraError::AuthError(format!("invalid TOML: {}", e)))?; + + let changed = table.remove("provider").is_some() + | table.remove("base_url").is_some() + | table.remove("model").is_some(); + + if changed { + // If only provider info was left (no api_key), just delete the file + if table.is_empty() { + std::fs::remove_file(&path) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + } else { + let content = table.to_string(); + std::fs::write(&path, content) + .map_err(|e| CoraError::AuthError(format!("{}: {}", path.display(), e)))?; + } + debug!("removed provider info from auth.toml"); + } + + Ok(()) +} diff --git a/website/src/routes/docs/getting-started/+page.svelte b/website/src/routes/docs/getting-started/+page.svelte index 775aaf7c..eff53de4 100644 --- a/website/src/routes/docs/getting-started/+page.svelte +++ b/website/src/routes/docs/getting-started/+page.svelte @@ -28,38 +28,107 @@

Quick Start

-

Get up and running with cora in four simple steps.

+

Get up and running with cora in three simple steps.

1
- Install cora — Single binary via Cargo: - cargo install cora-cli + Install cora — Single binary, no runtime dependencies: + curl -fsSL https://raw.githubusercontent.com/codecoradev/cora-cli/main/install.sh | sh
2
- Configure — Initialize your project: - cora init creates .cora.yaml + Authenticate — Run cora auth login to pick your provider and enter your API key. + Cora stores it securely in ~/.cora/auth.toml (never committed to git).
3
- Add API key — Run cora auth login or set CORA_API_KEY environment variable + Review — Analyze your staged changes: + cora review
+
-
-
4
-
- Review — Analyze your staged changes: - cora review +
+

Authentication — cora auth login

+

+ The interactive login guides you through provider selection: +

+ +
+
+ + + +
+
+
$ cora auth login
+
+
🔑 Cora Auth Setup
+
Choose your LLM provider:
+
+
[1] openai — https://api.openai.com/v1
+
[2] anthropic — https://api.anthropic.com/v1
+
[3] groq — https://api.groq.com/openai/v1
+
[4] ollama — http://localhost:11434/v1
+
[5] zai — https://api.z.ai/api/coding/paas/v4
+
[6] custom — any OpenAI-compatible endpoint
+
+
Select provider [1-6]: 1
+
+
→ Provider: openai
+
→ Model: gpt-4o-mini
+
→ Base URL: https://api.openai.com/v1
+
+
🔑 Enter your API key: ****
+
+
✅ API key saved to ~/.cora/auth.toml
+
+
+ +
+
+ Known providers + Just enter your API key — model and base URL are pre-configured +
+
+ Custom provider + Enter your own base URL, model name, and API key for any OpenAI-compatible API +
+
+ Provider info stored + Provider name, model, and base URL are saved alongside your API key for easy reference
+ +

Check your auth status anytime:

+
+
+ + + +
+
+
$ cora auth status
+
✅ API key is configured.
+
Source: ~/.cora/auth.toml
+
+
Provider: openai
+
Model: gpt-4o-mini
+
Base URL: https://api.openai.com/v1
+
+
+ +

+ You can also use environment variables instead of cora auth login: + CORA_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. +

@@ -109,8 +178,11 @@
-

Customizing Your Review

-

The .cora.yaml file controls how cora reviews your code.

+

Project Configuration — cora init

+

+ Create a .cora.yaml config file in your project root to customize review settings. + The CI action automatically reads this file when it exists. +

@@ -119,26 +191,24 @@
-
# .cora.yaml
-
review:
-
severity: warning
-
focus: security,performance
-
-
providers:
-
openai:
-
model: gpt-4o
+
$ cora init
+
✅ Created .cora.yaml with example configuration.
+

Key configuration options:

- review.severity — Minimum severity level (info, minor, major, critical) + focus — Review focus areas: security, performance, bugs, best_practice +
+
+ ignore — File patterns and rules to skip
- review.focus — Focus areas for review (e.g., security, performance) + hook — Pre-commit hook settings: mode, severity threshold, max diff size
- providers — Provider-specific model overrides (openai, anthropic, groq, ollama, zai) + llm — LLM parameters: temperature, max_tokens, timeout