Skip to main content

sparrow/cmd_handlers/
setup_cmd.rs

1// src/cmd_handlers/setup_cmd.rs — sparrow setup command handler
2
3use sparrow::auth::store::ChainedAuthStore;
4use sparrow::auth::{AuthStore, Credential};
5use sparrow::config::{Config, ConfigStore, FsConfigStore, ProviderConfig};
6
7pub async fn handle_setup(config: &Config, store: &FsConfigStore) -> anyhow::Result<()> {
8    use sparrow::tui::theme::boot_sequence;
9    use std::io::{self, Write};
10
11    for line in boot_sequence() {
12        println!("{}", line);
13    }
14    println!();
15    println!("═══ SPARROW SETUP ═══");
16    println!();
17    println!("Sparrow setup configures providers, model routing, budget, and autonomy.");
18    println!();
19    println!("Current configuration:");
20    println!("  Config dir : {:?}", config.config_dir);
21    println!("  State dir  : {:?}", config.state_dir);
22    println!("  Autonomy   : {:?}", config.defaults.autonomy);
23    println!(
24        "  Budget     : ${}/day, ${}/session",
25        config.budget.daily_usd, config.budget.session_usd
26    );
27    println!();
28
29    let effective = sparrow::config::effective_provider_configs(config);
30    if effective.is_empty() {
31        println!("No provider detected yet.");
32    } else {
33        println!("Detected/configured providers:");
34        for (name, pconfig) in &effective {
35            println!("  {} (adapter: {})", name, pconfig.adapter);
36            for model in &pconfig.models {
37                println!("    - {}", model);
38            }
39        }
40    }
41
42    println!();
43    println!("Recommended first setup:");
44    println!("  - local/free: ollama");
45    println!("  - cheap cloud: nvidia");
46    println!("  - strong cloud: anthropic");
47    println!();
48    print!("Configure or update a provider now? [Y/n] ");
49    io::stdout().flush().ok();
50    let mut answer = String::new();
51    io::stdin().read_line(&mut answer)?;
52    if matches!(answer.trim().to_lowercase().as_str(), "n" | "no" | "non") {
53        println!("Setup left unchanged. Run 'sparrow console' for the WebView config panel.");
54        return Ok(());
55    }
56
57    let registry = sparrow::config::providers::provider_registry();
58    println!("\nAvailable providers:");
59    for def in registry.iter().take(18) {
60        let env_state = def
61            .api_key_env
62            .as_ref()
63            .map(|env| {
64                if std::env::var(env)
65                    .map(|v| !v.trim().is_empty())
66                    .unwrap_or(false)
67                {
68                    "env found"
69                } else {
70                    "env missing"
71                }
72            })
73            .unwrap_or("no key needed");
74        println!("  {:18} {:22} {}", def.id, def.label, env_state);
75    }
76    println!("  custom             Custom Endpoint");
77
78    print!("\nProvider id [nvidia]: ");
79    io::stdout().flush().ok();
80    let mut provider_id = String::new();
81    io::stdin().read_line(&mut provider_id)?;
82    let provider_id = provider_id.trim();
83    let provider_id = if provider_id.is_empty() {
84        "nvidia"
85    } else {
86        provider_id
87    };
88    let Some(def) = sparrow::config::providers::find_provider(provider_id) else {
89        anyhow::bail!(
90            "Unknown provider '{}'. Use 'sparrow model --list' or the WebView config panel.",
91            provider_id
92        );
93    };
94
95    let default_models = sparrow::config::providers::default_models(&def.id);
96    let default_model = default_models
97        .first()
98        .cloned()
99        .unwrap_or_else(|| "model".into());
100    print!("Model [{}]: ", default_model);
101    io::stdout().flush().ok();
102    let mut model = String::new();
103    io::stdin().read_line(&mut model)?;
104    let model = model.trim();
105    let model = if model.is_empty() {
106        default_model
107    } else {
108        model.to_string()
109    };
110
111    let mut next = config.clone();
112    next.providers.insert(
113        def.id.clone(),
114        ProviderConfig {
115            adapter: def.adapter.clone(),
116            base_url: Some(def.base_url.clone()),
117            models: vec![model],
118            api_key_env: def.api_key_env.clone(),
119        },
120    );
121
122    print!(
123        "Default routing provider for medium tasks [{}]? [Y/n] ",
124        def.id
125    );
126    io::stdout().flush().ok();
127    let mut route_answer = String::new();
128    io::stdin().read_line(&mut route_answer)?;
129    if !matches!(
130        route_answer.trim().to_lowercase().as_str(),
131        "n" | "no" | "non"
132    ) {
133        next.routing.policy.insert("medium".into(), def.id.clone());
134        if def.tags.iter().any(|t| t == "strong" || t == "code") {
135            next.routing.policy.insert("small".into(), def.id.clone());
136        }
137    }
138
139    if let Some(env_name) = &def.api_key_env {
140        if std::env::var(env_name)
141            .map(|v| !v.trim().is_empty())
142            .unwrap_or(false)
143        {
144            println!(
145                "Credential: {} is already present in environment.",
146                env_name
147            );
148        } else {
149            print!(
150                "Paste API key for {} now, or leave empty to use env later: ",
151                def.label
152            );
153            io::stdout().flush().ok();
154            let mut key = String::new();
155            io::stdin().read_line(&mut key)?;
156            let key = key.trim();
157            if !key.is_empty() {
158                let auth = ChainedAuthStore::new(next.config_dir.clone());
159                auth.set(&def.id, Credential::api_key(key.to_string()))?;
160                println!("Credential stored for {}.", def.id);
161            }
162        }
163    }
164
165    store.save(&next)?;
166    println!("\nSetup saved.");
167    println!("Run 'sparrow doctor' to verify or 'sparrow console' for the graphical WebView.");
168
169    Ok(())
170}