@@ -1,7 +1,7 @@
1−use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
2−use anyhow::{Context, Error, Result, bail};
31use std::{
4− env, fs,
2+ env,
3+ ffi::OsString,
4+ fs,
55 io::{self, Write},
66 os::{
77 fd::RawFd,
@@ -15,7 +15,6 @@ use std::{
1515 process::{Command, Stdio},
1616 sync::{
1717 Arc, Condvar, Mutex,
18− atomic::{AtomicBool, Ordering},
1918 mpsc::{self, Receiver, Sender},
2019 },
2120 thread,
@@ -23,8 +22,8 @@ use std::{
2322};
2423
2524use block2::RcBlock;
26−use bytesize::ByteSize;
2725use dispatch2::DispatchQueue;
26+use lexopt::prelude::*;
2827use objc2::{AnyThread, rc::Retained, runtime::ProtocolObject};
2928use objc2_foundation::*;
3029use objc2_virtualization::*;
@@ -33,46 +32,39 @@ const DEBIAN_COMPRESSED_DISK_URL: &str = "https://cloud.debian.org/images/cloud/
3332const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
3433const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
3534const SHARED_DIRECTORIES_TAG: &str = "shared";
36−pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
3735
36+const BYTES_PER_MB: u64 = 1024 * 1024;
37+const DEFAULT_CPU_COUNT: usize = 2;
38+const DEFAULT_RAM_MB: u64 = 2048;
39+const DEFAULT_RAM_BYTES: u64 = DEFAULT_RAM_MB * BYTES_PER_MB;
3840const START_TIMEOUT: Duration = Duration::from_secs(60);
41+const DEFAULT_EXPECT_TIMEOUT: Duration = Duration::from_secs(30);
3942const LOGIN_EXPECT_TIMEOUT: Duration = Duration::from_secs(120);
40−const PROVISION_EXPECT_TIMEOUT: Duration = Duration::from_secs(900);
41−
42−const ERROR_REPORT_SCRIPT: &str = include_str!("error_report.sh");
4343const PROVISION_SCRIPT: &str = include_str!("provision.sh");
44−const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
45−const DEFAULT_RAW_NAME: &str = "default.raw";
46−const INSTANCE_RAW_NAME: &str = "instance.raw";
47−const BASE_DISK_RAW_NAME: &str = "disk.raw";
4844
4945#[derive(Clone)]
50−pub enum LoginAction {
51− Expect {
52− text: String,
53− timeout: Duration,
54− },
55− ExpectEither {
56− success: String,
57− failure: String,
58− timeout: Duration,
59− },
46+enum LoginAction {
47+ Expect { text: String, timeout: Duration },
6048 Send(String),
49+ Script { path: PathBuf, index: usize },
6150}
62−use crate::config::BoxConfig;
6351use LoginAction::*;
6452
6553#[derive(Clone)]
66−pub struct DirectoryShare {
54+struct DirectoryShare {
6755 host: PathBuf,
6856 guest: PathBuf,
6957 read_only: bool,
7058}
7159
7260impl DirectoryShare {
73− pub fn new(host: PathBuf, mut guest: PathBuf, read_only: bool) -> Result<Self> {
61+ fn new(
62+ host: PathBuf,
63+ mut guest: PathBuf,
64+ read_only: bool,
65+ ) -> Result<Self, Box<dyn std::error::Error>> {
7466 if !host.exists() {
75− bail!(format!("host path does not exist: {}", host.display()));
67+ return Err(format!("Host path does not exist: {}", host.display()).into());
7668 }
7769 if !guest.is_absolute() {
7870 guest = PathBuf::from("/root").join(guest);
@@ -84,22 +76,23 @@ impl DirectoryShare {
8476 })
8577 }
8678
87− pub fn from_mount_spec(spec: &str) -> Result<Self> {
79+ fn from_mount_spec(spec: &str) -> Result<Self, Box<dyn std::error::Error>> {
8880 let parts: Vec<&str> = spec.split(':').collect();
8981 if parts.len() < 2 || parts.len() > 3 {
90− bail!(format!("invalid mount spec: {spec}"));
82+ return Err(format!("Invalid mount spec: {spec}").into());
9183 }
92− let host = expand_tilde_path(parts[0]);
84+ let host = PathBuf::from(parts[0]);
9385 let guest = PathBuf::from(parts[1]);
9486 let read_only = if parts.len() == 3 {
9587 match parts[2] {
9688 "read-only" => true,
9789 "read-write" => false,
9890 _ => {
99− bail!(format!(
91+ return Err(format!(
10092 "Invalid mount mode '{}'; expected read-only or read-write",
10193 parts[2]
102− ));
94+ )
95+ .into());
10396 }
10497 }
10598 } else {
@@ -122,49 +115,23 @@ impl DirectoryShare {
122115 }
123116}
124117
125−fn expand_tilde_path(value: &str) -> PathBuf {
126− if let Some(stripped) = value.strip_prefix("~/") {
127− if let Ok(home) = env::var("HOME") {
128− return PathBuf::from(home).join(stripped);
129− }
130− } else if value == "~"
131− && let Ok(home) = env::var("HOME")
132− {
133− return PathBuf::from(home);
134− }
135− PathBuf::from(value)
136−}
137−
138−pub struct VmArg {
139− pub cpu_count: usize,
140− pub ram_bytes: u64,
141− pub disk_bytes: u64,
142− pub no_default_mounts: bool,
143− pub mounts: Vec<String>,
144−}
118+pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
119+ let args = parse_cli()?;
145120
146−type StatusEmitter<'a> = dyn Fn(&str) + std::marker::Send + Sync + 'a;
121+ if args.version() {
122+ print_version();
123+ return Ok(());
124+ }
147125
148−fn emit_status(status: Option<&StatusEmitter<'_>>, message: &str) {
149− if let Some(status) = status {
150− status(message);
126+ if args.help() {
127+ print_help();
128+ return Ok(());
151129 }
152−}
153130
154−pub fn run_with_args<F>(args: VmArg, io_handler: F) -> Result<()>
155−where
156− F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
157−{
158− run_with_args_and_extras(args, io_handler, Vec::new(), Vec::new(), None)
131+ run_with_args(args, spawn_vm_io)
159132}
160133
161−pub fn run_with_args_and_extras<F>(
162− args: VmArg,
163− io_handler: F,
164− extra_login_actions: Vec<LoginAction>,
165− extra_directory_shares: Vec<DirectoryShare>,
166− status: Option<&StatusEmitter<'_>>,
167−) -> Result<()>
134+pub fn run_with_args<F>(args: CliArgs, io_handler: F) -> Result<(), Box<dyn std::error::Error>>
168135where
169136 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
170137{
@@ -173,7 +140,7 @@ where
173140 let project_root = env::current_dir()?;
174141 let project_name = project_root
175142 .file_name()
176− .with_context(|| "Project directory has no name")?
143+ .ok_or("Project directory has no name")?
177144 .to_string_lossy()
178145 .into_owned();
179146
@@ -181,14 +148,10 @@ where
181148 let cache_home = env::var("XDG_CACHE_HOME")
182149 .map(PathBuf::from)
183150 .unwrap_or_else(|_| home.join(".cache"));
184− let cache_dir = cache_home.join(GLOBAL_CACHE_DIR_NAME);
151+ let cache_dir = cache_home.join("vibe");
185152 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
186153
187− let instance_dir = project_root.join(INSTANCE_DIR_NAME);
188− fs::create_dir_all(&instance_dir)?;
189− emit_status(status, "preparing VM image...");
190− tracing::info!("preparing VM image...");
191− let provision_log = instance_dir.join("provision.log");
154+ let instance_dir = project_root.join(".vibe");
192155
193156 let basename_compressed = DEBIAN_COMPRESSED_DISK_URL.rsplit('/').next().unwrap();
194157 let base_compressed = cache_dir.join(basename_compressed);
@@ -197,8 +160,8 @@ where
197160 basename_compressed.trim_end_matches(".tar.xz")
198161 ));
199162
200− let default_raw = cache_dir.join(DEFAULT_RAW_NAME);
201− let instance_raw = instance_dir.join(INSTANCE_RAW_NAME);
163+ let default_raw = cache_dir.join("default.raw");
164+ let instance_raw = instance_dir.join("instance.raw");
202165
203166 // Prepare system-wide directories
204167 fs::create_dir_all(&cache_dir)?;
@@ -207,31 +170,28 @@ where
207170 let mise_directory_share =
208171 DirectoryShare::new(guest_mise_cache, "/root/.local/share/mise".into(), false)?;
209172
210− ensure_default_image(
211− &base_raw,
212− &base_compressed,
213− &default_raw,
214− std::slice::from_ref(&mise_directory_share),
215− Some(&provision_log),
216− status,
217− )?;
218− let _ = ensure_instance_disk(
219− &instance_raw,
220− &default_raw,
221− ByteSize(args.disk_bytes),
222− status,
223− )?;
224− let base_size = fs::metadata(&default_raw)?.len();
225− let instance_size = fs::metadata(&instance_raw)?.len();
226− let needs_resize = instance_size > base_size;
227− let disk_path = instance_raw;
173+ let disk_path = if let Some(path) = args.disk {
174+ if !path.exists() {
175+ return Err(format!("Disk image does not exist: {}", path.display()).into());
176+ }
177+ path
178+ } else {
179+ ensure_default_image(
180+ &base_raw,
181+ &base_compressed,
182+ &default_raw,
183+ std::slice::from_ref(&mise_directory_share),
184+ )?;
185+ ensure_instance_disk(&instance_raw, &default_raw)?;
186+
187+ instance_raw
188+ };
228189
229190 let mut login_actions = Vec::new();
230191 let mut directory_shares = Vec::new();
231192
232193 if !args.no_default_mounts {
233− let project_guest_dir = PathBuf::from(PROJECT_GUEST_BASE).join(project_name);
234− login_actions.push(Send(format!("cd {}", project_guest_dir.display())));
194+ login_actions.push(Send(format!("cd {project_name}")));
235195
236196 // discourage read/write of .git folder from within the VM. note that this isn't secure, since the VM runs as root and could unmount this.
237197 // I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
@@ -239,25 +199,50 @@ where
239199 login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
240200 }
241201
202+ directory_shares.push(
203+ DirectoryShare::new(
204+ project_root,
205+ PathBuf::from("/root/").join(project_name),
206+ false,
207+ )
208+ .expect("Project directory must exist"),
209+ );
210+
242211 directory_shares.push(mise_directory_share);
243− }
244212
245− directory_shares.extend(extra_directory_shares);
213+ // Add default shares, if they exist
214+ for share in [
215+ DirectoryShare::new(home.join(".m2"), "/root/.m2".into(), false),
216+ DirectoryShare::new(
217+ home.join(".cargo/registry"),
218+ "/root/.cargo/registry".into(),
219+ false,
220+ ),
221+ DirectoryShare::new(home.join(".codex"), "/root/.codex".into(), false),
222+ DirectoryShare::new(home.join(".claude"), "/root/.claude".into(), false),
223+ DirectoryShare::new(
224+ "/Users/zhangjie/Documents/Code/CompletePrograms/vibebox/.ssh".into(),
225+ "/root/.ssh".into(),
226+ true,
227+ ),
228+ ]
229+ .into_iter()
230+ .flatten()
231+ {
232+ directory_shares.push(share)
233+ }
234+ }
246235
247236 for spec in &args.mounts {
248237 directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
249238 }
250239
251− if needs_resize {
252− let resize_cmd = script_command_from_content("resize_disk.sh", RESIZE_DISK_SCRIPT)?;
253− login_actions.push(Send(resize_cmd));
254− }
255−
256240 if let Some(motd_action) = motd_login_action(&directory_shares) {
257241 login_actions.push(motd_action);
258242 }
259243
260− login_actions.extend(extra_login_actions);
244+ // Any user-provided login actions must come after our system ones
245+ login_actions.extend(args.login_actions);
261246
262247 run_vm_with_io(
263248 &disk_path,
@@ -265,29 +250,184 @@ where
265250 &directory_shares[..],
266251 args.cpu_count,
267252 args.ram_bytes,
268− status,
269253 io_handler,
270254 )
271255}
272256
273−pub fn script_command_from_content(label: &str, script: &str) -> Result<String> {
257+pub fn print_help() {
258+ println!(
259+ "Vibe is a quick way to spin up a Linux virtual machine on Mac to sandbox LLM agents.
260+
261+vibe [OPTIONS] [disk-image.raw]
262+
263+Options
264+
265+ --help Print this help message.
266+ --version Print the version (commit SHA).
267+ --no-default-mounts Disable all default mounts.
268+ --mount host-path:guest-path[:read-only | :read-write] Mount `host-path` inside VM at `guest-path`.
269+ Defaults to read-write.
270+ Errors if host-path does not exist.
271+ --cpus <count> Number of virtual CPUs (default {DEFAULT_CPU_COUNT}).
272+ --ram <megabytes> RAM size in megabytes (default {DEFAULT_RAM_MB}).
273+ --script <path/to/script.sh> Run script in VM.
274+ --send <some-command> Type `some-command` followed by newline into the VM.
275+ --expect <string> [timeout-seconds] Wait for `string` to appear in console output before executing next `--script` or `--send`.
276+ If `string` does not appear within timeout (default 30 seconds), shutdown VM with error.
277+"
278+ );
279+}
280+
281+pub fn print_version() {
282+ println!("Vibe");
283+ println!("https://github.com/lynaghk/vibe/");
284+ println!("Git SHA: {}", env!("GIT_SHA"));
285+}
286+
287+pub struct CliArgs {
288+ disk: Option<PathBuf>,
289+ version: bool,
290+ help: bool,
291+ no_default_mounts: bool,
292+ mounts: Vec<String>,
293+ login_actions: Vec<LoginAction>,
294+ cpu_count: usize,
295+ ram_bytes: u64,
296+}
297+
298+impl CliArgs {
299+ pub fn version(&self) -> bool {
300+ self.version
301+ }
302+
303+ pub fn help(&self) -> bool {
304+ self.help
305+ }
306+
307+ pub fn cpu_count(&self) -> usize {
308+ self.cpu_count
309+ }
310+
311+ pub fn ram_bytes(&self) -> u64 {
312+ self.ram_bytes
313+ }
314+
315+ pub fn ram_mb(&self) -> u64 {
316+ self.ram_bytes / BYTES_PER_MB
317+ }
318+}
319+
320+pub fn parse_cli() -> Result<CliArgs, Box<dyn std::error::Error>> {
321+ parse_cli_from(env::args_os())
322+}
323+
324+pub fn parse_cli_from<I>(args: I) -> Result<CliArgs, Box<dyn std::error::Error>>
325+where
326+ I: IntoIterator<Item = OsString>,
327+{
328+ fn os_to_string(value: OsString, flag: &str) -> Result<String, Box<dyn std::error::Error>> {
329+ value
330+ .into_string()
331+ .map_err(|_| format!("{flag} expects valid UTF-8").into())
332+ }
333+
334+ let mut parser = lexopt::Parser::from_iter(args);
335+ let mut disk = None;
336+ let mut version = false;
337+ let mut help = false;
338+ let mut no_default_mounts = false;
339+ let mut mounts = Vec::new();
340+ let mut login_actions = Vec::new();
341+ let mut script_index = 0;
342+ let mut cpu_count = DEFAULT_CPU_COUNT;
343+ let mut ram_bytes = DEFAULT_RAM_BYTES;
344+
345+ while let Some(arg) = parser.next()? {
346+ match arg {
347+ Long("version") => version = true,
348+ Long("help") | Short('h') => help = true,
349+ Long("no-default-mounts") => no_default_mounts = true,
350+ Long("cpus") => {
351+ let value = os_to_string(parser.value()?, "--cpus")?.parse()?;
352+ if value == 0 {
353+ return Err("--cpus must be >= 1".into());
354+ }
355+ cpu_count = value;
356+ }
357+ Long("ram") => {
358+ let value: u64 = os_to_string(parser.value()?, "--ram")?.parse()?;
359+ if value == 0 {
360+ return Err("--ram must be >= 1".into());
361+ }
362+ ram_bytes = value * BYTES_PER_MB;
363+ }
364+ Long("mount") => {
365+ mounts.push(os_to_string(parser.value()?, "--mount")?);
366+ }
367+ Long("script") => {
368+ login_actions.push(Script {
369+ path: os_to_string(parser.value()?, "--script")?.into(),
370+ index: script_index,
371+ });
372+ script_index += 1;
373+ }
374+ Long("send") => {
375+ login_actions.push(Send(os_to_string(parser.value()?, "--send")?));
376+ }
377+ Long("expect") => {
378+ let text = os_to_string(parser.value()?, "--expect")?;
379+ let timeout = match parser.optional_value() {
380+ Some(value) => Duration::from_secs(os_to_string(value, "--expect")?.parse()?),
381+ None => DEFAULT_EXPECT_TIMEOUT,
382+ };
383+ login_actions.push(Expect { text, timeout });
384+ }
385+ Value(value) => {
386+ if disk.is_some() {
387+ return Err("Only one disk path may be provided".into());
388+ }
389+ disk = Some(PathBuf::from(value));
390+ }
391+ _ => return Err(arg.unexpected().into()),
392+ }
393+ }
394+
395+ Ok(CliArgs {
396+ disk,
397+ version,
398+ help,
399+ no_default_mounts,
400+ mounts,
401+ login_actions,
402+ cpu_count,
403+ ram_bytes,
404+ })
405+}
406+
407+fn script_command_from_path(
408+ path: &Path,
409+ index: usize,
410+) -> Result<String, Box<dyn std::error::Error>> {
411+ let script = fs::read_to_string(path)
412+ .map_err(|err| format!("Failed to read script {}: {err}", path.display()))?;
413+ let label = format!("{}_{}", index, path.file_name().unwrap().display());
414+ script_command_from_content(&label, &script)
415+}
416+
417+fn script_command_from_content(
418+ label: &str,
419+ script: &str,
420+) -> Result<String, Box<dyn std::error::Error>> {
274421 let marker = "VIBE_SCRIPT_EOF";
275422 let guest_dir = "/tmp/vibe-scripts";
276423 let guest_path = format!("{guest_dir}/{label}.sh");
277− let script_body = match script.split_once('\n') {
278− Some((first, rest)) if first.starts_with("#!") => rest,
279− _ => script,
280− };
281− let wrapped_script = ERROR_REPORT_SCRIPT
282− .replace("__LABEL__", label)
283− .replace("__SCRIPT_BODY__", script_body);
284424 let command = format!(
285− "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{wrapped_script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
425+ "mkdir -p {guest_dir}\ncat >{guest_path} <<'{marker}'\n{script}\n{marker}\nchmod +x {guest_path}\n{guest_path}"
286426 );
287427 if script.contains(marker) {
288− bail!(format!(
289− "Script '{label}' contains marker '{marker}', cannot safely upload"
290− ));
428+ return Err(
429+ format!("Script '{label}' contains marker '{marker}', cannot safely upload").into(),
430+ );
291431 }
292432 Ok(command)
293433}
@@ -321,6 +461,18 @@ fn motd_login_action(directory_shares: &[DirectoryShare]) -> Option<LoginAction>
321461 }
322462
323463 let mut output = String::new();
464+ output.push_str(
465+ "
466+░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░
467+░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
468+ ░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
469+ ░▒▓█▓▒▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓██████▓▒░
470+ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
471+ ░▒▓█▓▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
472+ ░▒▓██▓▒░ ░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░
473+
474+",
475+ );
324476 output.push_str(&format!(
325477 "{host_header:<host_width$} {guest_header:<guest_width$} {mode_header}\n",
326478 host_width = host_width
@@ -351,12 +503,6 @@ enum WaitResult {
351503 Found,
352504}
353505
354−#[derive(PartialEq, Eq)]
355−enum WaitAnyResult {
356− Timeout,
357− Found(usize),
358−}
359−
360506pub enum VmInput {
361507 Bytes(Vec<u8>),
362508 Shutdown,
@@ -364,7 +510,6 @@ pub enum VmInput {
364510
365511enum VmOutput {
366512 LoginActionTimeout { action: String, timeout: Duration },
367− LoginActionFailed { action: String, reason: String },
368513}
369514
370515#[derive(Default)]
@@ -401,100 +546,20 @@ impl OutputMonitor {
401546 WaitResult::Found
402547 }
403548 }
404−
405− fn wait_for_any(&self, needles: &[&str], timeout: Duration) -> WaitAnyResult {
406− let mut found: Option<usize> = None;
407− let (_unused, timeout_result) = self
408− .condvar
409− .wait_timeout_while(self.buffer.lock().unwrap(), timeout, |buf| {
410− if let Some((pos, idx, len)) = find_any(buf, needles) {
411− *buf = buf[(pos + len)..].to_string();
412− found = Some(idx);
413− false
414− } else {
415− true
416− }
417− })
418− .unwrap();
419−
420− if timeout_result.timed_out() {
421− WaitAnyResult::Timeout
422− } else {
423− WaitAnyResult::Found(found.unwrap_or(0))
424− }
425− }
426−}
427−
428−fn find_any(buf: &str, needles: &[&str]) -> Option<(usize, usize, usize)> {
429− let mut best: Option<(usize, usize, usize)> = None; // (pos, idx, len)
430− for (idx, needle) in needles.iter().enumerate() {
431− if let Some(pos) = buf.find(needle) {
432− let candidate = (pos, idx, needle.len());
433− if best.is_none_or(|b| candidate.0 < b.0) {
434− best = Some(candidate);
435− }
436− }
437− }
438− best
439−}
440−
441−#[derive(Debug)]
442−pub struct IoControl {
443− forward_input: AtomicBool,
444− forward_output: AtomicBool,
445− restore_terminal: AtomicBool,
446−}
447−
448−impl IoControl {
449− pub fn new() -> Arc<Self> {
450− Arc::new(Self {
451− forward_input: AtomicBool::new(true),
452− forward_output: AtomicBool::new(true),
453− restore_terminal: AtomicBool::new(false),
454− })
455− }
456−
457− pub fn set_forward_input(&self, enabled: bool) {
458− self.forward_input.store(enabled, Ordering::SeqCst);
459− }
460−
461− pub fn set_forward_output(&self, enabled: bool) {
462− self.forward_output.store(enabled, Ordering::SeqCst);
463− }
464−
465− pub fn request_terminal_restore(&self) {
466− self.restore_terminal.store(true, Ordering::SeqCst);
467− }
468−
469− fn forward_input(&self) -> bool {
470− self.forward_input.load(Ordering::SeqCst)
471− }
472−
473− fn forward_output(&self) -> bool {
474− self.forward_output.load(Ordering::SeqCst)
475− }
476−
477− fn take_restore_terminal(&self) -> bool {
478− self.restore_terminal
479− .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
480− .is_ok()
481− }
482549}
483550
484551fn ensure_base_image(
485552 base_raw: &Path,
486553 base_compressed: &Path,
487− status: Option<&StatusEmitter<'_>>,
488−) -> Result<()> {
554+) -> Result<(), Box<dyn std::error::Error>> {
489555 if base_raw.exists() {
490556 return Ok(());
491557 }
492558
493559 if !base_compressed.exists()
494− || fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
560+ || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
495561 {
496− emit_status(status, "downloading base image...");
497− tracing::info!("downloading base image");
562+ println!("Downloading base image...");
498563 let status = Command::new("curl")
499564 .args([
500565 "--continue-at",
@@ -508,14 +573,12 @@ fn ensure_base_image(
508573 ])
509574 .status()?;
510575 if !status.success() {
511− bail!("failed to download base image");
576+ return Err("Failed to download base image".into());
512577 }
513578 }
514579
515580 // Check SHA
516581 {
517− emit_status(status, "verifying base image...");
518− tracing::info!("verifying base image...");
519582 let input = format!("{} {}\n", DEBIAN_COMPRESSED_SHA, base_compressed.display());
520583
521584 let mut child = Command::new("/usr/bin/shasum")
@@ -533,25 +596,18 @@ fn ensure_base_image(
533596
534597 let status = child.wait().expect("failed to wait on child");
535598 if !status.success() {
536− bail!(format!(
537− "SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}"
538− ));
599+ return Err(format!("SHA validation failed for {DEBIAN_COMPRESSED_DISK_URL}").into());
539600 }
540601 }
541602
542− emit_status(status, "decompressing base image...");
543− tracing::info!("decompressing base image...");
603+ println!("Decompressing base image...");
544604 let status = Command::new("tar")
545− .args([
546− "-xOf",
547− &base_compressed.to_string_lossy(),
548− BASE_DISK_RAW_NAME,
549− ])
550− .stdout(fs::File::create(base_raw)?)
605+ .args(["-xOf", &base_compressed.to_string_lossy(), "disk.raw"])
606+ .stdout(std::fs::File::create(base_raw)?)
551607 .status()?;
552608
553609 if !status.success() {
554− bail!("Failed to decompress base image");
610+ return Err("Failed to decompress base image".into());
555611 }
556612
557613 Ok(())
@@ -562,56 +618,24 @@ fn ensure_default_image(
562618 base_compressed: &Path,
563619 default_raw: &Path,
564620 directory_shares: &[DirectoryShare],
565− provision_log: Option<&Path>,
566− status: Option<&StatusEmitter<'_>>,
567−) -> Result<()> {
621+) -> Result<(), Box<dyn std::error::Error>> {
568622 if default_raw.exists() {
569623 return Ok(());
570624 }
571625
572− ensure_base_image(base_raw, base_compressed, status)?;
626+ ensure_base_image(base_raw, base_compressed)?;
573627
574− emit_status(status, "configuring base image...");
575− tracing::info!("configuring base image...");
628+ println!("Configuring base image...");
576629 fs::copy(base_raw, default_raw)?;
577630
578631 let provision_command = script_command_from_content("provision.sh", PROVISION_SCRIPT)?;
579− let provision_actions = [
580− Send(provision_command),
581− ExpectEither {
582− success: "VIBEBOX_PROVISION_OK".to_string(),
583− failure: "VIBEBOX_PROVISION_FAILED".to_string(),
584− timeout: PROVISION_EXPECT_TIMEOUT,
585− },
586− ];
587− let provision_result = if let Some(log_path) = provision_log {
588− let log_path = log_path.to_path_buf();
589− run_vm_with_io(
590− default_raw,
591− &provision_actions,
592− directory_shares,
593− BoxConfig::default().cpu_count,
594− BoxConfig::default().ram_size.as_u64(),
595− status,
596− move |output_monitor, vm_output_fd, vm_input_fd| {
597− spawn_vm_io_with_log(output_monitor, vm_output_fd, vm_input_fd, log_path)
598− },
599− )
600− } else {
601− run_vm(
602− default_raw,
603− &provision_actions,
604− directory_shares,
605− BoxConfig::default().cpu_count,
606− BoxConfig::default().ram_size.as_u64(),
607− status,
608− )
609− };
610−
611− if let Err(err) = provision_result {
612− let _ = fs::remove_file(default_raw);
613− return Err(err);
614− }
632+ run_vm(
633+ default_raw,
634+ &[Send(provision_command)],
635+ directory_shares,
636+ DEFAULT_CPU_COUNT,
637+ DEFAULT_RAM_BYTES,
638+ )?;
615639
616640 Ok(())
617641}
@@ -619,47 +643,15 @@ fn ensure_default_image(
619643fn ensure_instance_disk(
620644 instance_raw: &Path,
621645 template_raw: &Path,
622− target_bytes: ByteSize,
623− status: Option<&StatusEmitter<'_>>,
624−) -> Result<bool> {
646+) -> Result<(), Box<dyn std::error::Error>> {
625647 if instance_raw.exists() {
626− let current_size = ByteSize(fs::metadata(instance_raw)?.len());
627− if current_size != target_bytes {
628− let current_gb = current_size;
629− let target_gb = target_bytes;
630− tracing::warn!(
631− "instance disk size does not match config (current {}, config {}); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
632− current_gb,
633− target_gb
634− );
635− }
636− return Ok(false);
637− }
638−
639− let template_size = ByteSize(fs::metadata(template_raw)?.len());
640− if target_bytes < template_size {
641− bail!(format!(
642− "Requested disk size {} is smaller than base image size {}",
643− target_bytes, template_size
644− ));
645− }
646− let target_size = target_bytes;
647− let needs_resize = target_size > template_size;
648−
649− emit_status(status, "creating instance disk...");
650− tracing::info!("creating instance disk...");
651− tracing::info!(path = %template_raw.display(), "creating instance disk");
652− fs::create_dir_all(instance_raw.parent().unwrap())?;
653− if target_size == template_size {
654− fs::copy(template_raw, instance_raw)?;
655− return Ok(needs_resize);
648+ return Ok(());
656649 }
657650
658− let mut dst = fs::File::create(instance_raw)?;
659− dst.set_len(target_size.as_u64())?;
660− let mut src = fs::File::open(template_raw)?;
661− std::io::copy(&mut src, &mut dst)?;
662− Ok(needs_resize)
651+ println!("Creating instance disk from {}...", template_raw.display());
652+ std::fs::create_dir_all(instance_raw.parent().unwrap())?;
653+ fs::copy(template_raw, instance_raw)?;
654+ Ok(())
663655}
664656
665657pub struct IoContext {
@@ -675,17 +667,15 @@ pub fn create_pipe() -> (OwnedFd, OwnedFd) {
675667 (read_stream.into(), write_stream.into())
676668}
677669
678−pub fn spawn_vm_io_with_hooks<
679− F: FnMut(&str) -> bool + std::marker::Send + 'static,
680− G: FnMut(&[u8]) + std::marker::Send + 'static,
681−>(
670+pub fn spawn_vm_io_with_line_handler<F>(
682671 output_monitor: Arc<OutputMonitor>,
683672 vm_output_fd: OwnedFd,
684673 vm_input_fd: OwnedFd,
685− io_control: Arc<IoControl>,
686674 mut on_line: F,
687− mut on_output: G,
688−) -> IoContext {
675+) -> IoContext
676+where
677+ F: FnMut(&str) -> bool + ::std::marker::Send + 'static,
678+{
689679 let (input_tx, input_rx): (Sender<VmInput>, Receiver<VmInput>) = mpsc::channel();
690680
691681 // raw_guard is set when we've put the user's terminal into raw mode because we've attached stdin/stdout to the VM.
@@ -700,12 +690,7 @@ pub fn spawn_vm_io_with_hooks<
700690 Error,
701691 }
702692
703− fn poll_with_wakeup<'a>(
704− main_fd: RawFd,
705− wakeup_fd: RawFd,
706− buf: &'a mut [u8],
707− timeout_ms: i32,
708− ) -> PollResult<'a> {
693+ fn poll_with_wakeup<'a>(main_fd: RawFd, wakeup_fd: RawFd, buf: &'a mut [u8]) -> PollResult<'a> {
709694 let mut fds = [
710695 libc::pollfd {
711696 fd: main_fd,
@@ -719,12 +704,8 @@ pub fn spawn_vm_io_with_hooks<
719704 },
720705 ];
721706
722− let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout_ms) };
723− if ret == 0 {
724− PollResult::Spurious
725− } else if ret < 0 {
726− PollResult::Error
727− } else if fds[1].revents & libc::POLLIN != 0 {
707+ let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, -1) };
708+ if ret <= 0 || fds[1].revents & libc::POLLIN != 0 {
728709 PollResult::Shutdown
729710 } else if fds[0].revents & libc::POLLIN != 0 {
730711 let n = unsafe { libc::read(main_fd, buf.as_mut_ptr() as *mut _, buf.len()) };
@@ -740,37 +721,18 @@ pub fn spawn_vm_io_with_hooks<
740721 }
741722 }
742723
743− fn poll_wakeup_only(wakeup_fd: RawFd, timeout_ms: i32) -> bool {
744− let mut fds = [libc::pollfd {
745− fd: wakeup_fd,
746− events: libc::POLLIN,
747− revents: 0,
748− }];
749−
750− let ret = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
751− ret > 0 && fds[0].revents & libc::POLLIN != 0
752− }
753−
754724 // Copies from stdin to the VM; also polls wakeup_read to exit the thread when it's time to shutdown.
755725 let stdin_thread = thread::spawn({
756726 let input_tx = input_tx.clone();
757727 let raw_guard = raw_guard.clone();
758728 let wakeup_read = wakeup_read.try_clone().unwrap();
759− let io_control = io_control.clone();
760729
761730 move || {
762731 let mut buf = [0u8; 1024];
763732 let mut pending_command: Vec<u8> = Vec::new();
764733 let mut command_mode = false;
765734 loop {
766− if !io_control.forward_input() {
767− if poll_wakeup_only(wakeup_read.as_raw_fd(), 100) {
768− break;
769− }
770− continue;
771− }
772−
773− match poll_with_wakeup(libc::STDIN_FILENO, wakeup_read.as_raw_fd(), &mut buf, -1) {
735+ match poll_with_wakeup(libc::STDIN_FILENO, wakeup_read.as_raw_fd(), &mut buf) {
774736 PollResult::Shutdown | PollResult::Error => break,
775737 PollResult::Spurious => continue,
776738 PollResult::Ready(bytes) => {
@@ -814,40 +776,28 @@ pub fn spawn_vm_io_with_hooks<
814776 let stdout_thread = thread::spawn({
815777 let raw_guard = raw_guard.clone();
816778 let wakeup_read = wakeup_read.try_clone().unwrap();
817− let io_control = io_control.clone();
818779
819780 move || {
781+ let mut stdout = std::io::stdout().lock();
820782 let mut buf = [0u8; 1024];
821783 loop {
822− if io_control.take_restore_terminal() {
823− let mut guard = raw_guard.lock().unwrap();
824− *guard = None;
825− }
826− match poll_with_wakeup(
827− vm_output_fd.as_raw_fd(),
828− wakeup_read.as_raw_fd(),
829− &mut buf,
830− 100,
831− ) {
784+ match poll_with_wakeup(vm_output_fd.as_raw_fd(), wakeup_read.as_raw_fd(), &mut buf)
785+ {
832786 PollResult::Shutdown | PollResult::Error => break,
833787 PollResult::Spurious => continue,
834788 PollResult::Ready(bytes) => {
835− if io_control.forward_output() {
836− // enable raw mode if we haven't already
837− if raw_guard.lock().unwrap().is_none()
838− && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
839− {
840− *raw_guard.lock().unwrap() = Some(guard);
841− }
789+ // enable raw mode, if we haven't already
790+ if raw_guard.lock().unwrap().is_none()
791+ && let Ok(guard) = enable_raw_mode(libc::STDIN_FILENO)
792+ {
793+ *raw_guard.lock().unwrap() = Some(guard);
794+ }
842795
843− let mut stdout = io::stdout().lock();
844− if stdout.write_all(bytes).is_err() {
845− break;
846− }
847− let _ = stdout.flush();
796+ if stdout.write_all(bytes).is_err() {
797+ break;
848798 }
799+ let _ = stdout.flush();
849800 output_monitor.push(bytes);
850− on_output(bytes);
851801 }
852802 }
853803 }
@@ -856,7 +806,7 @@ pub fn spawn_vm_io_with_hooks<
856806
857807 // Copies data from mpsc channel into VM, so vibe can "type" stuff and run scripts.
858808 let mux_thread = thread::spawn(move || {
859− let mut vm_writer = fs::File::from(vm_input_fd);
809+ let mut vm_writer = std::fs::File::from(vm_input_fd);
860810 loop {
861811 match input_rx.recv() {
862812 Ok(VmInput::Bytes(data)) => {
@@ -879,25 +829,6 @@ pub fn spawn_vm_io_with_hooks<
879829 }
880830}
881831
882−pub fn spawn_vm_io_with_line_handler<F>(
883− output_monitor: Arc<OutputMonitor>,
884− vm_output_fd: OwnedFd,
885− vm_input_fd: OwnedFd,
886− on_line: F,
887−) -> IoContext
888−where
889− F: FnMut(&str) -> bool + std::marker::Send + 'static,
890−{
891− spawn_vm_io_with_hooks(
892− output_monitor,
893− vm_output_fd,
894− vm_input_fd,
895− IoControl::new(),
896− on_line,
897− |_| {},
898− )
899−}
900−
901832pub fn spawn_vm_io(
902833 output_monitor: Arc<OutputMonitor>,
903834 vm_output_fd: OwnedFd,
@@ -906,36 +837,6 @@ pub fn spawn_vm_io(
906837 spawn_vm_io_with_line_handler(output_monitor, vm_output_fd, vm_input_fd, |_| false)
907838}
908839
909−fn spawn_vm_io_with_log(
910− output_monitor: Arc<OutputMonitor>,
911− vm_output_fd: OwnedFd,
912− vm_input_fd: OwnedFd,
913− log_path: PathBuf,
914−) -> IoContext {
915− let log_file = fs::OpenOptions::new()
916− .create(true)
917− .write(true)
918− .truncate(true)
919− .open(&log_path)
920− .ok()
921− .map(|file| Arc::new(Mutex::new(file)));
922−
923− spawn_vm_io_with_hooks(
924− output_monitor,
925− vm_output_fd,
926− vm_input_fd,
927− IoControl::new(),
928− |_| false,
929− move |bytes| {
930− if let Some(log) = &log_file
931− && let Ok(mut file) = log.lock()
932− {
933− let _ = file.write_all(bytes);
934− }
935− },
936− )
937−}
938−
939840impl IoContext {
940841 pub fn shutdown(self) {
941842 let _ = self.input_tx.send(VmInput::Shutdown);
@@ -953,7 +854,7 @@ fn create_vm_configuration(
953854 vm_writes_to_fd: OwnedFd,
954855 cpu_count: usize,
955856 ram_bytes: u64,
956−) -> Result<Retained<VZVirtualMachineConfiguration>> {
857+) -> Result<Retained<VZVirtualMachineConfiguration>, Box<dyn std::error::Error>> {
957858 unsafe {
958859 let platform =
959860 VZGenericPlatformConfiguration::init(VZGenericPlatformConfiguration::alloc());
@@ -987,7 +888,7 @@ fn create_vm_configuration(
987888 false,
988889 VZDiskImageCachingMode::Automatic,
989890 VZDiskImageSynchronizationMode::Full,
990− )?;
891+ ).unwrap();
991892
992893 let disk_device = VZVirtioBlockDeviceConfiguration::initWithAttachment(
993894 VZVirtioBlockDeviceConfiguration::alloc(),
@@ -1074,7 +975,7 @@ fn create_vm_configuration(
1074975 // Validate
1075976 config.validateWithError().map_err(|e| {
1076977 io::Error::other(format!(
1077− "invalid VM configuration: {:?}",
978+ "Invalid VM configuration: {:?}",
1078979 e.localizedDescription()
1079980 ))
1080981 })?;
@@ -1083,9 +984,9 @@ fn create_vm_configuration(
1083984 }
1084985}
1085986
1086−fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
987+fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>, Box<dyn std::error::Error>> {
1087988 unsafe {
1088− let temp_dir = env::temp_dir();
989+ let temp_dir = std::env::temp_dir();
1089990 let temp_path = temp_dir.join(format!("efi_variable_store_{}.efivars", std::process::id()));
1090991 let url = nsurl_from_path(&temp_path)?;
1091992 let options = VZEFIVariableStoreInitializationOptions::AllowOverwrite;
@@ -1101,8 +1002,8 @@ fn load_efi_variable_store() -> Result<Retained<VZEFIVariableStore>> {
11011002fn spawn_login_actions_thread(
11021003 login_actions: Vec<LoginAction>,
11031004 output_monitor: Arc<OutputMonitor>,
1104− input_tx: Sender<VmInput>,
1105− vm_output_tx: Sender<VmOutput>,
1005+ input_tx: mpsc::Sender<VmInput>,
1006+ vm_output_tx: mpsc::Sender<VmOutput>,
11061007) -> thread::JoinHandle<()> {
11071008 thread::spawn(move || {
11081009 for a in login_actions {
@@ -1116,31 +1017,22 @@ fn spawn_login_actions_thread(
11161017 return;
11171018 }
11181019 }
1119− ExpectEither {
1120− success,
1121− failure,
1122− timeout,
1123− } => match output_monitor.wait_for_any(&[&success, &failure], timeout) {
1124− WaitAnyResult::Found(0) => {}
1125− WaitAnyResult::Found(_) => {
1126− let _ = vm_output_tx.send(VmOutput::LoginActionFailed {
1127− action: format!("expect '{}'", success),
1128− reason: format!("saw failure marker '{}'", failure),
1129− });
1130− return;
1131− }
1132− WaitAnyResult::Timeout => {
1133− let _ = vm_output_tx.send(VmOutput::LoginActionTimeout {
1134− action: format!("expect '{}'", success),
1135− timeout,
1136− });
1137− return;
1138− }
1139− },
11401020 Send(mut text) => {
11411021 text.push('\n'); // Type the newline so the command is actually submitted.
11421022 input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
11431023 }
1024+ Script { path, index } => {
1025+ let command = match script_command_from_path(&path, index) {
1026+ Ok(command) => command,
1027+ Err(err) => {
1028+ eprintln!("{err}");
1029+ return;
1030+ }
1031+ };
1032+ let mut text = command;
1033+ text.push('\n');
1034+ input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap();
1035+ }
11441036 }
11451037 }
11461038 })
@@ -1152,9 +1044,8 @@ fn run_vm_with_io<F>(
11521044 directory_shares: &[DirectoryShare],
11531045 cpu_count: usize,
11541046 ram_bytes: u64,
1155− status: Option<&StatusEmitter<'_>>,
11561047 io_handler: F,
1157−) -> Result<()>
1048+) -> Result<(), Box<dyn std::error::Error>>
11581049where
11591050 F: FnOnce(Arc<OutputMonitor>, OwnedFd, OwnedFd) -> IoContext,
11601051{
@@ -1201,23 +1092,21 @@ where
12011092
12021093 match rx.try_recv() {
12031094 Ok(result) => {
1204− result.map_err(|e| Error::msg(format!("Failed to start VM: {}", e)))?;
1095+ result.map_err(|e| format!("Failed to start VM: {}", e))?;
12051096 break;
12061097 }
12071098 Err(mpsc::TryRecvError::Empty) => continue,
12081099 Err(mpsc::TryRecvError::Disconnected) => {
1209− bail!("VM start channel disconnected");
1100+ return Err("VM start channel disconnected".into());
12101101 }
12111102 }
12121103 }
12131104
12141105 if Instant::now() >= start_deadline {
1215− bail!("Timed out waiting for VM to start");
1106+ return Err("Timed out waiting for VM to start".into());
12161107 }
12171108
1218− emit_status(status, "vm booting... go vibecoder!");
1219− tracing::info!("vm booting... go vibecoder!");
1220− tracing::info!("vm booting");
1109+ println!("VM booting...");
12211110
12221111 let output_monitor = Arc::new(OutputMonitor::default());
12231112 let io_ctx = io_handler(output_monitor.clone(), we_read_from, we_write_to);
@@ -1262,7 +1151,7 @@ where
12621151 );
12631152
12641153 let mut last_state = None;
1265− let mut exit_result: Result<(), String> = Ok(());
1154+ let mut exit_result = Ok(());
12661155 loop {
12671156 unsafe {
12681157 NSRunLoop::mainRunLoop().runMode_beforeDate(
@@ -1281,28 +1170,12 @@ where
12811170 exit_result = Err(format!(
12821171 "Login action ({}) timed out after {:?}; shutting down.",
12831172 action, timeout
1284− ));
1285− unsafe {
1286− if vm.canRequestStop() {
1287− if let Err(err) = vm.requestStopWithError() {
1288− tracing::error!(error = ?err, "failed to request VM stop");
1289− }
1290− } else if vm.canStop() {
1291− let handler = RcBlock::new(|_error: *mut NSError| {});
1292− vm.stopWithCompletionHandler(&handler);
1293− }
1294− }
1295− break;
1296− }
1297− Ok(VmOutput::LoginActionFailed { action, reason }) => {
1298− exit_result = Err(format!(
1299− "Login action ({}) failed: {}; shutting down.",
1300− action, reason
1301− ));
1173+ )
1174+ .into());
13021175 unsafe {
13031176 if vm.canRequestStop() {
13041177 if let Err(err) = vm.requestStopWithError() {
1305− tracing::error!(error = ?err, "failed to request VM stop");
1178+ eprintln!("Failed to request VM stop: {:?}", err);
13061179 }
13071180 } else if vm.canStop() {
13081181 let handler = RcBlock::new(|_error: *mut NSError| {});
@@ -1314,7 +1187,7 @@ where
13141187 Err(mpsc::TryRecvError::Empty) => {}
13151188 Err(mpsc::TryRecvError::Disconnected) => {}
13161189 }
1317− if state != VZVirtualMachineState::Running {
1190+ if state != objc2_virtualization::VZVirtualMachineState::Running {
13181191 //eprintln!("VM stopped with state: {:?}", state);
13191192 break;
13201193 }
@@ -1324,7 +1197,7 @@ where
13241197
13251198 io_ctx.shutdown();
13261199
1327− exit_result.map_err(Error::msg)
1200+ exit_result
13281201}
13291202
13301203fn run_vm(
@@ -1333,20 +1206,18 @@ fn run_vm(
13331206 directory_shares: &[DirectoryShare],
13341207 cpu_count: usize,
13351208 ram_bytes: u64,
1336− status: Option<&StatusEmitter<'_>>,
1337−) -> Result<()> {
1209+) -> Result<(), Box<dyn std::error::Error>> {
13381210 run_vm_with_io(
13391211 disk_path,
13401212 login_actions,
13411213 directory_shares,
13421214 cpu_count,
13431215 ram_bytes,
1344− status,
13451216 spawn_vm_io,
13461217 )
13471218}
13481219
1349−fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
1220+fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, Box<dyn std::error::Error>> {
13501221 let abs_path = if path.is_absolute() {
13511222 path.to_path_buf()
13521223 } else {
@@ -1355,7 +1226,7 @@ fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>> {
13551226 let ns_path = NSString::from_str(
13561227 abs_path
13571228 .to_str()
1358− .with_context(|| "non-UTF8 path encountered while building NSURL")?,
1229+ .ok_or("Non-UTF8 path encountered while building NSURL")?,
13591230 );
13601231 Ok(NSURL::fileURLWithPath(&ns_path))
13611232}
@@ -1370,7 +1241,7 @@ fn enable_raw_mode(fd: i32) -> io::Result<RawModeGuard> {
13701241 let original = attributes;
13711242
13721243 // Disable translation of carriage return to newline on input
1373− attributes.c_iflag &= !libc::ICRNL;
1244+ attributes.c_iflag &= !(libc::ICRNL);
13741245 // Disable canonical mode (line buffering), echo, and signal generation
13751246 attributes.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
13761247 attributes.c_cc[libc::VMIN] = 0;
@@ -1398,10 +1269,7 @@ impl Drop for RawModeGuard {
13981269
13991270// Ensure the running binary has com.apple.security.virtualization entitlements by checking and, if not, signing and relaunching.
14001271pub fn ensure_signed() {
1401− if env::var("VIBEBOX_SKIP_CODESIGN").as_deref() == Ok("1") {
1402− return;
1403− }
1404− let exe = env::current_exe().expect("failed to get current exe path");
1272+ let exe = std::env::current_exe().expect("failed to get current exe path");
14051273 let exe_str = exe.to_str().expect("exe path not valid utf-8");
14061274
14071275 let has_required_entitlements = {
@@ -1423,10 +1291,10 @@ pub fn ensure_signed() {
14231291 }
14241292
14251293 const ENTITLEMENTS: &str = include_str!("entitlements.plist");
1426− let entitlements_path = env::temp_dir().join("entitlements.plist");
1427− fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
1294+ let entitlements_path = std::env::temp_dir().join("entitlements.plist");
1295+ std::fs::write(&entitlements_path, ENTITLEMENTS).expect("failed to write entitlements");
14281296
1429− let output = Command::new("codesign")
1297+ let status = Command::new("codesign")
14301298 .args([
14311299 "--sign",
14321300 "-",
@@ -1435,27 +1303,22 @@ pub fn ensure_signed() {
14351303 entitlements_path.to_str().unwrap(),
14361304 exe_str,
14371305 ])
1438− .output();
1306+ .status();
14391307
1440− let _ = fs::remove_file(&entitlements_path);
1308+ let _ = std::fs::remove_file(&entitlements_path);
14411309
1442− match output {
1443− Ok(o) if o.status.success() => {
1444− let stderr = String::from_utf8_lossy(&o.stderr);
1445− if !stderr.trim().is_empty() {
1446− tracing::debug!(codesign_stderr = %stderr.trim(), "codesign output");
1447− }
1448− let err = Command::new(&exe).args(env::args_os().skip(1)).exec();
1449− tracing::error!(error = %err, "failed to re-exec after signing");
1310+ match status {
1311+ Ok(s) if s.success() => {
1312+ let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1313+ eprintln!("failed to re-exec after signing: {err}");
14501314 std::process::exit(1);
14511315 }
1452− Ok(o) => {
1453− let stderr = String::from_utf8_lossy(&o.stderr);
1454− tracing::error!(status = %o.status, codesign_stderr = %stderr.trim(), "codesign failed");
1316+ Ok(s) => {
1317+ eprintln!("codesign failed with status: {s}");
14551318 std::process::exit(1);
14561319 }
14571320 Err(e) => {
1458− tracing::error!(error = %e, "failed to run codesign");
1321+ eprintln!("failed to run codesign: {e}");
14591322 std::process::exit(1);
14601323 }
14611324 }