use anyhow::{Context, Result};
use arcbox_core::machine::{MachineConfig, MachineState};
use arcbox_core::{Config, Runtime};
use clap::{Args, Subcommand};
use std::sync::OnceLock;
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
fn get_runtime() -> &'static Runtime {
RUNTIME.get_or_init(|| {
let config = Config::load().unwrap_or_default();
Runtime::new(config).expect("failed to initialize runtime")
})
}
#[derive(Subcommand)]
pub enum MachineCommands {
Create(CreateArgs),
Start(StartArgs),
Stop(StopArgs),
#[command(alias = "rm")]
Remove(RemoveArgs),
#[command(name = "ls", alias = "list")]
List(ListArgs),
Status(StatusArgs),
Ssh(SshArgs),
Exec(ExecArgs),
}
#[derive(Args)]
pub struct CreateArgs {
pub name: String,
#[arg(long, default_value = "4")]
pub cpus: u32,
#[arg(long, default_value = "4096")]
pub memory: u64,
#[arg(long, default_value = "50")]
pub disk: u64,
#[arg(long, default_value = "ubuntu")]
pub distro: String,
#[arg(long, name = "distro-version")]
pub distro_version: Option<String>,
#[arg(short, long)]
pub mount: Vec<String>,
}
#[derive(Args)]
pub struct StartArgs {
pub name: String,
}
#[derive(Args)]
pub struct StopArgs {
pub name: String,
#[arg(short, long)]
pub force: bool,
}
#[derive(Args)]
pub struct RemoveArgs {
pub name: String,
#[arg(short, long)]
pub force: bool,
#[arg(short, long)]
pub volumes: bool,
}
#[derive(Args)]
pub struct ListArgs {
#[arg(short, long)]
pub all: bool,
#[arg(short, long)]
pub quiet: bool,
}
#[derive(Args)]
pub struct StatusArgs {
pub name: String,
}
#[derive(Args)]
pub struct SshArgs {
pub name: String,
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
#[derive(Args)]
pub struct ExecArgs {
pub name: String,
#[arg(trailing_var_arg = true, required = true)]
pub command: Vec<String>,
}
pub async fn execute(cmd: MachineCommands) -> Result<()> {
match cmd {
MachineCommands::Create(args) => execute_create(args).await,
MachineCommands::Start(args) => execute_start(args).await,
MachineCommands::Stop(args) => execute_stop(args).await,
MachineCommands::Remove(args) => execute_remove(args).await,
MachineCommands::List(args) => execute_list(args).await,
MachineCommands::Status(args) => execute_status(args).await,
MachineCommands::Ssh(args) => execute_ssh(args).await,
MachineCommands::Exec(args) => execute_exec(args).await,
}
}
async fn execute_create(args: CreateArgs) -> Result<()> {
let runtime = get_runtime();
let config = MachineConfig {
name: args.name.clone(),
cpus: args.cpus,
memory_mb: args.memory,
disk_gb: args.disk,
};
runtime
.machine_manager()
.create(config)
.context("Failed to create machine")?;
println!("Machine '{}' created successfully", args.name);
println!(" CPUs: {}", args.cpus);
println!(" Memory: {} MB", args.memory);
println!(" Disk: {} GB", args.disk);
println!();
println!("To start the machine, run:");
println!(" arcbox machine start {}", args.name);
Ok(())
}
async fn execute_start(args: StartArgs) -> Result<()> {
let runtime = get_runtime();
println!("Starting machine '{}'...", args.name);
runtime
.machine_manager()
.start(&args.name)
.context("Failed to start machine")?;
println!("Machine '{}' started", args.name);
Ok(())
}
async fn execute_stop(args: StopArgs) -> Result<()> {
let runtime = get_runtime();
println!("Stopping machine '{}'...", args.name);
runtime
.machine_manager()
.stop(&args.name)
.context("Failed to stop machine")?;
println!("Machine '{}' stopped", args.name);
Ok(())
}
async fn execute_remove(args: RemoveArgs) -> Result<()> {
let runtime = get_runtime();
runtime
.machine_manager()
.remove(&args.name, args.force)
.context("Failed to remove machine")?;
println!("Machine '{}' removed", args.name);
Ok(())
}
async fn execute_list(args: ListArgs) -> Result<()> {
let runtime = get_runtime();
let machines = runtime.machine_manager().list();
if args.quiet {
for machine in &machines {
println!("{}", machine.name);
}
return Ok(());
}
if machines.is_empty() {
println!("No machines found.");
println!();
println!("To create a machine, run:");
println!(" arcbox machine create <name>");
return Ok(());
}
println!(
"{:<20} {:<12} {:<6} {:<12} {:<10}",
"NAME", "STATE", "CPUS", "MEMORY", "DISK"
);
for machine in &machines {
let state_str = match machine.state {
MachineState::Created => "Created",
MachineState::Starting => "Starting",
MachineState::Running => "Running",
MachineState::Stopping => "Stopping",
MachineState::Stopped => "Stopped",
};
println!(
"{:<20} {:<12} {:<6} {:<12} {:<10}",
machine.name,
state_str,
machine.cpus,
format!("{} MB", machine.memory_mb),
format!("{} GB", machine.disk_gb),
);
}
Ok(())
}
async fn execute_status(args: StatusArgs) -> Result<()> {
let runtime = get_runtime();
let machine = runtime
.machine_manager()
.get(&args.name)
.ok_or_else(|| anyhow::anyhow!("Machine '{}' not found", args.name))?;
let state_str = match machine.state {
MachineState::Created => "Created",
MachineState::Starting => "Starting",
MachineState::Running => "Running",
MachineState::Stopping => "Stopping",
MachineState::Stopped => "Stopped",
};
println!("Machine: {}", machine.name);
println!("State: {}", state_str);
println!("CPUs: {}", machine.cpus);
println!("Memory: {} MB", machine.memory_mb);
println!("Disk: {} GB", machine.disk_gb);
println!("VM ID: {}", machine.vm_id);
Ok(())
}
async fn execute_ssh(args: SshArgs) -> Result<()> {
let runtime = get_runtime();
let machine = runtime
.machine_manager()
.get(&args.name)
.ok_or_else(|| anyhow::anyhow!("Machine '{}' not found", args.name))?;
if machine.state != MachineState::Running {
anyhow::bail!("Machine '{}' is not running", args.name);
}
if args.command.is_empty() {
println!("SSH into '{}' (interactive mode not yet implemented)", args.name);
} else {
println!("SSH into '{}': {}", args.name, args.command.join(" "));
}
Ok(())
}
async fn execute_exec(args: ExecArgs) -> Result<()> {
let runtime = get_runtime();
let machine = runtime
.machine_manager()
.get(&args.name)
.ok_or_else(|| anyhow::anyhow!("Machine '{}' not found", args.name))?;
if machine.state != MachineState::Running {
anyhow::bail!("Machine '{}' is not running", args.name);
}
println!("Exec in '{}': {}", args.name, args.command.join(" "));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_machine_state_display() {
assert_eq!(
match MachineState::Running {
MachineState::Running => "Running",
_ => "Other",
},
"Running"
);
}
}