11 Commits

Author SHA1 Message Date
robcholz ad8fb139de feat: new version 2026-02-08 00:38:21 -05:00
robcholz 5e008c9471 fix: fixed the annoying dhcp conflict when having concurrent vm 2026-02-08 00:34:01 -05:00
robcholz 935e0cd566 fix: mise install fail will not fail the ssh 2026-02-08 00:32:29 -05:00
robcholz c53a6eff51 ci: added tests 2026-02-07 23:58:10 -05:00
robcholz 4dd9c31f74 refactor: now code and claude becomes default mounts 2026-02-07 23:50:25 -05:00
robcholz 9dd88dd304 fix: update to v0.2.0 2026-02-07 23:25:26 -05:00
Finn Sheng 4311168fdd Merge pull request #4 from robcholz/disk-support
Disk support
2026-02-07 23:22:59 -05:00
robcholz a4b9d62873 feat: now added warn for disk size mismatch 2026-02-07 23:16:12 -05:00
robcholz 727adffb4c feat: added disk support 2026-02-07 23:05:52 -05:00
robcholz 75adb1696a refactor: make install noninteractive terminal friendly 2026-02-07 22:25:26 -05:00
robcholz 57ebe7ffee refactor: added banner source 2026-02-07 22:14:20 -05:00
14 changed files with 239 additions and 52 deletions
+12
View File
@@ -54,3 +54,15 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: cargo build - name: cargo build
run: cargo build --locked run: cargo build --locked
test:
name: Test
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- name: cargo test
run: cargo test --locked
Generated
+1 -1
View File
@@ -1238,7 +1238,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vibebox" name = "vibebox"
version = "0.1.1" version = "0.2.1"
dependencies = [ dependencies = [
"block2", "block2",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vibebox" name = "vibebox"
version = "0.1.1" version = "0.2.1"
edition = "2024" edition = "2024"
authors = ["Finn Sheng"] authors = ["Finn Sheng"]
description = "Ultrafast CLI on Apple Silicon macOS for fast, sandboxed development and LLM agents." description = "Ultrafast CLI on Apple Silicon macOS for fast, sandboxed development and LLM agents."
+26 -22
View File
@@ -47,36 +47,40 @@ ensure_cargo() {
print_message warning "Rust (cargo) is required but not found." print_message warning "Rust (cargo) is required but not found."
print_message info "You should review and approve the Rust installer before proceeding." print_message info "You should review and approve the Rust installer before proceeding."
reply=""
if [[ -t 0 ]]; then if [[ -t 0 ]]; then
read -r -p "Install Rust using rustup now? (y/N) " reply read -r -p "Install Rust using rustup now? (y/N) " reply
case "${reply}" in elif [[ -r /dev/tty ]]; then
y|Y) read -r -p "Install Rust using rustup now? (y/N) " reply </dev/tty
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs | sh -s -- -y
else
print_message error "Missing required command: curl or wget"
print_message info "Install Rust manually from https://rustup.rs and retry."
exit 1
fi
# shellcheck source=/dev/null
if [[ -f "$HOME/.cargo/env" ]]; then
# shellcheck disable=SC1090
source "$HOME/.cargo/env"
fi
;;
*)
print_message info "Install Rust manually from https://rustup.rs and retry."
exit 1
;;
esac
else else
print_message error "Non-interactive shell: cannot prompt to install Rust." print_message error "Non-interactive shell: cannot prompt to install Rust."
print_message info "Install Rust manually from https://rustup.rs and retry." print_message info "Install Rust manually from https://rustup.rs and retry."
exit 1 exit 1
fi fi
case "${reply}" in
y|Y)
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs | sh -s -- -y
else
print_message error "Missing required command: curl or wget"
print_message info "Install Rust manually from https://rustup.rs and retry."
exit 1
fi
# shellcheck source=/dev/null
if [[ -f "$HOME/.cargo/env" ]]; then
# shellcheck disable=SC1090
source "$HOME/.cargo/env"
fi
;;
*)
print_message info "Install Rust manually from https://rustup.rs and retry."
exit 1
;;
esac
if ! command -v cargo >/dev/null 2>&1; then if ! command -v cargo >/dev/null 2>&1; then
print_message error "Cargo still not available after installation." print_message error "Cargo still not available after installation."
print_message info "Open a new shell or run: source \"$HOME/.cargo/env\"" print_message info "Open a new shell or run: source \"$HOME/.cargo/env\""
+1
View File
@@ -29,6 +29,7 @@ fn main() -> Result<()> {
let args = vm::VmArg { let args = vm::VmArg {
cpu_count: config.box_cfg.cpu_count, cpu_count: config.box_cfg.cpu_count,
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
no_default_mounts: false, no_default_mounts: false,
mounts: config.box_cfg.mounts.clone(), mounts: config.box_cfg.mounts.clone(),
}; };
+27 -3
View File
@@ -49,7 +49,7 @@ fn main() -> Result<()> {
let stderr_handle = init_tracing(&cwd); let stderr_handle = init_tracing(&cwd);
let cli = Cli::parse(); let cli = Cli::parse();
tracing::info!(cwd = %cwd.display(), "starting vibebox cli"); tracing::debug!(cwd = %cwd.display(), "starting vibebox cli");
if let Some(command) = cli.command { if let Some(command) = cli.command {
return handle_command(command, &cwd, cli.config.as_deref()); return handle_command(command, &cwd, cli.config.as_deref());
} }
@@ -63,6 +63,7 @@ fn main() -> Result<()> {
let args = vm::VmArg { let args = vm::VmArg {
cpu_count: config.box_cfg.cpu_count, cpu_count: config.box_cfg.cpu_count,
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
no_default_mounts: false, no_default_mounts: false,
mounts: config.box_cfg.mounts.clone(), mounts: config.box_cfg.mounts.clone(),
}; };
@@ -80,14 +81,15 @@ fn main() -> Result<()> {
let vm_args = vm::VmArg { let vm_args = vm::VmArg {
cpu_count: config.box_cfg.cpu_count, cpu_count: config.box_cfg.cpu_count,
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
no_default_mounts: false, no_default_mounts: false,
mounts: config.box_cfg.mounts.clone(), mounts: config.box_cfg.mounts.clone(),
}; };
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms; let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
let vm_info = VmInfo { let vm_info = VmInfo {
max_memory_mb: vm_args.ram_bytes / (1024 * 1024), max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
cpu_cores: vm_args.cpu_count, cpu_cores: vm_args.cpu_count,
max_disk_gb: (vm_args.disk_bytes as f32) / 1024.0 / 1024.0 / 1024.0,
system_name: "Debian".to_string(), // TODO: read system name from the VM. system_name: "Debian".to_string(), // TODO: read system name from the VM.
auto_shutdown_ms, auto_shutdown_ms,
}; };
@@ -110,6 +112,7 @@ fn main() -> Result<()> {
writeln!(stdout)?; writeln!(stdout)?;
stdout.flush()?; stdout.flush()?;
} }
warn_disk_size_mismatch(&cwd, vm_args.disk_bytes);
if let Some(handle) = stderr_handle { if let Some(handle) = stderr_handle {
let _ = handle.modify(|filter| *filter = LevelFilter::INFO); let _ = handle.modify(|filter| *filter = LevelFilter::INFO);
} }
@@ -351,6 +354,27 @@ fn format_last_active(value: Option<&str>) -> String {
format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
} }
fn warn_disk_size_mismatch(cwd: &Path, configured_bytes: u64) {
let instance_raw = cwd
.join(session_manager::INSTANCE_DIR_NAME)
.join("instance.raw");
let Ok(meta) = fs::metadata(&instance_raw) else {
return;
};
let current_bytes = meta.len();
if current_bytes == configured_bytes {
return;
}
let current_gb = current_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let target_gb = configured_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
tracing::warn!(
"instance disk size does not match config (current {:.2} GB, config {:.2} GB). \
disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using the existing disk.",
current_gb,
target_gb
);
}
type StderrHandle = reload::Handle<LevelFilter, Registry>; type StderrHandle = reload::Handle<LevelFilter, Registry>;
fn init_tracing(cwd: &Path) -> Option<StderrHandle> { fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
@@ -371,7 +395,7 @@ fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
}); });
if stderr_is_tty { if stderr_is_tty {
let (stderr_filter, handle) = reload::Layer::new(LevelFilter::OFF); let (stderr_filter, handle) = reload::Layer::new(LevelFilter::INFO);
let stderr_layer = fmt::layer() let stderr_layer = fmt::layer()
.with_target(false) .with_target(false)
.with_ansi(ansi) .with_ansi(ansi)
+16 -2
View File
@@ -13,6 +13,7 @@ pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
const DEFAULT_CPU_COUNT: usize = 2; const DEFAULT_CPU_COUNT: usize = 2;
const DEFAULT_RAM_MB: u64 = 2048; const DEFAULT_RAM_MB: u64 = 2048;
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000; const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
const DEFAULT_DISK_GB: u64 = 5;
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config { pub struct Config {
@@ -25,6 +26,7 @@ pub struct Config {
pub struct BoxConfig { pub struct BoxConfig {
pub cpu_count: usize, pub cpu_count: usize,
pub ram_mb: u64, pub ram_mb: u64,
pub disk_gb: u64,
pub mounts: Vec<String>, pub mounts: Vec<String>,
} }
@@ -33,6 +35,7 @@ impl Default for BoxConfig {
Self { Self {
cpu_count: default_cpu_count(), cpu_count: default_cpu_count(),
ram_mb: default_ram_mb(), ram_mb: default_ram_mb(),
disk_gb: default_disk_gb(),
mounts: default_mounts(), mounts: default_mounts(),
} }
} }
@@ -64,7 +67,14 @@ fn default_auto_shutdown_ms() -> u64 {
} }
fn default_mounts() -> Vec<String> { fn default_mounts() -> Vec<String> {
Vec::new() vec![
"~/.codex:~/.codex:read-write".into(),
"~/.claude:~/.claude:read-write".into(),
]
}
fn default_disk_gb() -> u64 {
DEFAULT_DISK_GB
} }
pub fn config_path(project_root: &Path) -> PathBuf { pub fn config_path(project_root: &Path) -> PathBuf {
@@ -102,7 +112,7 @@ pub fn load_config_with_path(project_root: &Path, override_path: Option<&Path>)
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config"); tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
if trimmed.is_empty() { if trimmed.is_empty() {
die(&format!( die(&format!(
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)", "config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].disk_gb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
path.display() path.display()
)); ));
} }
@@ -193,6 +203,7 @@ fn validate_schema(value: &toml::Value) -> Vec<String> {
Some(table) => { Some(table) => {
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors); validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors); validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
validate_int(table, "disk_gb", "[box].disk_gb (integer)", &mut errors);
validate_string_array( validate_string_array(
table, table,
"mounts", "mounts",
@@ -259,6 +270,9 @@ fn validate_or_exit(config: &Config) {
if config.box_cfg.ram_mb == 0 { if config.box_cfg.ram_mb == 0 {
die("box.ram_mb must be >= 1"); die("box.ram_mb must be >= 1");
} }
if config.box_cfg.disk_gb == 0 {
die("box.disk_gb must be >= 1");
}
if config.supervisor.auto_shutdown_ms == 0 { if config.supervisor.auto_shutdown_ms == 0 {
die("supervisor.auto_shutdown_ms must be >= 1"); die("supervisor.auto_shutdown_ms must be >= 1");
} }
+6
View File
@@ -14,6 +14,7 @@ apt-get install -y --no-install-recommends \
curl \ curl \
git \ git \
ripgrep \ ripgrep \
cloud-guest-utils \
openssh-server \ openssh-server \
sudo sudo
@@ -42,6 +43,11 @@ systemctl restart ssh
# Set this env var so claude doesn't complain about running as root.' # Set this env var so claude doesn't complain about running as root.'
echo "export IS_SANDBOX=1" >> .bashrc echo "export IS_SANDBOX=1" >> .bashrc
# Ensure cloned instances generate unique machine-id on first boot.
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -sf /etc/machine-id /var/lib/dbus/machine-id
# Shutdown the VM when you logout # Shutdown the VM when you logout
cat > .bash_logout <<EOF cat > .bash_logout <<EOF
systemctl poweroff systemctl poweroff
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
set -euo pipefail
ROOT_DEV="$(findmnt / -n -o SOURCE || true)"
ROOT_FSTYPE="$(findmnt / -n -o FSTYPE || true)"
if [ -z "$ROOT_DEV" ]; then
exit 0
fi
DISK_DEV=""
PART_NUM=""
if command -v lsblk >/dev/null 2>&1; then
DISK_DEV="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
PART_NUM="$(lsblk -no PARTNUM "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
fi
if [ -z "$DISK_DEV" ] || [ -z "$PART_NUM" ]; then
ROOT_BASENAME="$(basename "$ROOT_DEV")"
if echo "$ROOT_BASENAME" | grep -Eq '^nvme.+p[0-9]+$'; then
DISK_DEV="/dev/${ROOT_BASENAME%p[0-9]*}"
PART_NUM="${ROOT_BASENAME##*p}"
elif echo "$ROOT_BASENAME" | grep -Eq '^[a-z]+[0-9]+$'; then
DISK_DEV="/dev/${ROOT_BASENAME%%[0-9]*}"
PART_NUM="${ROOT_BASENAME##*[a-z]}"
fi
fi
if [ -n "$DISK_DEV" ] && [ -n "$PART_NUM" ]; then
if command -v growpart >/dev/null 2>&1; then
growpart "$DISK_DEV" "$PART_NUM" || true
elif command -v sfdisk >/dev/null 2>&1; then
sfdisk -N "$PART_NUM" --force "$DISK_DEV" <<'EOF' || true
,,
EOF
fi
fi
if command -v partprobe >/dev/null 2>&1; then
partprobe "$DISK_DEV" || true
fi
case "$ROOT_FSTYPE" in
ext4|ext3|ext2)
if command -v resize2fs >/dev/null 2>&1; then
resize2fs "$ROOT_DEV" || true
fi
;;
xfs)
if command -v xfs_growfs >/dev/null 2>&1; then
xfs_growfs / || true
fi
;;
esac
+2 -2
View File
@@ -117,7 +117,7 @@ impl SessionManager {
} }
if removed > 0 || added { if removed > 0 || added {
tracing::info!( tracing::debug!(
path = %self.sessions_dir.display(), path = %self.sessions_dir.display(),
removed, removed,
added, added,
@@ -196,7 +196,7 @@ impl SessionManager {
let path = self.session_path_for(&record.id); let path = self.session_path_for(&record.id);
let content = toml::to_string_pretty(record)?; let content = toml::to_string_pretty(record)?;
atomic_write(&path, content.as_bytes())?; atomic_write(&path, content.as_bytes())?;
tracing::info!( tracing::debug!(
path = %path.display(), path = %path.display(),
"wrote session record" "wrote session record"
); );
+34 -13
View File
@@ -88,16 +88,22 @@ fi
# Install Mise # Install Mise
MISE_BIN="${USER_HOME}/.local/bin/mise" MISE_BIN="${USER_HOME}/.local/bin/mise"
if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then mise_warn() { echo "[mise] $*" >&2; }
curl https://mise.run | HOME="$USER_HOME" sh mise_ok() { command -v mise >/dev/null 2>&1 || [ -x "$MISE_BIN" ]; }
fi mise_install() {
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc" if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then
if ! curl https://mise.run | HOME="$USER_HOME" sh; then
mise_warn "mise install script failed (continuing)"
return 0
fi
fi
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc"
export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH" export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH"
mkdir -p "${USER_HOME}/.config/mise" mkdir -p "${USER_HOME}/.config/mise"
cat > "${USER_HOME}/.config/mise/config.toml" <<MISE cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
[settings] [settings]
# Always use the venv created by uv, if available in directory # Always use the venv created by uv, if available in directory
python.uv_venv_auto = true python.uv_venv_auto = true
@@ -111,12 +117,21 @@ cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
"npm:@anthropic-ai/claude-code" = "latest" "npm:@anthropic-ai/claude-code" = "latest"
MISE MISE
touch "${USER_HOME}/.config/mise/mise.lock" touch "${USER_HOME}/.config/mise/mise.lock"
if [ -x "$MISE_BIN" ]; then if [ -x "$MISE_BIN" ]; then
HOME="$USER_HOME" "$MISE_BIN" install if ! HOME="$USER_HOME" "$MISE_BIN" install; then
else mise_warn "mise install failed (continuing)"
HOME="$USER_HOME" mise install return 0
fi fi
else
if ! HOME="$USER_HOME" mise install; then
mise_warn "mise install failed (continuing)"
return 0
fi
fi
}
mise_install || true
# 3) start ssh (don't swallow failures) # 3) start ssh (don't swallow failures)
# If ssh is already active, don't force start/restart. # If ssh is already active, don't force start/restart.
@@ -184,5 +199,11 @@ if ! listens_ok; then
exit 1 exit 1
fi fi
ip a
ip link
curl -s https://api.ipify.org ; echo
cat /etc/machine-id
echo VIBEBOX_SSH_READY echo VIBEBOX_SSH_READY
echo "VIBEBOX_IPV4=$ip" echo "VIBEBOX_IPV4=$ip"
+5 -3
View File
@@ -25,6 +25,7 @@ use ratatui::{
use crate::vm; use crate::vm;
// https://patorjk.com/software/taag/#p=display&f=ANSI+Shadow&t=VIBEBOX&x=none&v=4&h=4&w=80&we=false
const ASCII_BANNER: [&str; 7] = [ const ASCII_BANNER: [&str; 7] = [
"██╗ ██╗██╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗", "██╗ ██╗██╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗",
"██║ ██║██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗╚██╗██╔╝", "██║ ██║██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗╚██╗██╔╝",
@@ -40,6 +41,7 @@ const INFO_LINE_COUNT: u16 = 5;
pub struct VmInfo { pub struct VmInfo {
pub max_memory_mb: u64, pub max_memory_mb: u64,
pub cpu_cores: usize, pub cpu_cores: usize,
pub max_disk_gb: f32,
pub system_name: String, pub system_name: String,
pub auto_shutdown_ms: u64, pub auto_shutdown_ms: u64,
} }
@@ -569,11 +571,11 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)), Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
]), ]),
Line::from(vec![ Line::from(vec![
Span::raw("CPU / Memory: "), Span::raw("CPU / Memory / Disk: "),
Span::styled( Span::styled(
format!( format!(
"{} cores / {} MB", "{} cores / {} MB / {} GB",
app.vm_info.cpu_cores, app.vm_info.max_memory_mb app.vm_info.cpu_cores, app.vm_info.max_memory_mb, app.vm_info.max_disk_gb
), ),
Style::default().fg(Color::Green), Style::default().fg(Color::Green),
), ),
+52 -5
View File
@@ -74,6 +74,7 @@ impl Drop for StatusFile {
} }
const PROVISION_SCRIPT: &str = include_str!("provision.sh"); const PROVISION_SCRIPT: &str = include_str!("provision.sh");
const PROVISION_SCRIPT_NAME: &str = "provision.sh"; const PROVISION_SCRIPT_NAME: &str = "provision.sh";
const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
const DEFAULT_RAW_NAME: &str = "default.raw"; const DEFAULT_RAW_NAME: &str = "default.raw";
const INSTANCE_RAW_NAME: &str = "instance.raw"; const INSTANCE_RAW_NAME: &str = "instance.raw";
const BASE_DISK_RAW_NAME: &str = "disk.raw"; const BASE_DISK_RAW_NAME: &str = "disk.raw";
@@ -166,6 +167,7 @@ fn expand_tilde_path(value: &str) -> PathBuf {
pub struct VmArg { pub struct VmArg {
pub cpu_count: usize, pub cpu_count: usize,
pub ram_bytes: u64, pub ram_bytes: u64,
pub disk_bytes: u64,
pub no_default_mounts: bool, pub no_default_mounts: bool,
pub mounts: Vec<String>, pub mounts: Vec<String>,
} }
@@ -230,7 +232,15 @@ where
std::slice::from_ref(&mise_directory_share), std::slice::from_ref(&mise_directory_share),
Some(&status_file), Some(&status_file),
)?; )?;
ensure_instance_disk(&instance_raw, &default_raw, Some(&status_file))?; let _ = ensure_instance_disk(
&instance_raw,
&default_raw,
args.disk_bytes,
Some(&status_file),
)?;
let base_size = fs::metadata(&default_raw)?.len();
let instance_size = fs::metadata(&instance_raw)?.len();
let needs_resize = instance_size > base_size;
let disk_path = instance_raw; let disk_path = instance_raw;
let mut login_actions = Vec::new(); let mut login_actions = Vec::new();
@@ -263,6 +273,11 @@ where
directory_shares.push(DirectoryShare::from_mount_spec(spec)?); directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
} }
if needs_resize {
let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
login_actions.push(Send(resize_cmd));
}
if let Some(motd_action) = motd_login_action(&directory_shares) { if let Some(motd_action) = motd_login_action(&directory_shares) {
login_actions.push(motd_action); login_actions.push(motd_action);
} }
@@ -559,19 +574,51 @@ fn ensure_default_image(
fn ensure_instance_disk( fn ensure_instance_disk(
instance_raw: &Path, instance_raw: &Path,
template_raw: &Path, template_raw: &Path,
target_bytes: u64,
status: Option<&StatusFile>, status: Option<&StatusFile>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<bool, Box<dyn std::error::Error>> {
if instance_raw.exists() { if instance_raw.exists() {
return Ok(()); let current_size = fs::metadata(instance_raw)?.len();
if current_size != target_bytes {
let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
tracing::warn!(
current_bytes = current_size,
target_bytes,
"instance disk size does not match config (current {:.2} GB, config {:.2} GB); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
current_gb,
target_gb
);
}
return Ok(false);
} }
let template_size = fs::metadata(template_raw)?.len();
if target_bytes < template_size {
return Err(format!(
"Requested disk size {} bytes is smaller than base image size {} bytes",
target_bytes, template_size
)
.into());
}
let target_size = target_bytes;
let needs_resize = target_size > template_size;
if let Some(status) = status { if let Some(status) = status {
status.update("creating instance disk..."); status.update("creating instance disk...");
} }
tracing::info!(path = %template_raw.display(), "creating instance disk"); tracing::info!(path = %template_raw.display(), "creating instance disk");
std::fs::create_dir_all(instance_raw.parent().unwrap())?; std::fs::create_dir_all(instance_raw.parent().unwrap())?;
fs::copy(template_raw, instance_raw)?; if target_size == template_size {
Ok(()) fs::copy(template_raw, instance_raw)?;
return Ok(needs_resize);
}
let mut dst = std::fs::File::create(instance_raw)?;
dst.set_len(target_size)?;
let mut src = std::fs::File::open(template_raw)?;
std::io::copy(&mut src, &mut dst)?;
Ok(needs_resize)
} }
pub struct IoContext { pub struct IoContext {
+1
View File
@@ -1,6 +1,7 @@
[box] [box]
cpu_count = 2 cpu_count = 2
ram_mb = 2048 ram_mb = 2048
disk_gb = 5
mounts = [ mounts = [
"~/.codex:~/.codex:read-write", "~/.codex:~/.codex:read-write",
"~/.claude:~/.claude:read-write", "~/.claude:~/.claude:read-write",