feat: phase 2 — automatisation: doctor --watch, schedule, service, orchestration, conteneurs (closes #55 #56 #57 #58 #59)

This commit is contained in:
2026-08-18 10:57:00 -04:00
parent 6a6676fc5d
commit 63dc36660b
27 changed files with 1408 additions and 35 deletions
+71 -1
View File
@@ -84,8 +84,15 @@ pub fn start_with_cwd(app: &App, opts: &StartArgs, cwd: Option<&std::path::Path>
if members.is_empty() {
bail!("unknown group '{group}'");
}
// Issue #57: --parallel launches every member back-to-back without
// waiting (the OS spawns them concurrently); the default waits each
// health check before the next member.
for m in members {
start_one(app, m, &extra_args, &extra_env, opts.notify, true, cwd)?;
if !opts.parallel {
// Issue #57: block until healthy before the next member.
crate::automation::wait_health(&m.name, m.healthcheck.as_ref(), &app.log)?;
}
}
return Ok(0);
}
@@ -309,8 +316,18 @@ pub fn restart(app: &App, opts: &StartArgs, force: bool, timeout: Option<u64>) -
}
/// run: execute the agent command directly, no process management.
pub fn run(app: &App, target: &str, model: Option<&str>, extra: &[OsString]) -> Result<i32> {
pub fn run(
app: &App,
target: &str,
model: Option<&str>,
container: bool,
extra: &[OsString],
) -> Result<i32> {
let agent = require_agent(app, target)?;
// Issue #58: --container runs the agent inside its container profile.
if container {
return run_container(app, agent, extra);
}
let mut extra_args: Vec<String> = extra
.iter()
.map(|o| o.to_string_lossy().to_string())
@@ -353,6 +370,59 @@ pub fn run(app: &App, target: &str, model: Option<&str>, extra: &[OsString]) ->
Ok(code)
}
/// Issue #58: run the agent inside its container profile. Detects the
/// runtime (podman > docker), mounts state/log dirs and the profile
/// volumes/network/env, then execs the container in the foreground.
fn run_container(app: &App, agent: &crate::config::AgentDef, extra: &[OsString]) -> Result<i32> {
let Some(spec) = &agent.container else {
bail!(
"agent '{}' has no container profile — add 'container: {{image: ...}}' to its definition",
agent.name
);
};
let Some(runtime) = crate::automation::detect_runtime() else {
bail!(
"no container runtime found (docker or podman) — install one or drop --container"
);
};
let state_dir = app.paths.state_file.parent().unwrap_or(std::path::Path::new("."));
let log_dir = &app.paths.log_dir;
let mut cmd = crate::automation::container_command(
&runtime,
&agent.name,
spec,
state_dir,
log_dir,
)?;
// The agent's run command + extra args become the container command.
let exec = resolve_exec(app, agent, &[], &BTreeMap::new())?;
cmd.push(exec.program);
cmd.extend(exec.args.iter().cloned());
cmd.extend(extra.iter().map(|o| o.to_string_lossy().to_string()));
if app.dry_run() {
app.log.dry(&format!("would run: {}", cmd.join(" ")));
return Ok(0);
}
app.log.info(&format!(
"running '{}' in a container ({runtime})",
agent.name
));
let status = Command::new(&cmd[0])
.args(&cmd[1..])
.status()
.with_context(|| format!("failed to run {}", cmd.join(" ")))?;
let code = status.code().unwrap_or(1);
app.emit(
&Event::now(EventKind::Run)
.with_agent(agent.name.clone())
.with_reason(format!("container ({runtime})"))
.with_args(cmd.clone())
.with_exit_code(code),
);
let _ = app.state.touch(&agent.name);
Ok(code)
}
/// Current directory as a string (None when unavailable).
fn cwd_string() -> Option<String> {
std::env::current_dir().ok().map(|p| p.display().to_string())