@@ -1,8 +1,6 @@
11use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
22use std::{
3− env,
4− ffi::OsString,
5− fs,
3+ env, fs,
64 io::{self, Write},
75 os::{
86 fd::RawFd,
@@ -25,7 +23,6 @@ use std::{
2523
2624use block2::RcBlock;
2725use dispatch2::DispatchQueue;
28−use lexopt::prelude::*;
2926use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
3027use objc2_foundation::*;
3128use objc2_virtualization::*;
@@ -40,7 +37,6 @@ const DEFAULT_CPU_COUNT: usize = 2;
4037const DEFAULT_RAM_MB: u64 = 2048;
4138const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
4239const START_TIMEOUT: Duration = Duration::from_secs(60);
43−const DEFAULT_EXPECT_TIMEOUT: Duration = Duration::from_secs(30);
4440const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
4541const PROVISION_SCRIPT: &str = include_str!("provision.sh");
4642const PROVISION_SCRIPT_NAME: &str = "provision.sh";
@@ -121,7 +117,14 @@ impl DirectoryShare {
121117 }
122118}
123119
124−pub fn run_with_args<F>(args: CliArgs, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
120+pub struct VmArg {
121+ pub cpu_count: usize,
122+ pub ram_bytes: u64,
123+ pub no_default_mounts: bool,
124+ pub mounts: Vec<String>,
125+}
126+
127+pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
125128where
126129 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
127130{
@@ -129,7 +132,7 @@ where
129132}
130133
131134pub(crate) fn run_with_args_and_extras<F>(
132− args: CliArgs,
135+ args: VmArg,
133136 io_handler: F,
134137 extra_login_actions: Vec<LoginAction>,
135138 extra_directory_shares: Vec<DirectoryShare>,
@@ -172,22 +175,14 @@ where
172175 let mise_directory_share =
173176 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
174177
175− let disk_path = if let Some(path) = args.disk {
176− if !path.exists() {
177− return Err(format!("Disk image does not exist: {}", path.display()).into());
178− }
179− path
180− } else {
181− ensure_default_image(
182− &base_raw,
183− &base_compressed,
184− &default_raw,
185− std::slice::from_ref(&mise_directory_share),
186− )?;
187− ensure_instance_disk(&instance_raw, &default_raw)?;
188−
189− instance_raw
190− };
178+ ensure_default_image(
179+ &base_raw,
180+ &base_compressed,
181+ &default_raw,
182+ std::slice::from_ref(&mise_directory_share),
183+ )?;
184+ ensure_instance_disk(&instance_raw, &default_raw)?;
185+ let disk_path = instance_raw;
191186
192187 let mut login_actions = Vec::new();
193188 let mut directory_shares = Vec::new();
@@ -236,9 +231,6 @@ where
236231
237232 login_actions.extend(extra_login_actions);
238233
239− // Any user-provided login actions must come after our system ones
240− login_actions.extend(args.login_actions);
241−
242234 run_vm_with_io(
243235 &disk_path,
244236 &login_actions,
@@ -249,156 +241,6 @@ where
249241 )
250242}
251243
252−pub fn print_help() {
253− println!(
254− "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
255−
256−vibe [OPTIONS] [disk-image.raw]
257−
258−Options
259−
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.
272−"
273− );
274−}
275−
276−pub fn print_version() {
277− println!("Vibe");
278− println!("https://github.com/lynaghk/vibe/");
279− println!("Git SHA: {}", env!("GIT_SHA"));
280−}
281−
282−pub struct CliArgs {
283− disk: Option<PathBuf>,
284− version: bool,
285− help: bool,
286− no_default_mounts: bool,
287− mounts: Vec<String>,
288− login_actions: Vec<LoginAction>,
289− cpu_count: usize,
290− ram_bytes: u64,
291−}
292−
293−impl CliArgs {
294− pub fn version(&self) -> bool {
295− self.version
296− }
297−
298− pub fn help(&self) -> bool {
299− self.help
300− }
301−
302− pub fn cpu_count(&self) -> usize {
303− self.cpu_count
304− }
305−
306− pub fn ram_bytes(&self) -> u64 {
307− self.ram_bytes
308− }
309−
310− pub fn ram_mb(&self) -> u64 {
311− self.ram_bytes / BYTES_PER_MB
312− }
313−}
314−
315−pub fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
316− parse_cli_from(env::args_os())
317−}
318−
319−pub fn parse_cli_from<I>(args: I) -> Result<CliArgs, Box<dyn std::error::Error>>
320−where
321− I: IntoIterator<Item = OsString>,
322−{
323− fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
324− value
325− .into_string()
326− .map_err(|_| format!("{flag} expects valid UTF-8").into())
327− }
328−
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;
339−
340− while let Some(arg) = parser.next()? {
341− match arg {
342− Long("version") => version = true,
343− Long("help") | Short('h') => help = true,
344− Long("no-default-mounts") => no_default_mounts = true,
345− Long("cpus") => {
346− let value = os_to_string(parser.value()?, "--cpus")?.parse()?;
347− if value == 0 {
348− return Err("--cpus must be >= 1".into());
349− }
350− cpu_count = value;
351− }
352− Long("ram") => {
353− let value: u64 = os_to_string(parser.value()?, "--ram")?.parse()?;
354− if value == 0 {
355− return Err("--ram must be >= 1".into());
356− }
357− ram_bytes = value * BYTES_PER_MB;
358− }
359− Long("mount") => {
360− mounts.push(os_to_string(parser.value()?, "--mount")?);
361− }
362− Long("script") => {
363− login_actions.push(Script {
364− path: os_to_string(parser.value()?, "--script")?.into(),
365− index: script_index,
366− });
367− script_index += 1;
368− }
369− Long("send") => {
370− login_actions.push(Send(os_to_string(parser.value()?, "--send")?));
371− }
372− Long("expect") => {
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,
377− };
378− login_actions.push(Expect { text, timeout });
379− }
380− Value(value) => {
381− if disk.is_some() {
382− return Err("Only one disk path may be provided".into());
383− }
384− disk = Some(PathBuf::from(value));
385− }
386− _ => return Err(arg.unexpected().into()),
387− }
388− }
389−
390− Ok(CliArgs {
391− disk,
392− version,
393− help,
394− no_default_mounts,
395− mounts,
396− login_actions,
397− cpu_count,
398− ram_bytes,
399− })
400−}
401−
402244fn script_command_from_path(
403245 path: &Path,
404246 index: usize,