use anyhow::{Context, Result};
use std::io::{self, BufRead, Write};
use std::process::Command;
pub fn run() -> Result<()> {
println!();
println!(" aid setup");
println!(" ─────────────────────────────────");
println!(" Press Enter to skip any step.");
println!();
let config_path = crate::paths::config_path();
let mut existing = if config_path.exists() {
std::fs::read_to_string(&config_path)?
} else {
String::new()
};
let config = crate::config::load_config().unwrap_or_default();
let qc = &config.query;
let current_env_key = std::env::var("OPENROUTER_API_KEY").ok();
let has_key = qc.api_key.as_deref().filter(|k| !k.is_empty()).is_some()
|| current_env_key.is_some();
section("OpenRouter API Key");
println!(" Free model: {}", qc.free_model);
println!(" Auto model: {}", qc.auto_model);
if let Some(k) = qc.api_key.as_deref().filter(|k| !k.is_empty()) {
println!(" API key: {} (config.toml)", mask_key(k));
print!("\n Update key? Enter new key or press Enter to keep: ");
io::stdout().flush()?;
let key = read_line()?;
if !key.is_empty() {
eprint!(" Verifying... ");
io::stderr().flush()?;
if test_openrouter_key(&key) {
aid_info!("OK");
println!(" ✓ Key updated ({})", mask_key(&key));
replace_api_key(&mut existing, &key);
} else {
aid_warn!("failed");
println!(" ✗ Could not verify — saved anyway");
replace_api_key(&mut existing, &key);
}
}
} else if let Some(k) = ¤t_env_key {
println!(" API key: {} (env)", mask_key(k));
print!("\n Save to config? Enter key or press Enter to keep using env: ");
io::stdout().flush()?;
let key = read_line()?;
if !key.is_empty() {
eprint!(" Verifying... ");
io::stderr().flush()?;
if test_openrouter_key(&key) {
aid_info!("OK");
println!(" ✓ Key saved ({})", mask_key(&key));
} else {
aid_warn!("failed");
println!(" ✗ Could not verify — saved anyway");
}
append_query_section(&mut existing, &key);
}
} else {
println!(" API key: not set");
println!("\n Enables `aid query` — fast LLM queries without agent startup.");
println!(" Get a key at: https://openrouter.ai/keys\n");
print!(" Key: ");
io::stdout().flush()?;
let key = read_line()?;
if !key.is_empty() {
eprint!(" Verifying... ");
io::stderr().flush()?;
if test_openrouter_key(&key) {
aid_info!("OK");
println!(" ✓ Key verified ({})", mask_key(&key));
} else {
aid_warn!("failed");
println!(" ✗ Could not verify — saved anyway");
}
append_query_section(&mut existing, &key);
} else {
println!(" Skipped");
}
}
section("Agents");
let builtin = [
("gemini", "gemini"),
("codex", "codex"),
("copilot", "copilot"),
("opencode", "opencode"),
("cursor", "cursor"),
("kilo", "kilo"),
("codebuff", "codebuff"),
("droid", "droid"),
("oz", "oz"),
];
let mut installed = 0;
let mut missing = Vec::new();
for (name, cmd) in builtin {
let found = std::process::Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if found {
installed += 1;
println!(" ✓ {name}");
} else {
missing.push(name);
}
}
let agents_dir = crate::paths::aid_dir().join("agents");
if agents_dir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&agents_dir)
{
for entry in entries.flatten() {
if entry.path().extension().is_some_and(|e| e == "toml") {
let path = entry.path();
let Some(stem) = path.file_stem() else { continue };
let name = stem.to_string_lossy().to_string();
installed += 1;
println!(" ✓ {name} (custom)");
}
}
}
if !missing.is_empty() {
println!(" · not found: {}", missing.join(", "));
}
println!(" {installed} agent(s) ready");
let dir = config_path.parent().context("config path has no parent")?;
std::fs::create_dir_all(dir)?;
std::fs::write(&config_path, &existing)?;
let skills_dir = crate::paths::aid_dir().join("skills");
let needs_init = !skills_dir.is_dir()
|| std::fs::read_dir(&skills_dir)
.map(|d| d.count() == 0)
.unwrap_or(true);
if needs_init {
section("Skills & Templates");
crate::cmd::init::run()?;
}
section(if has_key { "Status" } else { "Done" });
println!(" Config: {}", config_path.display());
if has_key {
println!(" Ready to use.");
} else {
println!();
println!(" Quick start:");
println!(" aid query \"your question\" free LLM query");
println!(" aid query --auto \"question\" paid, better quality");
println!(" aid run codex \"task\" --worktree x dispatch agent");
}
println!();
Ok(())
}
fn section(title: &str) {
println!();
println!(" [{title}]");
}
fn mask_key(key: &str) -> String {
if key.len() > 12 {
format!("{}...{}", &key[..8], &key[key.len() - 4..])
} else {
"****".to_string()
}
}
fn read_line() -> Result<String> {
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
Ok(line.trim().to_string())
}
fn test_openrouter_key(key: &str) -> bool {
let body = serde_json::json!({
"model": "openrouter/free",
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 1
});
let body_str = match serde_json::to_string(&body) {
Ok(s) => s,
Err(_) => return false,
};
let output = match Command::new("curl")
.args([
"-s",
"-X",
"POST",
"https://openrouter.ai/api/v1/chat/completions",
"-H",
&format!("Authorization: Bearer {key}"),
"-H",
"Content-Type: application/json",
"-d",
&body_str,
])
.output()
{
Ok(out) => out,
Err(_) => return false,
};
if !output.status.success() {
return false;
}
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
!stdout.contains("error")
}
fn append_query_section(config: &mut String, key: &str) {
if !config.contains("[query]") {
if !config.is_empty() && !config.ends_with('\n') {
config.push('\n');
}
config.push_str(&format!("\n[query]\napi_key = \"{key}\"\n"));
}
}
fn replace_api_key(config: &mut String, new_key: &str) {
if let Some(start) = config.find("api_key")
&& let Some(eq) = config[start..].find('=')
{
let val_start = start + eq + 1;
let line_end = config[val_start..]
.find('\n')
.map(|p| val_start + p)
.unwrap_or(config.len());
config.replace_range(val_start..line_end, &format!(" \"{new_key}\""));
}
}