vvbox 0.1.0

Lightweight sandbox runner for macOS 26 using Apple container CLI.
Documentation
//! Run metadata persistence and patch generation.

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);
    }
}

/// Persist run metadata to `runs/<id>/meta.json`.
pub fn save_meta(meta: &RunMeta) {
    let meta_path = runs_dir().join(&meta.id).join("meta.json");
    write_json(&meta_path, meta);
}

/// Load run metadata from `runs/<id>/meta.json`.
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(),
    })
}

/// List runs sorted by most recent first.
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
}

/// Return the latest run id or exit if none exist.
pub fn last_run_id() -> String {
    let runs = list_runs();
    if runs.is_empty() {
        die("no runs found");
    }
    runs[0].id.clone()
}

/// Generate a patch from the snapshot and return its path.
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"),
    }
}

/// Stop containers and remove snapshot/worktree state for a run.
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));
}