use crate::ui::{
self,
prompts::{self, AuthProvider, OnboardFlow},
};
use anyhow::Result;
use openclaw_core::{ApiKey, Config, CredentialStore};
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct OnboardArgs {
pub non_interactive: bool,
pub accept_risk: bool,
pub flow: Option<String>,
pub auth_choice: Option<String>,
pub api_key: Option<String>,
pub install_daemon: bool,
}
pub async fn run_onboard(args: OnboardArgs) -> Result<()> {
ui::banner();
ui::header("Welcome to OpenClaw Setup");
if !args.accept_risk {
if args.non_interactive {
anyhow::bail!("Non-interactive mode requires --accept-risk flag");
}
let accepted = prompts::risk_acknowledgement()?;
if !accepted {
ui::error("Setup cancelled. You must accept the risks to continue.");
return Ok(());
}
}
ui::success("Risk acknowledgement accepted");
let config_path = get_config_path();
let existing_config = Config::load_default().ok();
if existing_config.is_some() {
ui::info("Existing configuration found");
if !args.non_interactive {
let options = [
("Keep", "Keep existing values, update only new settings"),
("Reset", "Start fresh with new configuration"),
];
let choice = prompts::select_with_help("Existing config found", &options)?;
if choice == 1 {
ui::warning("Resetting configuration...");
}
}
}
let flow = if let Some(f) = &args.flow {
match f.to_lowercase().as_str() {
"quickstart" | "quick" => OnboardFlow::QuickStart,
"advanced" | "manual" => OnboardFlow::Advanced,
_ => {
if args.non_interactive {
OnboardFlow::QuickStart
} else {
prompts::select_onboard_flow()?
}
}
}
} else if args.non_interactive {
OnboardFlow::QuickStart
} else {
prompts::select_onboard_flow()?
};
ui::info(&format!("Using {flow} mode"));
ui::header("Gateway Configuration");
let (bind_address, port) = match flow {
OnboardFlow::QuickStart => {
ui::info("Gateway: localhost:18789 (loopback only)");
("127.0.0.1".to_string(), 18789u16)
}
OnboardFlow::Advanced => {
if args.non_interactive {
("127.0.0.1".to_string(), 18789u16)
} else {
let port_str = prompts::input_with_default("Gateway port", "18789")?;
let port: u16 = port_str.parse().unwrap_or(18789);
let bind_options = [
("Loopback", "127.0.0.1 - local access only (recommended)"),
("LAN", "0.0.0.0 - accessible from local network"),
];
let bind_choice = prompts::select_with_help("Bind address", &bind_options)?;
let bind = match bind_choice {
0 => "127.0.0.1".to_string(),
_ => "0.0.0.0".to_string(),
};
(bind, port)
}
}
};
ui::success(&format!("Gateway configured: {bind_address}:{port}"));
ui::header("Authentication Setup");
let provider = if let Some(auth) = &args.auth_choice {
match auth.to_lowercase().as_str() {
"anthropic" => AuthProvider::Anthropic,
"openai" => AuthProvider::OpenAI,
"openrouter" => AuthProvider::OpenRouter,
"skip" => AuthProvider::Skip,
_ => {
if args.non_interactive {
AuthProvider::Skip
} else {
prompts::select_auth_provider()?
}
}
}
} else if args.non_interactive {
AuthProvider::Skip
} else {
prompts::select_auth_provider()?
};
if provider == AuthProvider::Skip {
ui::info("Skipping authentication setup");
} else {
let api_key = if let Some(key) = &args.api_key {
key.clone()
} else if args.non_interactive {
anyhow::bail!("API key required in non-interactive mode");
} else {
prompts::password(&format!("Enter {provider} API key"))?
};
let cred_path = get_credentials_path();
std::fs::create_dir_all(&cred_path)?;
let encryption_key: [u8; 32] = rand::random();
let store = CredentialStore::new(encryption_key, cred_path);
let provider_name = match provider {
AuthProvider::Anthropic => "anthropic",
AuthProvider::OpenAI => "openai",
AuthProvider::OpenRouter => "openrouter",
AuthProvider::Skip => unreachable!(),
};
store.store(provider_name, &ApiKey::new(api_key))?;
ui::success(&format!("{provider} credentials stored"));
}
ui::header("Workspace Setup");
let workspace = get_workspace_path();
std::fs::create_dir_all(&workspace)?;
ui::success(&format!("Workspace: {}", workspace.display()));
ui::header("Saving Configuration");
let config = create_config(&bind_address, port, &workspace, provider);
let config_json = serde_json::to_string_pretty(&config)?;
std::fs::create_dir_all(config_path.parent().unwrap())?;
std::fs::write(&config_path, config_json)?;
ui::success(&format!("Configuration saved: {}", config_path.display()));
ui::header("Shell Completion");
if !args.non_interactive {
let install_completion = prompts::confirm("Install shell completions?")?;
if install_completion {
ui::info("Run 'openclaw completion --install' to set up completions");
}
}
if args.install_daemon {
ui::header("Daemon Installation");
ui::info("Run 'openclaw daemon install' to install as a system service");
}
ui::header("Setup Complete!");
println!();
ui::kv("Config", &config_path.display().to_string());
ui::kv("Workspace", &workspace.display().to_string());
ui::kv("Gateway", &format!("{bind_address}:{port}"));
println!();
ui::info("Next steps:");
println!(" 1. Start the gateway: openclaw gateway run");
println!(" 2. Check status: openclaw status");
println!(" 3. Run diagnostics: openclaw doctor");
Ok(())
}
fn get_config_path() -> PathBuf {
if let Ok(path) = std::env::var("OPENCLAW_CONFIG_PATH") {
return PathBuf::from(path);
}
if let Ok(state_dir) = std::env::var("OPENCLAW_STATE_DIR") {
return PathBuf::from(state_dir).join("openclaw.json");
}
dirs::home_dir().map_or_else(
|| PathBuf::from(".openclaw/openclaw.json"),
|h| h.join(".openclaw").join("openclaw.json"),
)
}
fn get_credentials_path() -> PathBuf {
if let Ok(path) = std::env::var("OPENCLAW_OAUTH_DIR") {
return PathBuf::from(path);
}
dirs::home_dir().map_or_else(
|| PathBuf::from(".openclaw/credentials"),
|h| h.join(".openclaw").join("credentials"),
)
}
fn get_workspace_path() -> PathBuf {
dirs::home_dir().map_or_else(
|| PathBuf::from(".openclaw/workspace"),
|h| h.join(".openclaw").join("workspace"),
)
}
fn create_config(
bind_address: &str,
port: u16,
workspace: &PathBuf,
provider: AuthProvider,
) -> serde_json::Value {
let default_model = match provider {
AuthProvider::Anthropic => "claude-sonnet-4-20250514",
AuthProvider::OpenAI => "gpt-4o",
AuthProvider::OpenRouter => "anthropic/claude-sonnet-4-20250514",
AuthProvider::Skip => "claude-sonnet-4-20250514",
};
let default_provider = match provider {
AuthProvider::Anthropic => "anthropic",
AuthProvider::OpenAI => "openai",
AuthProvider::OpenRouter => "openrouter",
AuthProvider::Skip => "anthropic",
};
serde_json::json!({
"gateway": {
"mode": "local",
"port": port,
"bind": if bind_address == "127.0.0.1" { "loopback" } else { "lan" },
"auth": {
"mode": "token",
"token": generate_token()
}
},
"agents": {
"defaults": {
"workspace": workspace.display().to_string(),
"model": default_model,
"provider": default_provider
}
},
"channels": {},
"wizard": {
"lastRunAt": chrono::Utc::now().to_rfc3339(),
"lastRunVersion": env!("CARGO_PKG_VERSION"),
"lastRunCommand": "onboard"
}
})
}
fn generate_token() -> String {
use rand::Rng;
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
(0..32)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}