use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::Result;
use team_core::compose::Compose;
use team_core::supervisor::{AgentSpec, AgentState, DrainOutcome, Supervisor, TmuxSupervisor};
use super::snapshot::{self, AgentEntry, ReloadPlan, RemovedAgent};
pub fn run(root: &Path, dry_run: bool) -> Result<()> {
let compose = super::load(root)?;
let errs = team_core::validate::validate(&compose);
if !errs.is_empty() {
for e in &errs {
eprintln!("error: {e}");
}
anyhow::bail!("{} validation error(s) — fix before reload", errs.len());
}
let prev = snapshot::read(&compose.root);
let bin = super::team_mcp_bin().display().to_string();
let next = snapshot::compute(&compose, &bin);
let plan = snapshot::plan(prev.as_ref(), &next);
let no_changes = plan.is_empty()
&& prev
.as_ref()
.map(|s| s.compose_digest == next.compose_digest && s.global == next.global)
.unwrap_or(false);
if no_changes {
if dry_run {
println!("no changes (dry run)");
} else {
println!("no changes");
}
return Ok(());
}
if dry_run {
print_plan(&plan, true);
return Ok(());
}
super::up::ensure_wrapper_and_dirs(&compose)?;
super::up::render_all_public(&compose)?;
super::up::register_all_public(&compose)?;
apply_plan(&compose, &plan)?;
snapshot::write(&compose.root, &next)?;
Ok(())
}
fn print_plan(plan: &ReloadPlan, dry: bool) {
let suffix = if dry { " (dry run)" } else { "" };
for r in &plan.remove {
println!("removed · {}{suffix}", r.id);
}
for (id, inputs) in &plan.change {
println!("changed · {id} ({}){suffix}", inputs.label());
}
for id in &plan.add {
println!("added · {id}{suffix}");
}
}
fn apply_plan(compose: &Compose, plan: &ReloadPlan) -> Result<()> {
let sup = TmuxSupervisor;
let drain_timeout = Duration::from_secs(compose.global.supervisor.drain_timeout_secs);
for r in &plan.remove {
let outcome = sup.drain(&spec_from_removed(compose, r), drain_timeout)?;
println!("removed · {}{}", r.id, drain_suffix(outcome));
}
for (id, inputs) in &plan.change {
let prior = plan
.change_prior
.get(id)
.expect("change_prior populated by plan()");
let outcome = sup.drain(&spec_from_prior(compose, id, prior), drain_timeout)?;
if let Some(h) = compose.agents().find(|h| &h.id() == id) {
let spec =
AgentSpec::from_handle(h, &compose.root, &compose.global.supervisor.tmux_prefix);
sup.up(&spec)?;
}
println!(
"changed · {id} ({}){}",
inputs.label(),
drain_suffix(outcome)
);
}
for id in &plan.add {
if let Some(h) = compose.agents().find(|h| &h.id() == id) {
let spec =
AgentSpec::from_handle(h, &compose.root, &compose.global.supervisor.tmux_prefix);
sup.up(&spec)?;
println!("added · {id}");
}
}
for id in &plan.keep {
if let Some(h) = compose.agents().find(|h| &h.id() == id) {
let spec =
AgentSpec::from_handle(h, &compose.root, &compose.global.supervisor.tmux_prefix);
if sup.state(&spec)? == AgentState::Stopped {
sup.up(&spec)?;
println!("started · {id}");
}
}
}
Ok(())
}
fn drain_suffix(outcome: DrainOutcome) -> &'static str {
match outcome {
DrainOutcome::Graceful => "",
DrainOutcome::TimedOutKilled => " [drain timed out — killed]",
}
}
fn spec_from_removed(compose: &Compose, r: &RemovedAgent) -> AgentSpec {
let (project, agent) = r.id.split_once(':').unwrap_or((r.id.as_str(), ""));
AgentSpec {
project: project.into(),
agent: agent.into(),
tmux_session: r.tmux_session.clone(),
wrapper: super::agent_wrapper(&compose.root),
cwd: compose.root.clone(),
env_file: r.env_file.clone(),
}
}
fn spec_from_prior(compose: &Compose, id: &str, prior: &AgentEntry) -> AgentSpec {
let (project, agent) = id.split_once(':').unwrap_or((id, ""));
AgentSpec {
project: project.into(),
agent: agent.into(),
tmux_session: prior.tmux_session.clone(),
wrapper: super::agent_wrapper(&compose.root),
cwd: compose.root.clone(),
env_file: PathBuf::from(&prior.env_file),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drain_suffix_empty_on_graceful() {
assert_eq!(drain_suffix(DrainOutcome::Graceful), "");
}
#[test]
fn drain_suffix_annotates_timeout() {
assert!(drain_suffix(DrainOutcome::TimedOutKilled).contains("drain timed out"));
}
}