use std::io::{self, Write};
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result};
use klasp_core::{ConfigV1, InstallContext, GATE_SCHEMA_VERSION};
use crate::adopt::detect_agents::detect_installed_agents;
use crate::cli::SetupArgs;
use crate::cmd::doctor::{check_paths, check_surfaces, Counters};
use crate::cmd::install::{install_one_surface, print_hook_warning};
use crate::registry::SurfaceRegistry;
pub fn run(args: &SetupArgs) -> ExitCode {
match try_run(args) {
Ok(exit) => exit,
Err(e) => {
eprintln!("klasp setup: {e:#}");
ExitCode::FAILURE
}
}
}
fn try_run(args: &SetupArgs) -> Result<ExitCode> {
let repo_root = crate::cmd::install::resolve_repo_root(None).context("resolving repo root")?;
let plan = crate::adopt::detect::detect_all(&repo_root).context("detecting existing gates")?;
println!("klasp setup — detected {} gate(s)", plan.findings.len());
if args.dry_run {
println!("(--dry-run: printing plan only, writing nothing)");
}
let home = crate::fs_util::home_dir();
let (detected_agents, fell_back) = detect_installed_agents(home.as_deref());
println!(
"detected agents: {}",
if fell_back {
format!("(none — falling back to {})", detected_agents.join(", "))
} else {
detected_agents.join(", ")
}
);
print!("{}", crate::adopt::render::render_plan(&plan));
let gates_to_use = if args.interactive {
let confirmed = prompt_yes_no(&format!(
"Mirror {} detected gate(s) into klasp.toml?",
plan.findings.len()
))?;
if !confirmed {
println!("Skipping gate mirroring — klasp.toml will have no checks.");
crate::adopt::plan::AdoptionPlan::default()
} else {
plan
}
} else {
plan
};
if args.dry_run {
println!("\n--- dry-run plan ---");
println!("[gate].agents would be: {}", detected_agents.join(", "));
println!(
"checks: {}",
gates_to_use
.findings
.iter()
.flat_map(|f| f.proposed_checks.iter().map(|c| c.name.as_str()))
.collect::<Vec<_>>()
.join(", ")
.if_empty("(none)")
);
println!("No files written (--dry-run).");
return Ok(ExitCode::SUCCESS);
}
if args.interactive {
let confirmed = prompt_yes_no("Write klasp.toml now?")?;
if !confirmed {
println!("Aborted — klasp.toml not written.");
return Ok(ExitCode::SUCCESS);
}
}
let agents_arg = if fell_back {
None
} else {
Some(detected_agents.as_slice())
};
let toml_path = crate::adopt::writer::write_klasp_toml(
&repo_root,
&gates_to_use,
true, agents_arg,
)
.context("writing klasp.toml")?;
println!("wrote {}", toml_path.display());
if args.interactive {
let confirmed = prompt_yes_no("Install agent surfaces now?")?;
if !confirmed {
println!("Skipping install. Run `klasp install --agent all` when ready.");
return Ok(ExitCode::SUCCESS);
}
}
let config = ConfigV1::load(&repo_root).context("loading config after write")?;
let install_exit = run_install_all(&repo_root, &detected_agents)?;
if install_exit != ExitCode::SUCCESS {
eprintln!("klasp setup: install step failed — see above");
return Ok(install_exit);
}
println!("\n--- klasp doctor ---");
let doctor_exit = run_doctor_with_config(&repo_root, config);
if doctor_exit == ExitCode::SUCCESS {
println!("\nsetup complete — `klasp doctor` passed with no FAILs.");
} else {
println!("\nsetup finished with doctor failures — see above for details.");
}
Ok(doctor_exit)
}
fn run_install_all(repo_root: &Path, agents: &[String]) -> Result<ExitCode> {
let registry = SurfaceRegistry::default();
let ctx = InstallContext {
repo_root: repo_root.to_path_buf(),
dry_run: false,
force: false,
schema_version: GATE_SCHEMA_VERSION,
};
for agent_id in agents {
let surface = match registry.get(agent_id) {
Some(s) => s,
None => {
eprintln!("warning: unknown agent '{agent_id}' in detected list — skipping");
continue;
}
};
let (report, warnings) =
install_one_surface(surface, &ctx).with_context(|| format!("installing {agent_id}"))?;
for warning in &warnings {
print_hook_warning(warning);
}
println!("{}: {}", agent_id, install_result_label(&report));
}
Ok(ExitCode::SUCCESS)
}
fn install_result_label(report: &klasp_core::InstallReport) -> &'static str {
if report.already_installed {
"already installed (no changes)"
} else {
"installed"
}
}
fn run_doctor_with_config(repo_root: &Path, config: ConfigV1) -> ExitCode {
let mut c = Counters::new();
c.ok("config: klasp.toml loaded OK");
check_surfaces(repo_root, Some(&config), &mut c);
check_paths(&config, &mut c);
if c.fails > 0 || c.warns > 0 {
eprintln!("doctor: {} FAIL, {} WARN", c.fails, c.warns);
} else {
eprintln!("doctor: all checks passed");
}
if c.fails > 0 {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn prompt_yes_no(question: &str) -> io::Result<bool> {
let stdin = io::stdin();
let mut stdout = io::stdout();
loop {
print!("{question} [y/N] ");
stdout.flush()?;
let mut line = String::new();
stdin.read_line(&mut line)?;
match line.trim().to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" | "" => return Ok(false),
_ => println!("Please enter y or n."),
}
}
}
trait IfEmpty {
fn if_empty(self, fallback: &str) -> String;
}
impl IfEmpty for String {
fn if_empty(self, fallback: &str) -> String {
if self.is_empty() {
fallback.to_string()
} else {
self
}
}
}