diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index 9044830..1d78f31 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -11,27 +11,25 @@ use tracing_subscriber::EnvFilter; use vibebox::tui::{AppState, VmInfo}; use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager}; -const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 30000; - fn main() -> Result<()> { init_tracing(); color_eyre::install()?; let raw_args: Vec = env::args_os().collect(); + let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; + tracing::info!(cwd = %cwd.display(), "starting vibebox cli"); + let config = config::load_config(&cwd); + if env::var("VIBEBOX_VM_MANAGER").as_deref() == Ok("1") { tracing::info!("starting vm manager mode"); - // TODO: wire CLI args into VmArg once we reintroduce CLI parsing. let args = vm::VmArg { - cpu_count: 2, - ram_bytes: 2048 * 1024 * 1024, + cpu_count: config.box_cfg.cpu_count, + ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), no_default_mounts: false, - mounts: Vec::new(), + mounts: config.box_cfg.mounts.clone(), }; - let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS); + let auto_shutdown_ms = config.supervisor.auto_shutdown_ms; tracing::info!(auto_shutdown_ms, "vm manager config"); if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) { tracing::error!(error = %err, "vm manager exited"); @@ -42,12 +40,11 @@ fn main() -> Result<()> { vm::ensure_signed(); - // TODO: wire CLI args into VmArg once we reintroduce CLI parsing. let vm_args = vm::VmArg { - cpu_count: 2, - ram_bytes: 2048 * 1024 * 1024, + cpu_count: config.box_cfg.cpu_count, + ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), no_default_mounts: false, - mounts: Vec::new(), + mounts: config.box_cfg.mounts.clone(), }; let vm_info = VmInfo { @@ -55,15 +52,10 @@ fn main() -> Result<()> { max_memory_mb: vm_args.ram_bytes / (1024 * 1024), cpu_cores: vm_args.cpu_count, }; - let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; - tracing::info!(cwd = %cwd.display(), "starting vibebox cli"); - let auto_shutdown_ms = config::load_config(&cwd) - .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))? - .auto_shutdown_ms - .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS); + let auto_shutdown_ms = config.supervisor.auto_shutdown_ms; if let Ok(manager) = SessionManager::new() { if let Err(err) = manager.update_global_sessions(&cwd) { - tracing::warn!(error = %err, "failed to update global session list"); + tracing::warn!(error = %err, "failed to update a global session list"); } } else { tracing::warn!("failed to initialize session manager"); diff --git a/src/bin/vibebox-supervisor.rs b/src/bin/vibebox-supervisor.rs index 584fa5f..25464c1 100644 --- a/src/bin/vibebox-supervisor.rs +++ b/src/bin/vibebox-supervisor.rs @@ -6,9 +6,7 @@ use std::{ use color_eyre::Result; use tracing_subscriber::EnvFilter; -use vibebox::{instance, vm, vm_manager}; - -const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000; +use vibebox::{config, instance, vm, vm_manager}; fn main() -> Result<()> { init_tracing(); @@ -16,20 +14,17 @@ fn main() -> Result<()> { tracing::info!("starting vm supervisor"); let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; + let config = config::load_config(&cwd); let instance_dir = instance::ensure_instance_dir(&cwd) .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; let _ = instance::touch_last_active(&instance_dir); - // TODO: wire CLI args into VmArg once we reintroduce CLI parsing. let args = vm::VmArg { - cpu_count: 2, - ram_bytes: 2048 * 1024 * 1024, + cpu_count: config.box_cfg.cpu_count, + ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024), no_default_mounts: false, - mounts: Vec::new(), + mounts: config.box_cfg.mounts.clone(), }; - let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS") - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS); + let auto_shutdown_ms = config.supervisor.auto_shutdown_ms; tracing::info!(auto_shutdown_ms, "vm supervisor config"); let result = vm_manager::run_manager(args, auto_shutdown_ms); diff --git a/src/config.rs b/src/config.rs index 2c6a88e..6a66ff0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,13 +3,72 @@ use std::{ path::{Path, PathBuf}, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; + +use crate::vm::DirectoryShare; pub const CONFIG_FILENAME: &str = "vibebox.toml"; -#[derive(Debug, Default, Deserialize)] -pub struct ProjectConfig { - pub auto_shutdown_ms: Option, +const DEFAULT_CPU_COUNT: usize = 2; +const DEFAULT_RAM_MB: u64 = 2048; +const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(rename = "box")] + pub box_cfg: BoxConfig, + pub supervisor: SupervisorConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + box_cfg: BoxConfig::default(), + supervisor: SupervisorConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BoxConfig { + pub cpu_count: usize, + pub ram_mb: u64, + pub mounts: Vec, +} + +impl Default for BoxConfig { + fn default() -> Self { + Self { + cpu_count: default_cpu_count(), + ram_mb: default_ram_mb(), + mounts: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SupervisorConfig { + pub auto_shutdown_ms: u64, +} + +impl Default for SupervisorConfig { + fn default() -> Self { + Self { + auto_shutdown_ms: default_auto_shutdown_ms(), + } + } +} + +fn default_cpu_count() -> usize { + DEFAULT_CPU_COUNT +} + +fn default_ram_mb() -> u64 { + DEFAULT_RAM_MB +} + +fn default_auto_shutdown_ms() -> u64 { + DEFAULT_AUTO_SHUTDOWN_MS } pub fn config_path(project_root: &Path) -> PathBuf { @@ -19,20 +78,147 @@ pub fn config_path(project_root: &Path) -> PathBuf { pub fn ensure_config_file(project_root: &Path) -> Result { let path = config_path(project_root); if !path.exists() { - fs::write(&path, "")?; + let default_config = Config::default(); + let contents = toml::to_string_pretty(&default_config).unwrap_or_default(); + fs::write(&path, contents)?; tracing::info!(path = %path.display(), "created vibebox config"); } Ok(path) } -pub fn load_config( - project_root: &Path, -) -> Result> { - let path = ensure_config_file(project_root)?; - let raw = fs::read_to_string(&path)?; +pub fn load_config(project_root: &Path) -> Config { + let path = match ensure_config_file(project_root) { + Ok(path) => path, + Err(err) => die(&format!("failed to create config: {err}")), + }; + let raw = match fs::read_to_string(&path) { + Ok(raw) => raw, + Err(err) => die(&format!("failed to read config: {err}")), + }; + let trimmed = raw.trim(); tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config"); - if raw.trim().is_empty() { - return Ok(ProjectConfig::default()); + if trimmed.is_empty() { + 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)", + path.display() + )); } - Ok(toml::from_str::(&raw)?) + + let value: toml::Value = match toml::from_str(trimmed) { + Ok(value) => value, + Err(err) => die(&format!("invalid config: {err}")), + }; + let schema_errors = validate_schema(&value); + if !schema_errors.is_empty() { + let message = format!( + "config file ({}) is missing or invalid fields:\n- {}", + path.display(), + schema_errors.join("\n- ") + ); + die(&message); + } + + let config: Config = match toml::from_str(trimmed) { + Ok(config) => config, + Err(err) => die(&format!("invalid config: {err}")), + }; + validate_or_exit(&config); + config +} + +fn validate_schema(value: &toml::Value) -> Vec { + let mut errors = Vec::new(); + let root = match value.as_table() { + Some(table) => table, + None => { + errors.push("config must be a table".to_string()); + return errors; + } + }; + + match root.get("box") { + None => errors.push("missing [box] table".to_string()), + Some(value) => match value.as_table() { + Some(table) => { + validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors); + validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors); + validate_string_array( + table, + "mounts", + "[box].mounts (array of strings)", + &mut errors, + ); + } + None => errors.push("[box] must be a table".to_string()), + }, + } + + match root.get("supervisor") { + None => errors.push("missing [supervisor] table".to_string()), + Some(value) => match value.as_table() { + Some(table) => { + validate_int( + table, + "auto_shutdown_ms", + "[supervisor].auto_shutdown_ms (integer)", + &mut errors, + ); + } + None => errors.push("[supervisor] must be a table".to_string()), + }, + } + + errors +} + +fn validate_int(table: &toml::value::Table, key: &str, label: &str, errors: &mut Vec) { + match table.get(key) { + None => errors.push(format!("missing {label}")), + Some(value) => { + if value.as_integer().is_none() { + errors.push(format!("invalid {label}: expected integer")); + } + } + } +} + +fn validate_string_array( + table: &toml::value::Table, + key: &str, + label: &str, + errors: &mut Vec, +) { + match table.get(key) { + None => errors.push(format!("missing {label}")), + Some(value) => match value.as_array() { + Some(values) => { + if values.iter().any(|value| !value.is_str()) { + errors.push(format!("invalid {label}: expected array of strings")); + } + } + None => errors.push(format!("invalid {label}: expected array of strings")), + }, + } +} + +fn validate_or_exit(config: &Config) { + if config.box_cfg.cpu_count == 0 { + die("box.cpu_count must be >= 1"); + } + if config.box_cfg.ram_mb == 0 { + die("box.ram_mb must be >= 1"); + } + if config.supervisor.auto_shutdown_ms == 0 { + die("supervisor.auto_shutdown_ms must be >= 1"); + } + for spec in &config.box_cfg.mounts { + if let Err(err) = DirectoryShare::from_mount_spec(spec) { + die(&format!("invalid mount spec '{spec}': {err}")); + } + } +} + +fn die(message: &str) -> ! { + eprintln!("[vibebox] {message}"); + std::process::exit(1); } diff --git a/vibebox.toml b/vibebox.toml index e69de29..86f0a63 100644 --- a/vibebox.toml +++ b/vibebox.toml @@ -0,0 +1,7 @@ +[box] +cpu_count = 2 +ram_mb = 2048 +mounts = [] + +[supervisor] +auto_shutdown_ms = 20000