use anyhow::Result;
use inquire::{Confirm, Password, Select, Text};
use crate::compose::{BotConfig, GardenConfig};
use crate::deps;
use crate::garden::load_gardens;
use crate::providers::{ProviderAuthMethod, ProviderPlugin, ProviderRegistry};
use crate::ui;
use std::sync::Arc;
type SelectedProvider = (Arc<ProviderPlugin>, String, String);
const TOTAL_STEPS: usize = 7;
pub fn run_wizard() -> Result<()> {
run_wizard_inner(false)
}
pub fn run_wizard_skip_deps() -> Result<()> {
run_wizard_inner(true)
}
fn run_wizard_inner(skip_deps: bool) -> Result<()> {
ui::print_banner();
println!(
" {}Welcome to the garden setup wizard.{}",
ui::DIM,
ui::RESET
);
println!(
" {}We'll walk through {} steps to plant your garden.{}",
ui::DIM,
TOTAL_STEPS,
ui::RESET
);
ui::flower_separator();
if !skip_deps {
let step = 1;
ui::progress_bar(0, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "🌱", "Garden Bed Preparation");
ui::hint("First, let's make sure your soil is ready...");
println!();
step_dependency_check()?;
}
let step = 2;
ui::progress_bar(1, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "📛", "Name Your Garden");
ui::hint("Give your garden a name — like \"my-garden\" or \"team-chat\".");
println!();
let name: String = Text::new(" Garden name:")
.with_placeholder("my-garden")
.with_help_message("lowercase, hyphens allowed")
.prompt()?;
let name = name.trim().to_string();
if name.is_empty() {
anyhow::bail!("Garden name is required");
}
let step = 3;
ui::progress_bar(2, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "🤖", "Plant Your Seeds");
ui::hint("Each agent needs a Telegram bot. Add as many as you like.");
println!();
let bots = step_telegram_bots()?;
let step = 4;
ui::progress_bar(3, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "💬", "Telegram Group Link");
ui::hint("Where will your agents live? This is the group they'll chat in.");
println!();
let group_id = step_telegram_group_id()?;
let step = 5;
ui::progress_bar(4, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "☀️", "Choose Your Sunlight");
ui::hint("AI providers power your agents. Pick one or more to fuel them.");
println!();
let providers = step_providers()?;
let step = 6;
ui::progress_bar(5, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "📋", "Garden Blueprint");
ui::hint("Review everything before we plant.");
println!();
step_create_garden(&name, &group_id, &bots, &providers)?;
let step = 7;
ui::progress_bar(6, TOTAL_STEPS);
ui::section_header(step, TOTAL_STEPS, "🚀", "Let It Bloom");
println!();
step_start_garden(&name)?;
ui::progress_bar(TOTAL_STEPS, TOTAL_STEPS);
ui::celebration(&name);
ui::next_steps(&name);
ui::flower_separator();
Ok(())
}
fn step_dependency_check() -> Result<()> {
ui::spinner("Inspecting garden bed...", 800);
let check = deps::DependencyCheck::check_all();
check.print_report();
if !check.is_ready() {
println!();
ui::error("Your garden bed needs some work. Fix the issues above and re-run.");
anyhow::bail!("Missing dependencies");
}
println!();
let cont = Confirm::new(" Ready to proceed?")
.with_default(true)
.prompt()?;
if !cont {
anyhow::bail!("Onboarding cancelled.");
}
Ok(())
}
fn step_telegram_bots() -> Result<Vec<BotConfig>> {
let roles = [
("PM", "📋", "Coordinates tasks & keeps the team on track"),
("DEV", "💻", "Writes and reviews code, implements features"),
(
"CRITIC",
"🔍",
"Reviews output, catches issues & blind spots",
),
(
"DESIGNER",
"🎨",
"UI/UX design, system architecture thinking",
),
(
"RESEARCHER",
"🔬",
"Investigates, documents, and gathers context",
),
("TESTER", "🧪", "Quality assurance, edge-case explorer"),
("OPS", "🔧", "Deployment, DevOps, infrastructure management"),
("ANALYST", "📊", "Data analysis, metrics, insights"),
("OTHER", "✨", "Custom role — define your own specialty"),
];
let role_names: Vec<&str> = roles.iter().map(|r| r.0).collect();
let mut bots = Vec::new();
let mut bot_number = 0;
loop {
bot_number += 1;
println!(" ──── Agent #{} ────\n", bot_number);
let name = Text::new(" Agent name (lowercase, e.g. alex):")
.with_validator(|input: &str| {
if input.is_empty() {
return Err("Please enter a name".into());
}
if input.contains(' ') {
return Err("No spaces allowed".into());
}
Ok(inquire::validator::Validation::Valid)
})
.with_help_message("This will be used as the bot identifier internally")
.prompt()?;
if bots.iter().any(|b: &BotConfig| b.name == name) {
println!();
ui::warn(&format!(
"'{}' is already planted. Choose a different name.",
name
));
println!();
bot_number -= 1;
continue;
}
println!(" {} Available roles:", "\x1b[1m");
for (role_name, icon, desc) in &roles {
println!(" {} {} {}", icon, ui::role_badge(role_name), desc,);
}
println!("{}", ui::RESET);
let role = Select::new(" Choose a role:", role_names.to_vec()).prompt()?;
let role_desc = roles
.iter()
.find(|r| r.0 == role)
.map(|r| r.2)
.unwrap_or("");
ui::hint(role_desc);
println!();
let token = Password::new(" Telegram bot token:")
.without_confirmation()
.with_help_message("Get this from @BotFather on Telegram")
.prompt()?;
let bot = BotConfig {
name: name.clone(),
role: role.to_string(),
token,
};
println!(
" {} {} {} as {}",
ui::GREEN,
&bot.name,
ui::role_badge(&bot.role),
if bot.token.len() > 8 {
&bot.token[..8]
} else {
&bot.token
}
.to_string()
+ "...",
);
println!("{}", ui::RESET);
let confirm = Confirm::new(" Add this agent?")
.with_default(true)
.prompt()?;
if confirm {
bots.push(bot);
ui::success("Agent planted!");
} else {
ui::warn("Skipped.");
bot_number -= 1;
}
println!();
if bots.is_empty() {
let add_more = Confirm::new(" No agents yet. Add one?")
.with_default(true)
.prompt()?;
if !add_more {
break;
}
} else {
println!(" {} Your garden so far:", ui::DIM);
for (i, b) in bots.iter().enumerate() {
println!(
" {}{}. {} {}",
ui::DIM,
i + 1,
b.name,
ui::role_badge(&b.role),
);
}
println!("{}", ui::RESET);
let add_more = Confirm::new("\n Plant another agent?")
.with_default(false)
.prompt()?;
if !add_more {
break;
}
}
}
if bots.is_empty() {
println!();
ui::error("A garden needs at least one agent to tend it.");
anyhow::bail!("At least one bot is required");
}
println!();
ui::divider();
println!(
" {} Garden Roster ({} agents){}",
"\x1b[1m",
bots.len(),
ui::RESET
);
println!();
for (i, bot) in bots.iter().enumerate() {
println!(
" {} {} {}",
format!("{:>2}.", i + 1),
bot.name,
ui::role_badge(&bot.role),
);
}
ui::divider();
Ok(bots)
}
fn step_telegram_group_id() -> Result<String> {
println!(" Which Telegram group will your agents live in?");
println!();
ui::tip("Add your bots to a Telegram group first, then get the group ID.");
println!();
let group_id = Text::new(" Telegram group ID:")
.with_default("-1001234567890")
.with_validator(|input: &str| {
if !input.starts_with('-') {
return Err("Group IDs start with '-' (e.g. -1001234567890)".into());
}
if input.len() < 10 {
return Err("That looks too short for a group ID".into());
}
Ok(inquire::validator::Validation::Valid)
})
.with_help_message("You can find this by adding @RawDataBot to your group")
.prompt()?;
println!();
ui::success(&format!("Group {} linked.", group_id));
Ok(group_id)
}
fn step_providers() -> Result<Vec<SelectedProvider>> {
let providers = ProviderRegistry::providers();
println!(" Which AI providers will power your agents?");
println!();
println!(" {} Available providers:", "\x1b[1m");
for (i, p) in providers.iter().enumerate() {
println!(
" {} {} {}{}",
format!("{:>2}.", i + 1),
p.icon,
p.label,
match &p.default_model {
Some(m) => format!(" {}→ {}{}", ui::DIM, m, ui::RESET),
None => String::new(),
},
);
}
println!("{}", ui::RESET);
println!();
let provider_options: Vec<String> = providers
.iter()
.map(|p| format!("{} {}", p.icon, p.label))
.collect();
let selections = inquire::MultiSelect::new(
" Select providers (space to toggle, enter to confirm):",
provider_options,
)
.prompt()?;
if selections.is_empty() {
anyhow::bail!("At least one provider must be selected");
}
let mut selected_providers = Vec::new();
for provider_label in &selections {
let provider = providers
.iter()
.find(|p| format!("{} {}", p.icon, p.label) == *provider_label)
.expect("Provider not found");
ui::flower_separator();
println!(" {} {} setup:", provider.icon, provider.label);
println!();
let auth_method = if provider.auth.len() > 1 {
select_auth_method(provider)?
} else {
provider
.auth
.first()
.expect("No auth method defined")
.clone()
};
let api_key = Password::new(&format!(" Enter {} API key:", auth_method.label))
.without_confirmation()
.with_help_message("This will be stored in .env and pi-auth.json")
.prompt()?;
selected_providers.push((Arc::new(provider.clone()), auth_method.id.clone(), api_key));
println!();
ui::success(&format!(
"{} ({}) configured",
provider.label, auth_method.label
));
}
println!();
ui::divider();
println!(
" {} Sunlight Sources ({} providers){}",
"\x1b[1m",
selected_providers.len(),
ui::RESET
);
println!();
for (i, (p, method_id, _)) in selected_providers.iter().enumerate() {
println!(
" {} {} {} via {}",
format!("{:>2}.", i + 1),
p.icon,
p.label,
method_id,
);
}
ui::divider();
Ok(selected_providers)
}
fn select_auth_method(provider: &ProviderPlugin) -> Result<ProviderAuthMethod> {
if provider.auth.len() == 1 {
return Ok(provider.auth.first().unwrap().clone());
}
let method_options: Vec<String> = provider
.auth
.iter()
.map(|m| match &m.hint {
Some(h) => format!("{} ({})", m.label, h),
None => m.label.clone(),
})
.collect();
let selection = Select::new(" Select authentication method:", method_options).prompt()?;
provider
.auth
.iter()
.find(|m| match &m.hint {
Some(h) => format!("{} ({})", m.label, h) == selection,
None => m.label == selection,
})
.cloned()
.ok_or_else(|| anyhow::anyhow!("Auth method not found"))
}
fn step_create_garden(
name: &str,
group_id: &str,
bots: &[BotConfig],
providers: &[SelectedProvider],
) -> Result<()> {
let mut rows = vec![
("🏡".to_string(), "Garden".to_string(), name.to_string()),
("💬".to_string(), "Group".to_string(), group_id.to_string()),
];
rows.push((
"🤖".to_string(),
"Agents".to_string(),
format!("{} bots", bots.len()),
));
for (i, bot) in bots.iter().enumerate() {
rows.push((
" ".to_string(),
format!(" #{}", i + 1),
format!("{} {}", bot.name, ui::role_badge(&bot.role)),
));
}
rows.push((
"🔌".to_string(),
"Providers".to_string(),
format!("{} providers", providers.len()),
));
for (i, (p, method_id, _)) in providers.iter().enumerate() {
rows.push((
" ".to_string(),
format!(" #{}", i + 1),
format!("{} ({})", p.icon, method_id),
));
}
ui::summary_box(&format!("🌿 {} — Garden Blueprint", name), &rows);
println!();
let confirm = Confirm::new(" Create this garden?")
.with_default(true)
.prompt()?;
if !confirm {
anyhow::bail!("Onboarding cancelled.");
}
println!();
ui::spinner("Planting seeds...", 600);
let config = GardenConfig {
name: name.to_string(),
telegram_group_id: group_id.to_string(),
bots: bots.to_vec(),
providers: providers.to_vec(),
};
let (compose_path, env_path) = config.write()?;
ui::spinner("Watering the garden...", 400);
println!();
println!(" {} Files created:", "\x1b[1m");
println!(
" 📄 {}",
compose_path.file_name().unwrap().to_string_lossy()
);
println!(" 🔐 {}", env_path.file_name().unwrap().to_string_lossy());
println!("{}", ui::RESET);
let mut registry = load_gardens()?;
registry.add(name.to_string(), registry.garden_dir(name))?;
println!();
ui::success(&format!("Garden '{}' registered.", name));
Ok(())
}
fn step_start_garden(name: &str) -> Result<()> {
println!(" Your garden is ready. Want to bring it to life?");
println!();
let start = Confirm::new(" Start the garden now?")
.with_default(true)
.prompt()?;
if start {
println!();
ui::spinner("Warming up the greenhouse...", 500);
crate::compose::start_garden(name)?;
} else {
println!();
ui::hint("You can start it later with:");
println!(" {}garden up --name {}{}", ui::GREEN, name, ui::RESET);
}
Ok(())
}