Skip to main content

oxios_kernel/
onboarding.rs

1//! Interactive first-run setup wizard.
2//!
3//! Detects existing credentials (oxi auth store, config.toml).
4//! If none found, runs an interactive wizard that stores the API key
5//! in `~/.oxi/auth.json` (shared with oxi CLI if installed).
6
7use crate::config::OxiosConfig;
8use crate::credential::CredentialStore;
9use std::io::{self, Write};
10
11const WORKSPACE_SUBDIRS: &[&str] = &[
12    "workspace",
13    "workspace/memory",
14    "workspace/memory/knowledge",
15    "workspace/seeds",
16    "workspace/sessions",
17    "workspace/skills",
18    "workspace/programs",
19];
20
21/// Check if any credentials exist (config.toml, oxi auth store, or env vars).
22/// Returns true if credentials are found (onboarding should be skipped).
23pub fn has_credentials(config: &OxiosConfig) -> bool {
24    let provider = CredentialStore::provider_from_model(&config.engine.default_model);
25    CredentialStore::has_credential(provider, config.api_key().as_deref())
26}
27
28/// Run the onboarding wizard if no credentials are configured.
29///
30/// Returns `Ok(true)` if onboarding completed (new credentials stored).
31/// Returns `Ok(false)` if skipped (already configured).
32pub fn run_onboarding(
33    oxios_home: &std::path::Path,
34    config: &mut OxiosConfig,
35) -> anyhow::Result<bool> {
36    let provider = CredentialStore::provider_from_model(&config.engine.default_model);
37
38    // Check if already configured
39    if CredentialStore::has_credential(provider, config.api_key().as_deref()) {
40        return Ok(false);
41    }
42
43    print_banner();
44    print_intro();
45
46    // 1. Ask provider
47    let provider = prompt_provider()?;
48
49    // 2. Check if oxi auth.json already has credentials for this provider
50    if let Ok(Some(token)) = oxi_sdk::load_token(provider) {
51        if !token.access_token.is_empty() {
52            println!();
53            println!(
54                "  ── Detected ~/.oxi/auth.json with '{}' credentials ──",
55                provider
56            );
57            if prompt_bool("  Use existing credentials?", true) {
58                // Update config with the provider's default model
59                config.engine.default_model =
60                    format!("{}/{}", provider, default_model_for(provider));
61                write_config(oxios_home, config)?;
62                print_success(oxios_home, &config.engine.default_model);
63                return Ok(true);
64            }
65        }
66    }
67
68    // 3. Ask for API key
69    print!("\n  Enter your {} API key: ", provider.to_uppercase());
70    io::stdout().flush()?;
71    let api_key = read_line();
72    if api_key.trim().is_empty() {
73        println!("  API key is required — setup cancelled.");
74        return Ok(false);
75    }
76
77    // 4. Ask model
78    let model_default = default_model_for(provider);
79    print!("  Default model [{}]: ", model_default);
80    io::stdout().flush()?;
81    let model_input = read_line();
82    let model = if model_input.trim().is_empty() {
83        format!("{}/{}", provider, model_default)
84    } else {
85        format!("{}/{}", provider, model_input.trim())
86    };
87
88    // 5. Workspace
89    let default_workspace = dirs::home_dir()
90        .map(|h| format!("{}/.oxios/workspace", h.display()))
91        .unwrap_or_else(|| "~/.oxios/workspace".to_string());
92    print!("  Workspace [{}]: ", default_workspace);
93    io::stdout().flush()?;
94    let workspace = read_line();
95    let workspace = if workspace.trim().is_empty() {
96        default_workspace
97    } else {
98        workspace.trim().to_string()
99    };
100    let workspace = crate::config::expand_home(&workspace)
101        .to_string_lossy()
102        .to_string();
103
104    // 6. Store credentials in ~/.oxi/auth.json
105    print!("\n  Storing credentials... ");
106    io::stdout().flush()?;
107    CredentialStore::store(provider, api_key.trim())?;
108    println!("done");
109
110    // 7. Create workspace directories
111    print!("  Creating workspace... ");
112    io::stdout().flush()?;
113    std::fs::create_dir_all(&workspace)?;
114    for subdir in WORKSPACE_SUBDIRS {
115        std::fs::create_dir_all(std::path::Path::new(&workspace).join(subdir))?;
116    }
117    println!("done");
118
119    // 8. Update config
120    config.engine.default_model = model;
121    config.kernel.workspace = workspace;
122    write_config(oxios_home, config)?;
123
124    print_success(oxios_home, &config.engine.default_model);
125    Ok(true)
126}
127
128fn default_model_for(provider: &str) -> &str {
129    match provider {
130        "anthropic" => "claude-sonnet-4-20250514",
131        "openai" => "gpt-4o",
132        "google" => "gemini-2.0-flash",
133        "deepseek" => "deepseek-chat",
134        "groq" => "llama-3.3-70b-versatile",
135        _ => "default",
136    }
137}
138
139fn prompt_provider() -> anyhow::Result<&'static str> {
140    println!();
141    println!("  Select LLM provider:");
142    println!("    1) Anthropic (Claude)");
143    println!("    2) OpenAI");
144    println!("    3) Google (Gemini)");
145    println!("    4) DeepSeek");
146    println!("    5) Groq");
147    loop {
148        print!("  Enter choice [1]: ");
149        io::stdout().flush()?;
150        let input = read_line();
151        let choice = if input.trim().is_empty() {
152            "1"
153        } else {
154            input.trim()
155        };
156        let provider = match choice {
157            "1" => "anthropic",
158            "2" => "openai",
159            "3" => "google",
160            "4" => "deepseek",
161            "5" => "groq",
162            _ => {
163                println!("  Invalid choice — enter 1-5");
164                continue;
165            }
166        };
167        return Ok(provider);
168    }
169}
170
171fn write_config(oxios_home: &std::path::Path, config: &OxiosConfig) -> anyhow::Result<()> {
172    std::fs::create_dir_all(oxios_home)?;
173    let toml_str = toml::to_string_pretty(config)
174        .map_err(|e| anyhow::anyhow!("failed to serialize config: {}", e))?;
175    let config_path = oxios_home.join("config.toml");
176    std::fs::write(&config_path, &toml_str)?;
177    Ok(())
178}
179
180fn print_banner() {
181    println!();
182    println!("  ╔═══════════════════════════════════════════╗");
183    println!("  ║         ⬡  Oxios — First-Time Setup       ║");
184    println!("  ╚═══════════════════════════════════════════╝");
185}
186
187fn print_intro() {
188    println!();
189    println!("  Welcome! This wizard configures your API credentials.");
190    println!("  Press Ctrl+C at any time to cancel.");
191}
192
193fn print_success(oxios_home: &std::path::Path, model: &str) {
194    println!();
195    println!("  ╔═══════════════════════════════════════════╗");
196    println!("  ║             ✅  Setup Complete!            ║");
197    println!("  ╚═══════════════════════════════════════════╝");
198    println!();
199    println!("    Config:  {}", oxios_home.join("config.toml").display());
200    println!("    Model:   {}", model);
201    println!();
202    println!("  Next steps:");
203    println!("    oxios              → start the daemon");
204    println!("    oxios daemon install → install as system service");
205    println!("    open http://127.0.0.1:4200");
206    println!();
207}
208
209fn prompt_bool(prompt: &str, default: bool) -> bool {
210    let suffix = if default { "[Y/n]" } else { "[y/N]" };
211    print!("{} {}: ", prompt, suffix);
212    io::stdout().flush().unwrap_or_default();
213    let input = read_line();
214    if input.trim().is_empty() {
215        return default;
216    }
217    matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
218}
219
220fn read_line() -> String {
221    let mut buf = String::new();
222    io::stdin().read_line(&mut buf).unwrap_or_default();
223    buf.trim_end().to_string()
224}