@@ -1,8 +1,6 @@
1use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
1use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
6 io::{self, Write},
4 io::{self, Write},
@@ -25,7 +23,6 @@ use std::{
26use block2::RcBlock;
24use block2::RcBlock;
27use dispatch2::DispatchQueue;
25use dispatch2::DispatchQueue;
28−use lexopt::prelude::*;
29use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
26use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
30use objc2_foundation::*;
27use objc2_foundation::*;
31use objc2_virtualization::*;
28use objc2_virtualization::*;
@@ -40,7 +37,6 @@ const DEFAULT_CPU_COUNT: usize = 2;
40const DEFAULT_RAM_MB: u64 = 2048;
37const DEFAULT_RAM_MB: u64 = 2048;
41const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
38const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
42const START_TIMEOUT: Duration = Duration::from_secs(60);
39const START_TIMEOUT: Duration = Duration::from_secs(60);
43−const DEFAULT_EXPECT_TIMEOUT: Duration = Duration::from_secs(30);
44const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
40const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
45const PROVISION_SCRIPT: &str = include_str!("provision.sh");
41const PROVISION_SCRIPT: &str = include_str!("provision.sh");
46const PROVISION_SCRIPT_NAME: &str = "provision.sh";
42const PROVISION_SCRIPT_NAME: &str = "provision.sh";
@@ -121,7 +117,14 @@ impl DirectoryShare {
121+ pub cpu_count: usize,
123+ pub no_default_mounts: bool,
124+ pub mounts: Vec<String>,
124−pub fn run_with_args<F>(args: CliArgs, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
127+pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
126 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
129 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
@@ -129,7 +132,7 @@ where
131pub(crate) fn run_with_args_and_extras<F>(
134pub(crate) fn run_with_args_and_extras<F>(
132− args: CliArgs,
135+ args: VmArg,
133 io_handler: F,
136 io_handler: F,
134 extra_login_actions: Vec<LoginAction>,
137 extra_login_actions: Vec<LoginAction>,
135 extra_directory_shares: Vec<DirectoryShare>,
138 extra_directory_shares: Vec<DirectoryShare>,
@@ -172,22 +175,14 @@ where
172 let mise_directory_share =
175 let mise_directory_share =
173 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
176 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
175− let disk_path = if let Some(path) = args.disk {
177− return Err(format!("Disk image does not exist: {}", path.display()).into());
181− ensure_default_image(
178+ ensure_default_image(
182− &base_raw,
179+ &base_raw,
183− &base_compressed,
180+ &base_compressed,
184− &default_raw,
181+ &default_raw,
185− std::slice::from_ref(&mise_directory_share),
182+ std::slice::from_ref(&mise_directory_share),
187− ensure_instance_disk(&instance_raw, &default_raw)?;
184+ ensure_instance_disk(&instance_raw, &default_raw)?;
189− instance_raw
185+ let disk_path = instance_raw;
192 let mut login_actions = Vec::new();
187 let mut login_actions = Vec::new();
193 let mut directory_shares = Vec::new();
188 let mut directory_shares = Vec::new();
@@ -236,9 +231,6 @@ where
237 login_actions.extend(extra_login_actions);
232 login_actions.extend(extra_login_actions);
239− // Any user-provided login actions must come after our system ones
240− login_actions.extend(args.login_actions);
242 run_vm_with_io(
234 run_vm_with_io(
243 &disk_path,
235 &disk_path,
244 &login_actions,
236 &login_actions,
@@ -249,156 +241,6 @@ where
252−pub fn print_help() {
254− "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
256−vibe [OPTIONS] [disk-image.raw]
260− --help Print this help message.
261− --version Print the version (commit SHA).
262− --no-default-mounts Disable all default mounts.
263− --mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
264− Defaults to read-write.
265− Errors if host-path does not exist.
266− --cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
267− --ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
268− --script <path/to/script.sh> Run script in VM.
269− --send <some-command> Type `some-command` followed by newline into the VM.
270− --expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
271− If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
276−pub fn print_version() {
278− println!("https://github.com/lynaghk/vibe/");
279− println!("Git SHA: {}", env!("GIT_SHA"));
283− disk: Option<PathBuf>,
286− no_default_mounts: bool,
287− mounts: Vec<String>,
288− login_actions: Vec<LoginAction>,
294− pub fn version(&self) -> bool {
298− pub fn help(&self) -> bool {
302− pub fn cpu_count(&self) -> usize {
306− pub fn ram_bytes(&self) -> u64 {
310− pub fn ram_mb(&self) -> u64 {
311− self.ram_bytes / BYTES_PER_MB
315−pub fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
316− parse_cli_from(env::args_os())
319−pub fn parse_cli_from<I>(args: I) -> Result<CliArgs, Box<dyn std::error::Error>>
321− I: IntoIterator<Item = OsString>,
323− fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
326− .map_err(|_| format!("{flag} expects valid UTF-8").into())
329− let mut parser = lexopt::Parser::from_iter(args);
330− let mut disk = None;
331− let mut version = false;
332− let mut help = false;
333− let mut no_default_mounts = false;
334− let mut mounts = Vec::new();
335− let mut login_actions = Vec::new();
336− let mut script_index = 0;
337− let mut cpu_count = DEFAULT_CPU_COUNT;
338− let mut ram_bytes = DEFAULT_RAM_BYTES;
340− while let Some(arg) = parser.next()? {
342− Long("version") => version = true,
343− Long("help") | Short('h') => help = true,
344− Long("no-default-mounts") => no_default_mounts = true,
346− let value = os_to_string(parser.value()?, "--cpus")?.parse()?;
348− return Err("--cpus must be >= 1".into());
353− let value: u64 = os_to_string(parser.value()?, "--ram")?.parse()?;
355− return Err("--ram must be >= 1".into());
357− ram_bytes = value * BYTES_PER_MB;
360− mounts.push(os_to_string(parser.value()?, "--mount")?);
363− login_actions.push(Script {
364− path: os_to_string(parser.value()?, "--script")?.into(),
365− index: script_index,
370− login_actions.push(Send(os_to_string(parser.value()?, "--send")?));
373− let text = os_to_string(parser.value()?, "--expect")?;
374− let timeout = match parser.optional_value() {
375− Some(value) => Duration::from_secs(os_to_string(value, "--expect")?.parse()?),
376− None => DEFAULT_EXPECT_TIMEOUT,
378− login_actions.push(Expect { text, timeout });
382− return Err("Only one disk path may be provided".into());
384− disk = Some(PathBuf::from(value));
386− _ => return Err(arg.unexpected().into()),
402fn script_command_from_path(
244fn script_command_from_path(
403 path: &Path,
245 path: &Path,
404 index: usize,
246 index: usize,