sparrow/onboarding/
wizard.rs1use std::io::{self, Write};
11
12use crate::auth::{AuthStore, Credential};
13use crate::config::{Config, ConfigStore, ProviderConfig};
14use crate::provider::detect::{self, DetectedProvider, ProviderTier};
15
16pub async fn run_wizard(
24 config: &Config,
25 store: &dyn ConfigStore,
26) -> anyhow::Result<()> {
27 let _auth = crate::auth::store::ChainedAuthStore::new(config.config_dir.clone());
28
29 print_banner();
30
31 println!("🔍 Détection des providers...\n");
33 let mut providers = detect::detect_all_providers();
34 detect::validate_detected_providers(&mut providers).await;
35
36 let ready = detect::ready_providers(&providers);
37 let free = detect::free_providers(&providers);
38 let gh_available = detect::gh_cli_installed();
39
40 show_detection_summary(&providers, &ready, &free, gh_available);
42
43 println!();
45 println!("1. ⚡ Démarrage rapide — utilise tout ce qui est détecté");
46 println!("2. 🧭 Assistant pas à pas — je choisis ce que je veux configurer");
47 println!("3. 🚪 Quitter sans configurer");
48 print!("\nChoix [1]: ");
49 io::stdout().flush().ok();
50
51 let mut choice = String::new();
52 io::stdin().read_line(&mut choice)?;
53 let choice = choice.trim();
54
55 match choice {
56 "3" => {
57 println!("\n👋 Pas de souci ! Tu pourras configurer plus tard avec : sparrow setup");
58 return Ok(());
59 }
60 "2" => {
61 step_by_step(config, store, &providers, gh_available).await?;
62 }
63 _ => {
64 quick_start(config, store, &ready, gh_available).await?;
66 }
67 }
68
69 Ok(())
70}
71
72fn print_banner() {
75 println!("══════════════════════════════════════════════════");
76 println!(" 🐦 Sparrow — Assistant de configuration");
77 println!(" version {}", env!("CARGO_PKG_VERSION"));
78 println!("══════════════════════════════════════════════════");
79 println!();
80 println!("On va détecter tes clés API et configurer Sparrow");
81 println!("en quelques secondes. C'est gratuit et open-source !");
82 println!();
83}
84
85fn show_detection_summary(
88 all: &[DetectedProvider],
89 ready: &[&DetectedProvider],
90 free: &[&DetectedProvider],
91 gh_available: bool,
92) {
93 if !ready.is_empty() {
94 println!("✅ Providers prêts à l'emploi :");
95 for p in ready {
96 let env_hint = p
97 .env_var
98 .as_ref()
99 .map(|e| format!(" ({e})"))
100 .unwrap_or_default();
101 println!(" • {} — clé détectée et validée{}", p.label, env_hint);
102 }
103 } else {
104 println!("⚠️ Aucune clé API trouvée dans ton environnement.");
105 }
106
107 let need_key: Vec<_> = all
108 .iter()
109 .filter(|p| p.key_found && p.validated == Some(false))
110 .collect();
111 if !need_key.is_empty() {
112 println!("\n⚠️ Clés trouvées mais invalides :");
113 for p in &need_key {
114 println!(" • {} — {}", p.label, p.validation_error.as_deref().unwrap_or("?"));
115 }
116 }
117
118 if !free.is_empty() && ready.is_empty() {
119 println!("\n🎁 Providers GRATUITS disponibles :");
120 for p in free {
121 println!(" • {} — {}", p.label, p.description);
122 if let Some(url) = &p.signup_url {
123 println!(" → Crée une clé : {url}");
124 }
125 if let Some(env) = &p.env_var {
126 println!(" → Puis exporte-la : export {}=\"ta-clé\"", env);
127 }
128 }
129 }
130
131 if gh_available {
132 println!("\n🐙 GitHub CLI détecté (gh) — GitHub Copilot dispo !");
133 println!(" → Connecte-toi avec : gh auth login");
134 println!(" → Puis : gh copilot suggest \"hello\"");
135 }
136
137 let not_configured = all
139 .iter()
140 .filter(|p| !p.key_found)
141 .count();
142 if not_configured > 0 {
143 println!(
144 "\n📋 {} autres providers dispos (payants ou nécessitent une clé).",
145 not_configured
146 );
147 }
148}
149
150async fn quick_start(
153 config: &Config,
154 store: &dyn ConfigStore,
155 ready: &[&DetectedProvider],
156 gh_available: bool,
157) -> anyhow::Result<()> {
158 println!("\n⚡ Démarrage rapide — configuration automatique...\n");
159
160 let auth = crate::auth::store::ChainedAuthStore::new(config.config_dir.clone());
161 let mut updated = config.clone();
162
163 for p in ready {
164 add_provider_to_config(&mut updated, p);
165 if let Some(env_var) = &p.env_var {
167 if let Ok(key) = std::env::var(env_var) {
168 if !key.trim().is_empty() {
169 auth.set(&p.id, Credential::api_key(key))?;
170 println!(" ✓ {:<25} configuré (modèle par défaut)", p.label);
171 }
172 }
173 }
174 }
175
176 if ready.is_empty() {
178 println!(" ⚠️ Aucun provider prêt. Voici des options gratuites :\n");
179 println!(" • NVIDIA NIM — gratuit, crée une clé : https://build.nvidia.com/explore/discover");
180 println!(" puis : export NVIDIA_API_KEY=\"ta-clé\" && sparrow setup");
181 println!(" • Groq — tier gratuit généreux : https://console.groq.com/keys");
182 println!(" puis : export GROQ_API_KEY=\"ta-clé\" && sparrow setup");
183 println!(" • Google Gemini — gratuit 1500 req/j : https://aistudio.google.com/app/apikey");
184 println!(" puis : export GEMINI_API_KEY=\"ta-clé\" && sparrow setup\n");
185 }
186
187 if gh_available {
188 println!(" 💡 GitHub Copilot détecté. Pour l'activer :");
189 println!(" sparrow auth add copilot");
190 }
191
192 store.save(&updated)?;
193 println!("\n✅ Configuration sauvegardée !");
194 println!(" Lance sparrow pour démarrer.\n");
195
196 Ok(())
197}
198
199async fn step_by_step(
202 config: &Config,
203 store: &dyn ConfigStore,
204 providers: &[DetectedProvider],
205 gh_available: bool,
206) -> anyhow::Result<()> {
207 println!("\n🧭 Assistant pas à pas — je te guide.\n");
208
209 let auth = crate::auth::store::ChainedAuthStore::new(config.config_dir.clone());
210 let mut updated = config.clone();
211
212 let mut free_list: Vec<&DetectedProvider> = Vec::new();
214 let mut ready_list: Vec<&DetectedProvider> = Vec::new();
215 let mut need_key_list: Vec<&DetectedProvider> = Vec::new();
216
217 for p in providers {
218 if p.key_found && p.validated == Some(true) {
219 ready_list.push(p);
220 } else if p.tier == ProviderTier::Free {
221 free_list.push(p);
222 } else if p.key_found {
223 need_key_list.push(p);
224 }
225 }
226
227 if !ready_list.is_empty() {
229 println!("── Providers prêts à l'emploi ──");
230 for (i, p) in ready_list.iter().enumerate() {
231 println!(" [{i}] {} — clé validée ✓", p.label);
232 }
233 println!(" [A] Tous les activer");
234 println!(" [N] Aucun, passer à la suite");
235 print!("\nChoix [A]: ");
236 io::stdout().flush().ok();
237
238 let mut answer = String::new();
239 io::stdin().read_line(&mut answer)?;
240 let answer = answer.trim().to_lowercase();
241
242 if answer == "n" {
243 } else if answer == "a" || answer.is_empty() {
245 for p in &ready_list {
246 add_provider_to_config(&mut updated, p);
247 if let Some(env_var) = &p.env_var {
248 if let Ok(key) = std::env::var(env_var) {
249 auth.set(&p.id, Credential::api_key(key))?;
250 }
251 }
252 println!(" ✓ {:<25} configuré", p.label);
253 }
254 } else if let Ok(idx) = answer.parse::<usize>() {
255 if idx < ready_list.len() {
256 let p = ready_list[idx];
257 add_provider_to_config(&mut updated, p);
258 if let Some(env_var) = &p.env_var {
259 if let Ok(key) = std::env::var(env_var) {
260 auth.set(&p.id, Credential::api_key(key))?;
261 }
262 }
263 println!(" ✓ {} configuré", p.label);
264 }
265 }
266 }
267
268 if !free_list.is_empty() && ready_list.is_empty() {
270 println!("\n── Providers gratuits (nécessitent une clé) ──");
271 for (i, p) in free_list.iter().enumerate() {
272 println!(" [{i}] {} — gratuit !", p.label);
273 if let Some(url) = &p.signup_url {
274 println!(" → Inscription : {url}");
275 }
276 }
277 println!(" [N] Passer");
278 print!("\nLequel t'intéresse ? (numéro ou N) [N]: ");
279 io::stdout().flush().ok();
280
281 let mut answer = String::new();
282 io::stdin().read_line(&mut answer)?;
283 let answer = answer.trim().to_lowercase();
284
285 if answer != "n" && !answer.is_empty() {
286 if let Ok(idx) = answer.parse::<usize>() {
287 if idx < free_list.len() {
288 let p = free_list[idx];
289 handle_provider_key_prompt(&mut updated, p, &auth).await?;
290 }
291 }
292 }
293 }
294
295 if gh_available {
297 println!("\n🐙 GitHub Copilot détecté (gh CLI installé).");
298 print!("Activer GitHub Copilot ? [O/n] ");
299 io::stdout().flush().ok();
300
301 let mut answer = String::new();
302 io::stdin().read_line(&mut answer)?;
303 if !matches!(answer.trim().to_lowercase().as_str(), "n" | "no" | "non") {
304 println!(" → Pour utiliser Copilot, assure-toi d'être connecté : gh auth login");
305 updated.providers.entry("copilot".into()).or_insert_with(|| {
306 ProviderConfig {
307 adapter: "openai-compatible".into(),
308 base_url: Some("https://api.githubcopilot.com".into()),
309 models: vec!["gpt-4o".into()],
310 api_key_env: Some("COPILOT_TOKEN".into()),
311 }
312 });
313 println!(" ✓ GitHub Copilot ajouté à la config.");
314 }
315 }
316
317 println!("\n── Autres providers ──");
319 println!("Tu peux aussi configurer un provider manuellement :");
320 println!(" → sparrow auth add <provider>");
321 println!(" → Ou exporte la clé : export PROVIDER_API_KEY=\"ta-clé\"");
322 println!("\nProviders supportés :");
323 for chunk in crate::config::providers::provider_registry().chunks(5) {
324 let ids: Vec<&str> = chunk.iter().map(|d| d.id.as_str()).collect();
325 println!(" {}", ids.join(", "));
326 }
327
328 store.save(&updated)?;
329 println!("\n✅ Configuration sauvegardée !");
330 println!(" Tu peux la modifier à tout moment : sparrow config --edit\n");
331
332 Ok(())
333}
334
335async fn handle_provider_key_prompt(
339 config: &mut Config,
340 provider: &DetectedProvider,
341 auth: &crate::auth::store::ChainedAuthStore,
342) -> anyhow::Result<()> {
343 let env_var_name = provider
344 .env_var
345 .as_deref()
346 .unwrap_or(&provider.id);
347
348 println!();
349 println!("Pour utiliser {}, il te faut une clé API.", provider.label);
350 if let Some(url) = &provider.signup_url {
351 println!("→ Crée une clé ici : {url}");
352 }
353 println!("→ Puis colle-la ci-dessous (ou laisse vide pour passer).");
354 print!("Clé API ({}): ", env_var_name);
355 io::stdout().flush().ok();
356
357 let key = rpassword::read_password().unwrap_or_default();
358 let key = key.trim().to_string();
359
360 if key.is_empty() {
361 println!(" ↳ Passé.");
362 return Ok(());
363 }
364
365 print!(" Validation de la clé... ");
367 io::stdout().flush().ok();
368 match detect::validate_api_key(&provider.id, &key).await {
369 Ok(()) => {
370 println!("✓");
371 auth.set(&provider.id, Credential::api_key(&key))?;
372 unsafe { std::env::set_var(env_var_name, &key); }
374 add_provider_to_config(config, provider);
375 println!(" ✓ {} configuré avec succès !", provider.label);
376 }
377 Err(err) => {
378 println!("✗");
379 println!(" Échec de validation : {err}");
380 print!(" Ajouter quand même ? [o/N] ");
381 io::stdout().flush().ok();
382 let mut answer = String::new();
383 io::stdin().read_line(&mut answer)?;
384 if matches!(answer.trim().to_lowercase().as_str(), "o" | "oui" | "y" | "yes") {
385 auth.set(&provider.id, Credential::api_key(&key))?;
386 unsafe { std::env::set_var(env_var_name, &key); }
388 add_provider_to_config(config, provider);
389 println!(" ✓ {} ajouté (clé non validée).", provider.label);
390 }
391 }
392 }
393
394 Ok(())
395}
396
397fn add_provider_to_config(config: &mut Config, provider: &DetectedProvider) {
399 let def = crate::config::providers::find_provider(&provider.id);
400
401 let entry = config.providers.entry(provider.id.clone()).or_insert_with(|| {
402 ProviderConfig {
403 adapter: def
404 .as_ref()
405 .map(|d| d.adapter.clone())
406 .unwrap_or_else(|| "openai-compatible".into()),
407 base_url: def.as_ref().map(|d| Some(d.base_url.clone())).unwrap_or(None),
408 models: def
409 .as_ref()
410 .map(|d| {
411 crate::config::providers::default_models(&d.id)
412 })
413 .unwrap_or_default(),
414 api_key_env: provider.env_var.clone(),
415 }
416 });
417
418 if let Some(d) = &def {
420 if entry.adapter.is_empty() || entry.adapter == "openai-compatible" {
421 entry.adapter = d.adapter.clone();
422 }
423 if entry.base_url.is_none() {
424 entry.base_url = Some(d.base_url.clone());
425 }
426 if entry.api_key_env.is_none() {
427 entry.api_key_env = d.api_key_env.clone();
428 }
429 if entry.models.is_empty() {
430 entry.models = crate::config::providers::default_models(&d.id);
431 }
432 }
433}