use crate::config::OxiosConfig;
use crate::credential::CredentialStore;
use std::io::{self, Write};
const WORKSPACE_SUBDIRS: &[&str] = &[
"workspace",
"workspace/memory",
"workspace/memory/knowledge",
"workspace/seeds",
"workspace/sessions",
"workspace/skills",
"workspace/programs",
];
pub fn has_credentials(config: &OxiosConfig) -> bool {
let provider = CredentialStore::provider_from_model(&config.engine.default_model);
CredentialStore::has_credential(provider, config.api_key().as_deref())
}
pub fn run_onboarding(
oxios_home: &std::path::Path,
config: &mut OxiosConfig,
) -> anyhow::Result<bool> {
let provider = CredentialStore::provider_from_model(&config.engine.default_model);
if CredentialStore::has_credential(provider, config.api_key().as_deref()) {
return Ok(false);
}
print_banner();
print_intro();
let provider = prompt_provider()?;
if let Ok(Some(token)) = oxi_sdk::load_token(provider) {
if !token.access_token.is_empty() {
println!();
println!(
" ── Detected ~/.oxi/auth.json with '{}' credentials ──",
provider
);
if prompt_bool(" Use existing credentials?", true) {
config.engine.default_model =
format!("{}/{}", provider, default_model_for(provider));
write_config(oxios_home, config)?;
print_success(oxios_home, &config.engine.default_model);
return Ok(true);
}
}
}
print!("\n Enter your {} API key: ", provider.to_uppercase());
io::stdout().flush()?;
let api_key = read_line();
if api_key.trim().is_empty() {
println!(" API key is required — setup cancelled.");
return Ok(false);
}
let model_default = default_model_for(provider);
print!(" Default model [{}]: ", model_default);
io::stdout().flush()?;
let model_input = read_line();
let model = if model_input.trim().is_empty() {
format!("{}/{}", provider, model_default)
} else {
format!("{}/{}", provider, model_input.trim())
};
let default_workspace = dirs::home_dir()
.map(|h| format!("{}/.oxios/workspace", h.display()))
.unwrap_or_else(|| "~/.oxios/workspace".to_string());
print!(" Workspace [{}]: ", default_workspace);
io::stdout().flush()?;
let workspace = read_line();
let workspace = if workspace.trim().is_empty() {
default_workspace
} else {
workspace.trim().to_string()
};
let workspace = crate::config::expand_home(&workspace)
.to_string_lossy()
.to_string();
print!("\n Storing credentials... ");
io::stdout().flush()?;
CredentialStore::store(provider, api_key.trim())?;
println!("done");
print!(" Creating workspace... ");
io::stdout().flush()?;
std::fs::create_dir_all(&workspace)?;
for subdir in WORKSPACE_SUBDIRS {
std::fs::create_dir_all(std::path::Path::new(&workspace).join(subdir))?;
}
println!("done");
config.engine.default_model = model;
config.kernel.workspace = workspace;
write_config(oxios_home, config)?;
print_success(oxios_home, &config.engine.default_model);
Ok(true)
}
fn default_model_for(provider: &str) -> &str {
match provider {
"anthropic" => "claude-sonnet-4-20250514",
"openai" => "gpt-4o",
"google" => "gemini-2.0-flash",
"deepseek" => "deepseek-chat",
"groq" => "llama-3.3-70b-versatile",
_ => "default",
}
}
fn prompt_provider() -> anyhow::Result<&'static str> {
println!();
println!(" Select LLM provider:");
println!(" 1) Anthropic (Claude)");
println!(" 2) OpenAI");
println!(" 3) Google (Gemini)");
println!(" 4) DeepSeek");
println!(" 5) Groq");
loop {
print!(" Enter choice [1]: ");
io::stdout().flush()?;
let input = read_line();
let choice = if input.trim().is_empty() {
"1"
} else {
input.trim()
};
let provider = match choice {
"1" => "anthropic",
"2" => "openai",
"3" => "google",
"4" => "deepseek",
"5" => "groq",
_ => {
println!(" Invalid choice — enter 1-5");
continue;
}
};
return Ok(provider);
}
}
fn write_config(oxios_home: &std::path::Path, config: &OxiosConfig) -> anyhow::Result<()> {
std::fs::create_dir_all(oxios_home)?;
let toml_str = toml::to_string_pretty(config)
.map_err(|e| anyhow::anyhow!("failed to serialize config: {}", e))?;
let config_path = oxios_home.join("config.toml");
std::fs::write(&config_path, &toml_str)?;
Ok(())
}
fn print_banner() {
println!();
println!(" ╔═══════════════════════════════════════════╗");
println!(" ║ ⬡ Oxios — First-Time Setup ║");
println!(" ╚═══════════════════════════════════════════╝");
}
fn print_intro() {
println!();
println!(" Welcome! This wizard configures your API credentials.");
println!(" Press Ctrl+C at any time to cancel.");
}
fn print_success(oxios_home: &std::path::Path, model: &str) {
println!();
println!(" ╔═══════════════════════════════════════════╗");
println!(" ║ ✅ Setup Complete! ║");
println!(" ╚═══════════════════════════════════════════╝");
println!();
println!(" Config: {}", oxios_home.join("config.toml").display());
println!(" Model: {}", model);
println!();
println!(" Next steps:");
println!(" oxios → start the daemon");
println!(" oxios daemon install → install as system service");
println!(" open http://127.0.0.1:4200");
println!();
}
fn prompt_bool(prompt: &str, default: bool) -> bool {
let suffix = if default { "[Y/n]" } else { "[y/N]" };
print!("{} {}: ", prompt, suffix);
io::stdout().flush().unwrap_or_default();
let input = read_line();
if input.trim().is_empty() {
return default;
}
matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
}
fn read_line() -> String {
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap_or_default();
buf.trim_end().to_string()
}