vvbox 0.1.0

Lightweight sandbox runner for macOS 26 using Apple container CLI.
Documentation
//! CLI types for parsing vvbox commands.

use clap::{Parser, Subcommand};

/// Top-level CLI entry point.
#[derive(Parser)]
#[command(name = "vvbox")]
#[command(about = "Sandboxed runner for macOS 26 using Apple container CLI", long_about = None)]
pub struct Cli {
    /// Parsed subcommand and its arguments.
    #[command(subcommand)]
    pub command: Commands,
}

/// Supported vvbox subcommands.
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum Commands {
    /// Snapshot repo and run command in container
    Run {
        /// Repo path (required)
        #[arg(long)]
        repo: String,

        /// Use an existing worktree path directly (no snapshot)
        #[arg(long)]
        worktree: Option<String>,

        /// Run in-place in the provided worktree (no patch apply workflow)
        #[arg(long = "in-place")]
        in_place: bool,

        /// Config file path (vvbox.yaml)
        #[arg(long)]
        config: Option<String>,

        /// Container image
        #[arg(long)]
        image: Option<String>,

        /// Container network
        #[arg(long)]
        network: Option<String>,

        /// Disable networking
        #[arg(long = "no-network")]
        no_network: bool,

        /// Container workdir
        #[arg(long)]
        workdir: Option<String>,

        /// Optional task label
        #[arg(long)]
        task: Option<String>,

        /// Environment variables (repeatable, key=value)
        #[arg(long = "env")]
        env: Vec<String>,

        /// Env file (key=value)
        #[arg(long = "env-file")]
        env_file: Option<String>,

        /// Publish ports (repeatable, host:container[/proto])
        #[arg(long = "port")]
        port: Vec<String>,

        /// Bind mount volumes (repeatable, source:target\[:ro\])
        #[arg(long = "volume")]
        volume: Vec<String>,

        /// Mount repo read-only
        #[arg(long = "read-only")]
        read_only: bool,

        /// Generate patch after run
        #[arg(long)]
        diff: bool,

        /// Cleanup snapshot + metadata after run
        #[arg(long)]
        cleanup: bool,

        /// Cleanup only if run succeeds
        #[arg(long = "cleanup-on-success")]
        cleanup_on_success: bool,

        /// Run an interactive shell in the container
        #[arg(long)]
        shell: bool,

        /// Keep the main container after exit (no --rm)
        #[arg(long)]
        keep: bool,

        /// JSON output
        #[arg(long)]
        json: bool,

        /// Command string (runs via sh -lc)
        #[arg(long)]
        cmd: Option<String>,

        /// Command args after `--`
        #[arg(trailing_var_arg = true)]
        args: Vec<String>,
    },

    /// Generate patch from snapshot
    Diff {
        /// Run id
        #[arg(long)]
        id: Option<String>,

        /// Use last run
        #[arg(long)]
        last: bool,

        /// JSON output
        #[arg(long)]
        json: bool,
    },

    /// Apply patch to original repo
    Apply {
        /// Run id
        #[arg(long)]
        id: Option<String>,

        /// Use last run
        #[arg(long)]
        last: bool,

        /// JSON output
        #[arg(long)]
        json: bool,

        /// Apply even if working tree is dirty
        #[arg(long = "allow-dirty")]
        allow_dirty: bool,

        /// Auto-confirm apply
        #[arg(long)]
        yes: bool,
    },

    /// Attach to a running container from a run
    Attach {
        /// Run id
        #[arg(long)]
        id: Option<String>,

        /// Use last run
        #[arg(long)]
        last: bool,

        /// Run an interactive shell
        #[arg(long)]
        shell: bool,

        /// Command string (runs via sh -lc)
        #[arg(long)]
        cmd: Option<String>,

        /// Command args after `--`
        #[arg(trailing_var_arg = true)]
        args: Vec<String>,
    },

    /// Stream logs for a run container
    Logs {
        /// Run id
        #[arg(long)]
        id: Option<String>,

        /// Use last run
        #[arg(long)]
        last: bool,

        /// Tail N lines
        #[arg(long)]
        tail: Option<u32>,

        /// Show logs since duration (e.g. 10s, 5m, 2h)
        #[arg(long)]
        since: Option<String>,

        /// Do not follow logs (single shot)
        #[arg(long = "no-follow")]
        no_follow: bool,
    },

    /// Manage service containers from config
    Services {
        /// Service subcommand to execute.
        #[command(subcommand)]
        action: ServiceCommands,
    },

    /// Start services (alias for `services up`)
    Up {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,

        /// Stop existing vvbox service containers for this repo first
        #[arg(long = "restart")]
        restart: bool,
    },

    /// Stop services (alias for `services down`)
    Down {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,
    },

    /// List runs
    List {
        /// JSON output
        #[arg(long)]
        json: bool,
    },

    /// Remove snapshot + metadata
    Cleanup {
        /// Run id
        #[arg(long)]
        id: Option<String>,

        /// Use last run
        #[arg(long)]
        last: bool,

        /// JSON output
        #[arg(long)]
        json: bool,
    },

    /// Initialize vvbox.yaml in the repo
    Init {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Output path (defaults to vvbox.yaml in repo root)
        #[arg(long)]
        out: Option<String>,

        /// Overwrite if file exists
        #[arg(long)]
        force: bool,
    },
}

/// Service management subcommands.
#[derive(Subcommand)]
pub enum ServiceCommands {
    /// Start services from config
    Up {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,

        /// Stop existing vvbox service containers for this repo first
        #[arg(long = "restart")]
        restart: bool,
    },

    /// Stop services from config
    Down {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,
    },

    /// Show service status
    Status {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,

        /// JSON output
        #[arg(long)]
        json: bool,
    },

    /// Restart services
    Restart {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,
    },

    /// Stream logs for a service
    Logs {
        /// Repo path (defaults to current directory)
        #[arg(long)]
        repo: Option<String>,

        /// Config file path (yaml/json)
        #[arg(long)]
        config: Option<String>,

        /// Service name (from config)
        #[arg(long)]
        name: String,

        /// Tail N lines
        #[arg(long)]
        tail: Option<u32>,

        /// Show logs since duration (e.g. 10s, 5m, 2h)
        #[arg(long)]
        since: Option<String>,

        /// Do not follow logs (single shot)
        #[arg(long = "no-follow")]
        no_follow: bool,
    },
}