use crate::config::runs_dir;
use crate::container::stop_container;
use crate::types::RunMeta;
use crate::util::{die, run_cmd};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
fn read_json<T: for<'de> serde::Deserialize<'de>>(path: &Path, fallback: T) -> T {
if !path.exists() {
return fallback;
}
let mut buf = String::new();
if File::open(path).and_then(|mut f| f.read_to_string(&mut buf)).is_ok() {
if let Ok(val) = serde_json::from_str::<T>(&buf) {
return val;
}
}
fallback
}
fn write_json<T: serde::Serialize>(path: &Path, value: &T) {
if let Ok(json) = serde_json::to_string_pretty(value) {
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(path, json);
}
}
pub fn save_meta(meta: &RunMeta) {
let meta_path = runs_dir().join(&meta.id).join("meta.json");
write_json(&meta_path, meta);
}
pub fn load_meta(run_id: &str) -> RunMeta {
let meta_path = runs_dir().join(run_id).join("meta.json");
if !meta_path.exists() {
die(&format!("run not found: {}", run_id));
}
read_json(&meta_path, RunMeta {
id: run_id.to_string(),
created_at: "".to_string(),
repo_root: "".to_string(),
snapshot_path: "".to_string(),
snapshot_mode: "worktree".to_string(),
container_name: "".to_string(),
image: "".to_string(),
workdir: "".to_string(),
network: None,
env: HashMap::new(),
volumes: vec![],
ports: vec![],
command: vec![],
pre_install: vec![],
run: vec![],
services: vec![],
in_place: false,
task: None,
exit_code: None,
status: "created".to_string(),
})
}
pub fn list_runs() -> Vec<RunMeta> {
let mut runs = vec![];
let dir = runs_dir();
if !dir.exists() {
return runs;
}
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let meta_path = entry.path().join("meta.json");
if meta_path.exists() {
let meta = read_json(&meta_path, RunMeta {
id: "".to_string(),
created_at: "".to_string(),
repo_root: "".to_string(),
snapshot_path: "".to_string(),
snapshot_mode: "worktree".to_string(),
container_name: "".to_string(),
image: "".to_string(),
workdir: "".to_string(),
network: None,
env: HashMap::new(),
volumes: vec![],
ports: vec![],
command: vec![],
pre_install: vec![],
run: vec![],
services: vec![],
in_place: false,
task: None,
exit_code: None,
status: "created".to_string(),
});
runs.push(meta);
}
}
}
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
runs
}
pub fn last_run_id() -> String {
let runs = list_runs();
if runs.is_empty() {
die("no runs found");
}
runs[0].id.clone()
}
pub fn ensure_patch(meta: &RunMeta) -> PathBuf {
let run_dir = runs_dir().join(&meta.id);
let patch_path = run_dir.join("patch.diff");
let output = Command::new("git")
.arg("-C")
.arg(&meta.snapshot_path)
.arg("diff")
.arg("--binary")
.output();
match output {
Ok(out) if out.status.success() => {
let _ = fs::create_dir_all(&run_dir);
let mut file = File::create(&patch_path).unwrap_or_else(|_| die("failed to write patch"));
let _ = file.write_all(&out.stdout);
patch_path
}
_ => die("failed to generate diff"),
}
}
pub fn cleanup_run(meta: &RunMeta) {
if !meta.container_name.is_empty() {
stop_container(&meta.container_name);
}
for svc in &meta.services {
if !svc.is_empty() {
stop_container(svc);
}
}
if !meta.in_place {
let cmd = vec![
"git".to_string(),
"-C".to_string(),
meta.repo_root.clone(),
"worktree".to_string(),
"remove".to_string(),
"--force".to_string(),
meta.snapshot_path.clone(),
];
let _ = run_cmd(&cmd, None, true);
}
let _ = fs::remove_dir_all(runs_dir().join(&meta.id));
}