From 589921bbf88a4fd3fa0fbaa87f02718036a4cd13 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Sat, 14 Feb 2026 03:19:00 -0500 Subject: [PATCH] feat: add --quick flag to onboard for non-interactive setup - zeroclaw onboard --quick: generates config with sensible defaults, zero prompts - zeroclaw onboard --quick --api-key sk-... --provider anthropic: one-liner setup - Fixes wizard hanging in non-TTY / IDE terminals - Scaffolds workspace files, prints summary, shows next steps - 1,017 tests, 0 clippy warnings --- src/main.rs | 31 ++++++++-- src/onboard/mod.rs | 2 +- src/onboard/wizard.rs | 132 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1c66172..1dcb99e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,19 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { /// Initialize your workspace and configuration - Onboard, + Onboard { + /// Skip interactive prompts — generate config with sensible defaults + #[arg(long)] + quick: bool, + + /// API key (used with --quick) + #[arg(long)] + api_key: Option, + + /// Provider name (used with --quick, default: openrouter) + #[arg(long)] + provider: Option, + }, /// Start the AI agent loop Agent { @@ -210,9 +222,18 @@ async fn main() -> Result<()> { tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); - // Onboard runs the interactive wizard — no existing config needed - if matches!(cli.command, Commands::Onboard) { - let config = onboard::run_wizard()?; + // Onboard runs the wizard or quick setup — no existing config needed + if let Commands::Onboard { + quick, + api_key, + provider, + } = &cli.command + { + let config = if *quick { + onboard::run_quick_setup(api_key.as_deref(), provider.as_deref())? + } else { + onboard::run_wizard()? + }; // Auto-start channels if user said yes during wizard if std::env::var("ZEROCLAW_AUTOSTART_CHANNELS").as_deref() == Ok("1") { channels::start_channels(config).await?; @@ -224,7 +245,7 @@ async fn main() -> Result<()> { let config = Config::load_or_init()?; match cli.command { - Commands::Onboard => unreachable!(), + Commands::Onboard { .. } => unreachable!(), Commands::Agent { message, diff --git a/src/onboard/mod.rs b/src/onboard/mod.rs index 885f690..0f16b88 100644 --- a/src/onboard/mod.rs +++ b/src/onboard/mod.rs @@ -1,3 +1,3 @@ pub mod wizard; -pub use wizard::run_wizard; +pub use wizard::{run_quick_setup, run_wizard}; diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index 2c1f22b..acf0651 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -153,6 +153,138 @@ pub fn run_wizard() -> Result { Ok(config) } +// ── Quick setup (zero prompts) ─────────────────────────────────── + +/// Non-interactive setup: generates a sensible default config instantly. +/// Use `zeroclaw onboard --quick` or `zeroclaw onboard --quick --api-key sk-... --provider openrouter` +#[allow(clippy::too_many_lines)] +pub fn run_quick_setup(api_key: Option<&str>, provider: Option<&str>) -> Result { + println!("{}", style(BANNER).cyan().bold()); + println!( + " {}", + style("Quick Setup — generating config with sensible defaults...") + .white() + .bold() + ); + println!(); + + let home = directories::UserDirs::new() + .map(|u| u.home_dir().to_path_buf()) + .context("Could not find home directory")?; + let zeroclaw_dir = home.join(".zeroclaw"); + let workspace_dir = zeroclaw_dir.join("workspace"); + let config_path = zeroclaw_dir.join("config.toml"); + + fs::create_dir_all(&workspace_dir).context("Failed to create workspace directory")?; + + let provider_name = provider.unwrap_or("openrouter").to_string(); + let model = default_model_for_provider(&provider_name); + + let config = Config { + workspace_dir: workspace_dir.clone(), + config_path: config_path.clone(), + api_key: api_key.map(String::from), + default_provider: Some(provider_name.clone()), + default_model: Some(model.clone()), + default_temperature: 0.7, + observability: ObservabilityConfig::default(), + autonomy: AutonomyConfig { + level: AutonomyLevel::Full, + workspace_only: false, + ..AutonomyConfig::default() + }, + runtime: RuntimeConfig::default(), + heartbeat: HeartbeatConfig::default(), + channels_config: ChannelsConfig::default(), + memory: MemoryConfig::default(), + tunnel: crate::config::TunnelConfig::default(), + gateway: crate::config::GatewayConfig::default(), + composio: ComposioConfig::default(), + secrets: SecretsConfig::default(), + }; + + config.save()?; + + // Scaffold minimal workspace files + let default_ctx = ProjectContext { + user_name: std::env::var("USER").unwrap_or_else(|_| "User".into()), + timezone: "UTC".into(), + agent_name: "ZeroClaw".into(), + communication_style: "Direct and concise".into(), + }; + scaffold_workspace(&workspace_dir, &default_ctx)?; + + println!( + " {} Workspace: {}", + style("✓").green().bold(), + style(workspace_dir.display()).green() + ); + println!( + " {} Provider: {}", + style("✓").green().bold(), + style(&provider_name).green() + ); + println!( + " {} Model: {}", + style("✓").green().bold(), + style(&model).green() + ); + println!( + " {} API Key: {}", + style("✓").green().bold(), + if api_key.is_some() { + style("set").green() + } else { + style("not set (use --api-key or edit config.toml)").yellow() + } + ); + println!( + " {} Security: {}", + style("✓").green().bold(), + style("Full Autonomy").green() + ); + println!( + " {} Memory: {}", + style("✓").green().bold(), + style("sqlite (auto-save)").green() + ); + println!( + " {} Secrets: {}", + style("✓").green().bold(), + style("encrypted").green() + ); + println!(); + println!( + " {} {}", + style("Config saved:").white().bold(), + style(config_path.display()).green() + ); + println!(); + println!(" Next steps:"); + if api_key.is_none() { + println!(" 1. Set your API key: export OPENROUTER_API_KEY=\"sk-...\""); + println!(" 2. Or edit: ~/.zeroclaw/config.toml"); + println!(" 3. Run: zeroclaw agent -m \"Hello!\""); + } else { + println!(" Run: zeroclaw agent -m \"Hello!\""); + } + println!(); + + Ok(config) +} + +/// Pick a sensible default model for the given provider. +fn default_model_for_provider(provider: &str) -> String { + match provider { + "anthropic" => "claude-sonnet-4-20250514".into(), + "openai" => "gpt-4o".into(), + "ollama" => "llama3.2".into(), + "groq" => "llama-3.3-70b-versatile".into(), + "deepseek" => "deepseek-chat".into(), + _ => "anthropic/claude-sonnet-4-20250514".into(), + } +} + // ── Step helpers ───────────────────────────────────────────────── fn print_step(current: u8, total: u8, title: &str) {