use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;
use tracing::{debug, info};
use crate::config::Config;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LimaInstanceInfo {
pub name: String,
pub status: String,
#[serde(default)]
pub dir: Option<String>,
}
impl LimaInstanceInfo {
pub fn is_running(&self) -> bool {
self.status == "Running"
}
}
pub fn parse_lima_instances(stdout: &[u8]) -> Result<Vec<LimaInstanceInfo>> {
std::str::from_utf8(stdout)?
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| {
serde_json::from_str::<LimaInstanceInfo>(l)
.with_context(|| format!("Failed to parse limactl row: {}", l))
})
.collect()
}
pub(crate) enum VmState {
Running,
Stopped,
NotFound,
}
pub(crate) fn check_vm_state(vm_name: &str) -> Result<VmState> {
let instances = LimaInstance::list()?;
match instances.iter().find(|i| i.name == vm_name) {
Some(info) if info.is_running() => Ok(VmState::Running),
Some(_) => Ok(VmState::Stopped),
None => Ok(VmState::NotFound),
}
}
pub struct LimaInstance;
impl LimaInstance {
pub fn is_lima_available() -> bool {
Command::new("limactl")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn list() -> Result<Vec<LimaInstanceInfo>> {
let output = Command::new("limactl")
.arg("list")
.arg("--json")
.output()
.context("Failed to list Lima instances")?;
if !output.status.success() {
bail!("Failed to list Lima instances");
}
parse_lima_instances(&output.stdout)
}
pub fn stop_by_name(name: &str) -> Result<()> {
let output = Command::new("limactl")
.arg("stop")
.arg(name)
.output()
.with_context(|| format!("Failed to execute limactl stop for '{}'", name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not running") {
return Ok(());
}
bail!("Failed to stop Lima VM '{}': {}", name, stderr);
}
Ok(())
}
}
pub fn ensure_vm_running(config: &Config, worktree_path: &Path) -> Result<String> {
if !LimaInstance::is_lima_available() {
bail!(
"Lima backend is enabled but limactl is not installed.\n\
Install Lima: https://lima-vm.io/docs/installation/\n\
Or disable sandbox: set 'sandbox.enabled: false' in config."
);
}
let isolation = config.sandbox.lima.isolation();
let vm_name = super::instance_name(worktree_path, isolation.clone(), config)?;
debug!(vm_name = %vm_name, "checking Lima VM state");
let vm_state = check_vm_state(&vm_name)?;
match vm_state {
VmState::Running => {
debug!(vm_name = %vm_name, "Lima VM already running");
if config.sandbox.lima.provision_script().is_some() {
info!(vm_name = %vm_name, "custom provision script only runs on first VM creation; recreate VM to apply changes");
}
}
VmState::Stopped => {
info!(vm_name = %vm_name, "starting stopped Lima VM");
if config.sandbox.lima.provision_script().is_some() {
info!(vm_name = %vm_name, "custom provision script only runs on first VM creation; recreate VM to apply changes");
}
let msg = format!("Starting Lima VM {}", vm_name);
let mut cmd = Command::new("limactl");
cmd.args(["start", "--tty=false", "--progress", &vm_name]);
let start = std::time::Instant::now();
match crate::spinner::with_streaming_command_formatted(&msg, cmd, move |line| {
super::log_format::format_lima_log_line(line, &start)
}) {
Ok(()) => {}
Err(_) => {
if matches!(check_vm_state(&vm_name)?, VmState::Running) {
return Ok(vm_name);
}
bail!("Failed to start Lima VM '{}'", vm_name);
}
}
}
VmState::NotFound => {
info!(vm_name = %vm_name, "creating new Lima VM");
let agent = crate::multiplexer::agent::resolve_profile(config.agent.as_deref()).name();
let mounts = super::generate_mounts(worktree_path, isolation, config, &vm_name, agent)?;
eprintln!(" Mounts:");
for m in &mounts {
if m.host_path == m.guest_path {
eprintln!(" {} (rw)", m.host_path.display());
} else {
eprintln!(
" {} -> {} ({})",
m.host_path.display(),
m.guest_path.display(),
if m.read_only { "ro" } else { "rw" }
);
}
}
let needs_nix = {
use crate::sandbox::toolchain::{DetectedToolchain, resolve_toolchain};
resolve_toolchain(&config.sandbox.toolchain(), worktree_path)
!= DetectedToolchain::None
};
let lima_config =
super::generate_lima_config(&vm_name, &mounts, &config.sandbox, agent, needs_nix)?;
let config_path = std::env::temp_dir().join(format!("workmux-lima-{}.yaml", vm_name));
std::fs::write(&config_path, &lima_config).with_context(|| {
format!("Failed to write Lima config to {}", config_path.display())
})?;
let msg = format!("Creating Lima VM {}", vm_name);
let mut cmd = Command::new("limactl");
cmd.args([
"start",
"--name",
&vm_name,
"--tty=false",
"--progress",
&config_path.to_string_lossy(),
]);
let start = std::time::Instant::now();
match crate::spinner::with_streaming_command_formatted(&msg, cmd, move |line| {
super::log_format::format_lima_log_line(line, &start)
}) {
Ok(()) => {}
Err(_) => {
if matches!(check_vm_state(&vm_name)?, VmState::Running) {
return Ok(vm_name);
}
bail!("Failed to create Lima VM '{}'", vm_name);
}
}
}
}
info!(vm_name = %vm_name, "Lima VM ready");
Ok(vm_name)
}