oxios_kernel/
onboarding.rs1use 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
21pub 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
28pub 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 if CredentialStore::has_credential(provider, config.api_key().as_deref()) {
40 return Ok(false);
41 }
42
43 print_banner();
44 print_intro();
45
46 let provider = prompt_provider()?;
48
49 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 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 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 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 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 print!("\n Storing credentials... ");
106 io::stdout().flush()?;
107 CredentialStore::store(provider, api_key.trim())?;
108 println!("done");
109
110 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 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}