vvbox 0.1.0

Lightweight sandbox runner for macOS 26 using Apple container CLI.
Documentation
//! Helpers for interacting with the Apple container CLI.

use crate::config::resolve_volume_spec;
use crate::types::{RunMeta, ServiceConfig};
use crate::util::{die, run_cmd};
use std::process::Command;

/// Ensure the Apple container CLI is available and the system service is running.
pub fn ensure_container_available() {
    let status = run_cmd(&["container".to_string(), "--version".to_string()], None, false);
    if !status.success() {
        die("Apple container CLI not found. Install from https://github.com/apple/container");
    }

    let system = run_cmd(&["container".to_string(), "system".to_string(), "start".to_string()], None, true);
    if !system.success() {
        die("failed to start container system service");
    }
}

/// Build `container run` arguments for the main vvbox run container.
pub fn build_container_args(meta: &RunMeta, env_args: &[String], env_file: &Option<String>, read_only: bool) -> Vec<String> {
    let mut args: Vec<String> = vec![
        "container".to_string(),
        "run".to_string(),
        "--name".to_string(),
        meta.container_name.clone(),
        "--mount".to_string(),
        format!(
            "type=bind,source={},target={}{}",
            meta.snapshot_path,
            meta.workdir,
            if read_only { ",readonly" } else { "" }
        ),
        "-w".to_string(),
        meta.workdir.clone(),
    ];

    if let Some(network) = &meta.network {
        args.push("--network".to_string());
        args.push(network.clone());
    }

    if let Some(env_file) = env_file {
        args.push("--env-file".to_string());
        args.push(env_file.clone());
    }

    for (k, v) in &meta.env {
        args.push("-e".to_string());
        args.push(format!("{}={}", k, v));
    }

    for env in env_args {
        args.push("-e".to_string());
        args.push(env.clone());
    }

    for vol in &meta.volumes {
        args.push("--mount".to_string());
        args.push(format!(
            "type=bind,source={},target={}{}",
            vol.source,
            vol.target,
            if vol.readonly { ",readonly" } else { "" }
        ));
    }

    for p in &meta.ports {
        args.push("--publish".to_string());
        args.push(p.clone());
    }

    args.push(meta.image.clone());
    for c in &meta.command {
        args.push(c.clone());
    }

    args
}

/// Build `container run` arguments for a service container tied to a run id.
pub fn build_service_args(run_id: &str, svc: &ServiceConfig, repo_root: &std::path::Path, network: &Option<String>) -> (String, Vec<String>) {
    let name = format!("vvbox-svc-{}-{}", run_id, svc.name);
    let mut args: Vec<String> = vec![
        "container".to_string(),
        "run".to_string(),
        "--rm".to_string(),
        "-d".to_string(),
        "--name".to_string(),
        name.clone(),
    ];

    if let Some(net) = network {
        args.push("--network".to_string());
        args.push(net.clone());
    }

    if let Some(env) = &svc.env {
        for (k, v) in env {
            args.push("-e".to_string());
            args.push(format!("{}={}", k, v));
        }
    }

    if let Some(ports) = &svc.ports {
        for p in ports {
            args.push("--publish".to_string());
            args.push(p.clone());
        }
    }

    if let Some(vols) = &svc.volumes {
        for v in vols {
            let resolved = resolve_volume_spec(v, repo_root);
            args.push("--mount".to_string());
            args.push(format!(
                "type=bind,source={},target={}{}",
                resolved.source,
                resolved.target,
                if resolved.readonly { ",readonly" } else { "" }
            ));
        }
    }

    if let Some(workdir) = &svc.workdir {
        args.push("-w".to_string());
        args.push(workdir.clone());
    }

    args.push(svc.image.clone());
    if let Some(cmd) = &svc.command {
        for c in cmd.to_vec() {
            args.push(c);
        }
    }

    (name, args)
}

/// Build `container run` arguments for a service container using a custom prefix.
pub fn build_named_service_args(prefix: &str, svc: &ServiceConfig, repo_root: &std::path::Path, network: &Option<String>) -> (String, Vec<String>) {
    let name = format!("vvbox-svc-{}-{}", prefix, svc.name);
    let mut args: Vec<String> = vec![
        "container".to_string(),
        "run".to_string(),
        "--rm".to_string(),
        "-d".to_string(),
        "--name".to_string(),
        name.clone(),
    ];

    if let Some(net) = network {
        args.push("--network".to_string());
        args.push(net.clone());
    }

    if let Some(env) = &svc.env {
        for (k, v) in env {
            args.push("-e".to_string());
            args.push(format!("{}={}", k, v));
        }
    }

    if let Some(ports) = &svc.ports {
        for p in ports {
            args.push("--publish".to_string());
            args.push(p.clone());
        }
    }

    if let Some(vols) = &svc.volumes {
        for v in vols {
            let resolved = resolve_volume_spec(v, repo_root);
            args.push("--mount".to_string());
            args.push(format!(
                "type=bind,source={},target={}{}",
                resolved.source,
                resolved.target,
                if resolved.readonly { ",readonly" } else { "" }
            ));
        }
    }

    if let Some(workdir) = &svc.workdir {
        args.push("-w".to_string());
        args.push(workdir.clone());
    }

    args.push(svc.image.clone());
    if let Some(cmd) = &svc.command {
        for c in cmd.to_vec() {
            args.push(c);
        }
    }

    (name, args)
}

/// Stop a container by name (best-effort).
pub fn stop_container(name: &str) {
    let _ = run_cmd(&["container".to_string(), "stop".to_string(), name.to_string()], None, true);
}

/// Stream container logs, optionally tailing or filtering by time.
pub fn tail_container_logs(name: &str, tail: Option<u32>, since: Option<String>, follow: bool) -> bool {
    let mut args: Vec<String> = vec!["container".to_string(), "logs".to_string()];
    if follow {
        args.push("-f".to_string());
    }
    if let Some(t) = tail {
        args.push("--tail".to_string());
        args.push(t.to_string());
    }
    if let Some(s) = since {
        args.push("--since".to_string());
        args.push(s);
    }
    args.push(name.to_string());
    let status = run_cmd(&args, None, true);
    status.success()
}

/// List container identifiers and statuses via `container list --all`.
pub fn container_list_entries() -> Vec<(String, String)> {
    let output = Command::new("container")
        .arg("list")
        .arg("--all")
        .arg("--format")
        .arg("json")
        .output();
    if let Ok(out) = output {
        if out.status.success() {
            if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&out.stdout) {
                if let Some(arr) = value.as_array() {
                    return arr
                        .iter()
                        .filter_map(|item| {
                            let id = item
                                .get("configuration")
                                .and_then(|c| c.get("id"))
                                .and_then(|v| v.as_str())?;
                            let status = item.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
                            Some((id.to_string(), status.to_string()))
                        })
                        .collect();
                }
            }
        }
    }
    vec![]
}

/// List service container names for the provided prefix.
pub fn list_service_containers(prefix: &str) -> Vec<String> {
    let name_prefix = format!("vvbox-svc-{}-", prefix);
    container_list_entries()
        .into_iter()
        .filter(|(id, _)| id.starts_with(&name_prefix))
        .map(|(id, _)| id)
        .collect()
}

/// List service container names and statuses for the provided prefix.
pub fn list_service_status(prefix: &str) -> Vec<(String, String)> {
    let name_prefix = format!("vvbox-svc-{}-", prefix);
    container_list_entries()
        .into_iter()
        .filter(|(id, _)| id.starts_with(&name_prefix))
        .collect()
}