diff --git a/src/bootstrap.rs b/src/bootstrap.rs index 3641c790..90ce74c8 100644 --- a/src/bootstrap.rs +++ b/src/bootstrap.rs @@ -492,4 +492,32 @@ INJECTED="pwned"#; assert_eq!(parsed.len(), 2); assert!(parsed.iter().all(|(k, _)| k != "DATABASE_URL")); } + + #[test] + fn test_onboard_completed_round_trips_through_env() { + let dir = tempdir().unwrap(); + let env_path = dir.path().join(".env"); + + // Simulate what the wizard writes: bootstrap vars + ONBOARD_COMPLETED + let vars = [ + ("DATABASE_BACKEND", "libsql"), + ("ONBOARD_COMPLETED", "true"), + ]; + let mut content = String::new(); + for (key, value) in &vars { + let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); + content.push_str(&format!("{}=\"{}\"\n", key, escaped)); + } + std::fs::write(&env_path, &content).unwrap(); + + // Verify dotenvy parses ONBOARD_COMPLETED correctly + let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + assert_eq!(parsed.len(), 2); + let onboard = parsed.iter().find(|(k, _)| k == "ONBOARD_COMPLETED"); + assert!(onboard.is_some(), "ONBOARD_COMPLETED must be present"); + assert_eq!(onboard.unwrap().1, "true"); + } } diff --git a/src/main.rs b/src/main.rs index 7f0252a9..dbd8875d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1575,14 +1575,21 @@ fn check_onboard_needed() -> Option<&'static str> { return Some("Database not configured"); } + // The wizard writes ONBOARD_COMPLETED=true to ~/.ironclaw/.env, + // which load_ironclaw_env() loads before this function runs. + if std::env::var("ONBOARD_COMPLETED") + .map(|v| v == "true") + .unwrap_or(false) + { + return None; + } + // First run (onboarding never completed and no session). - // Reads NEARAI_API_KEY env var directly because this function runs - // before Config is loaded -- Config::from_env() may fail without a - // database URL, which is what triggers onboarding in the first place. + // Check for a NEAR AI API key or session file as a fallback + // for users who configured credentials manually (no wizard). if std::env::var("NEARAI_API_KEY").is_err() { - let settings = ironclaw::settings::Settings::load(); let session_path = ironclaw::llm::session::default_session_path(); - if !settings.onboard_completed && !session_path.exists() { + if !session_path.exists() { return Some("First run"); } } diff --git a/src/setup/README.md b/src/setup/README.md index dda642b3..36889d30 100644 --- a/src/setup/README.md +++ b/src/setup/README.md @@ -19,8 +19,9 @@ Explicit invocation. Loads `.env` files, runs the wizard, exits. ironclaw (first run, no database configured) ``` -Auto-detection via `check_onboard_needed()` in `main.rs`. Triggers when -none of these are true: +Auto-detection via `check_onboard_needed()` in `main.rs`. Skips onboarding +when `ONBOARD_COMPLETED` env var is set (written to `~/.ironclaw/.env` by +the wizard). Otherwise triggers when no database is configured: - `DATABASE_URL` env var is set - `LIBSQL_PATH` env var is set - `~/.ironclaw/ironclaw.db` exists on disk @@ -345,13 +346,14 @@ Final step of the wizard: 1. Mark onboard_completed = true 2. Write ALL settings to database (try postgres pool, then libSQL backend) 3. Write bootstrap vars to ~/.ironclaw/.env: - - DATABASE_BACKEND (always) - - DATABASE_URL (if postgres) - - LIBSQL_PATH (if libsql) - - LIBSQL_URL (if turso sync) - - LLM_BACKEND (always, when set) - - LLM_BASE_URL (if openai_compatible) - - OLLAMA_BASE_URL (if ollama) + - DATABASE_BACKEND (always) + - DATABASE_URL (if postgres) + - LIBSQL_PATH (if libsql) + - LIBSQL_URL (if turso sync) + - LLM_BACKEND (always, when set) + - LLM_BASE_URL (if openai_compatible) + - OLLAMA_BASE_URL (if ollama) + - ONBOARD_COMPLETED (always, "true") 4. Print configuration summary ``` diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index e204b17c..e91f922b 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -1542,6 +1542,10 @@ impl SetupWizard { env_vars.push(("OLLAMA_BASE_URL", url.clone())); } + // Always write ONBOARD_COMPLETED so that check_onboard_needed() + // (which runs before the DB is connected) knows to skip re-onboarding. + env_vars.push(("ONBOARD_COMPLETED", "true".to_string())); + if !env_vars.is_empty() { let pairs: Vec<(&str, &str)> = env_vars.iter().map(|(k, v)| (*k, v.as_str())).collect();