on main help sign in
feat: add an actual config file.
57798d2robcholz
src/bin/vibebox-cli.rs+13−21
@@ -11,27 +11,25 @@ use tracing_subscriber::EnvFilter;
11use vibebox::tui::{AppState, VmInfo};
11use vibebox::tui::{AppState, VmInfo};
12use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager};
12use vibebox::{SessionManager, commands, config, instance, tui, vm, vm_manager};
13
13
14−const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 30000;
15−
16fn main() -> Result<()> {
14fn main() -> Result<()> {
17 init_tracing();
15 init_tracing();
18 color_eyre::install()?;
16 color_eyre::install()?;
19
17
20 let raw_args: Vec<OsString> = env::args_os().collect();
18 let raw_args: Vec<OsString> = env::args_os().collect();
21
19
20+ let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
21+ tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
22+ let config = config::load_config(&cwd);
23+
22 if env::var("VIBEBOX_VM_MANAGER").as_deref() == Ok("1") {
24 if env::var("VIBEBOX_VM_MANAGER").as_deref() == Ok("1") {
23 tracing::info!("starting vm manager mode");
25 tracing::info!("starting vm manager mode");
24− // TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
25 let args = vm::VmArg {
26 let args = vm::VmArg {
26− cpu_count: 2,
27+ cpu_count: config.box_cfg.cpu_count,
27− ram_bytes: 2048 * 1024 * 1024,
28+ ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
28 no_default_mounts: false,
29 no_default_mounts: false,
29− mounts: Vec::new(),
30+ mounts: config.box_cfg.mounts.clone(),
30 };
31 };
31− let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
32+ let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
32− .ok()
33− .and_then(|value| value.parse::<u64>().ok())
34− .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
35 tracing::info!(auto_shutdown_ms, "vm manager config");
33 tracing::info!(auto_shutdown_ms, "vm manager config");
36 if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) {
34 if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) {
37 tracing::error!(error = %err, "vm manager exited");
35 tracing::error!(error = %err, "vm manager exited");
@@ -42,12 +40,11 @@ fn main() -> Result<()> {
42
40
43 vm::ensure_signed();
41 vm::ensure_signed();
44
42
45− // TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
46 let vm_args = vm::VmArg {
43 let vm_args = vm::VmArg {
47− cpu_count: 2,
44+ cpu_count: config.box_cfg.cpu_count,
48− ram_bytes: 2048 * 1024 * 1024,
45+ ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
49 no_default_mounts: false,
46 no_default_mounts: false,
50− mounts: Vec::new(),
47+ mounts: config.box_cfg.mounts.clone(),
51 };
48 };
52
49
53 let vm_info = VmInfo {
50 let vm_info = VmInfo {
@@ -55,15 +52,10 @@ fn main() -> Result<()> {
55 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
52 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
56 cpu_cores: vm_args.cpu_count,
53 cpu_cores: vm_args.cpu_count,
57 };
54 };
58− let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
59− tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
60− let auto_shutdown_ms = config::load_config(&cwd)
55+ let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
61− .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?
62− .auto_shutdown_ms
63− .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
64 if let Ok(manager) = SessionManager::new() {
56 if let Ok(manager) = SessionManager::new() {
65 if let Err(err) = manager.update_global_sessions(&cwd) {
57 if let Err(err) = manager.update_global_sessions(&cwd) {
66− tracing::warn!(error = %err, "failed to update global session list");
58+ tracing::warn!(error = %err, "failed to update a global session list");
67 }
59 }
68 } else {
60 } else {
69 tracing::warn!("failed to initialize session manager");
61 tracing::warn!("failed to initialize session manager");
src/bin/vibebox-supervisor.rs+6−11
@@ -6,9 +6,7 @@ use std::{
6use color_eyre::Result;
6use color_eyre::Result;
7use tracing_subscriber::EnvFilter;
7use tracing_subscriber::EnvFilter;
8
8
9−use vibebox::{instance, vm, vm_manager};
9+use vibebox::{config, instance, vm, vm_manager};
10−
11−const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000;
12
10
13fn main() -> Result<()> {
11fn main() -> Result<()> {
14 init_tracing();
12 init_tracing();
@@ -16,20 +14,17 @@ fn main() -> Result<()> {
16
14
17 tracing::info!("starting vm supervisor");
15 tracing::info!("starting vm supervisor");
18 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
16 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
17+ let config = config::load_config(&cwd);
19 let instance_dir = instance::ensure_instance_dir(&cwd)
18 let instance_dir = instance::ensure_instance_dir(&cwd)
20 .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
19 .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
21 let _ = instance::touch_last_active(&instance_dir);
20 let _ = instance::touch_last_active(&instance_dir);
22− // TODO: wire CLI args into VmArg once we reintroduce CLI parsing.
23 let args = vm::VmArg {
21 let args = vm::VmArg {
24− cpu_count: 2,
22+ cpu_count: config.box_cfg.cpu_count,
25− ram_bytes: 2048 * 1024 * 1024,
23+ ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
26 no_default_mounts: false,
24 no_default_mounts: false,
27− mounts: Vec::new(),
25+ mounts: config.box_cfg.mounts.clone(),
28 };
26 };
29− let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
27+ let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
30− .ok()
31− .and_then(|value| value.parse::<u64>().ok())
32− .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
33 tracing::info!(auto_shutdown_ms, "vm supervisor config");
28 tracing::info!(auto_shutdown_ms, "vm supervisor config");
34
29
35 let result = vm_manager::run_manager(args, auto_shutdown_ms);
30 let result = vm_manager::run_manager(args, auto_shutdown_ms);
src/config.rs+199−13
@@ -3,13 +3,72 @@ use std::{
3 path::{Path, PathBuf},
3 path::{Path, PathBuf},
4};
4};
5
5
6−use serde::Deserialize;
6+use serde::{Deserialize, Serialize};
7+
8+use crate::vm::DirectoryShare;
7
9
8pub const CONFIG_FILENAME: &str = "vibebox.toml";
10pub const CONFIG_FILENAME: &str = "vibebox.toml";
9
11
10−#[derive(Debug, Default, Deserialize)]
12+const DEFAULT_CPU_COUNT: usize = 2;
13+const DEFAULT_RAM_MB: u64 = 2048;
14+const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
15+
16+#[derive(Debug, Clone, Serialize, Deserialize)]
17+pub struct Config {
18+ #[serde(rename = "box")]
19+ pub box_cfg: BoxConfig,
20+ pub supervisor: SupervisorConfig,
21+}
22+
23+impl Default for Config {
24+ fn default() -> Self {
25+ Self {
26+ box_cfg: BoxConfig::default(),
27+ supervisor: SupervisorConfig::default(),
28+ }
29+ }
30+}
31+
32+#[derive(Debug, Clone, Serialize, Deserialize)]
11−pub struct ProjectConfig {
33+pub struct BoxConfig {
12− pub auto_shutdown_ms: Option<u64>,
34+ pub cpu_count: usize,
35+ pub ram_mb: u64,
36+ pub mounts: Vec<String>,
37+}
38+
39+impl Default for BoxConfig {
40+ fn default() -> Self {
41+ Self {
42+ cpu_count: default_cpu_count(),
43+ ram_mb: default_ram_mb(),
44+ mounts: Vec::new(),
45+ }
46+ }
47+}
48+
49+#[derive(Debug, Clone, Serialize, Deserialize)]
50+pub struct SupervisorConfig {
51+ pub auto_shutdown_ms: u64,
52+}
53+
54+impl Default for SupervisorConfig {
55+ fn default() -> Self {
56+ Self {
57+ auto_shutdown_ms: default_auto_shutdown_ms(),
58+ }
59+ }
60+}
61+
62+fn default_cpu_count() -> usize {
63+ DEFAULT_CPU_COUNT
64+}
65+
66+fn default_ram_mb() -> u64 {
67+ DEFAULT_RAM_MB
68+}
69+
70+fn default_auto_shutdown_ms() -> u64 {
71+ DEFAULT_AUTO_SHUTDOWN_MS
13}
72}
14
73
15pub fn config_path(project_root: &Path) -> PathBuf {
74pub fn config_path(project_root: &Path) -> PathBuf {
@@ -19,20 +78,147 @@ pub fn config_path(project_root: &Path) -> PathBuf {
19pub fn ensure_config_file(project_root: &Path) -> Result<PathBuf, io::Error> {
78pub fn ensure_config_file(project_root: &Path) -> Result<PathBuf, io::Error> {
20 let path = config_path(project_root);
79 let path = config_path(project_root);
21 if !path.exists() {
80 if !path.exists() {
81+ let default_config = Config::default();
82+ let contents = toml::to_string_pretty(&default_config).unwrap_or_default();
22− fs::write(&path, "")?;
83+ fs::write(&path, contents)?;
23 tracing::info!(path = %path.display(), "created vibebox config");
84 tracing::info!(path = %path.display(), "created vibebox config");
24 }
85 }
25 Ok(path)
86 Ok(path)
26}
87}
27
88
28−pub fn load_config(
89+pub fn load_config(project_root: &Path) -> Config {
29− project_root: &Path,
30−) -> Result<ProjectConfig, Box<dyn std::error::Error + Send + Sync>> {
31− let path = ensure_config_file(project_root)?;
90+ let path = match ensure_config_file(project_root) {
91+ Ok(path) => path,
92+ Err(err) => die(&format!("failed to create config: {err}")),
93+ };
32− let raw = fs::read_to_string(&path)?;
94+ let raw = match fs::read_to_string(&path) {
95+ Ok(raw) => raw,
96+ Err(err) => die(&format!("failed to read config: {err}")),
97+ };
98+ let trimmed = raw.trim();
33 tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
99 tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
34− if raw.trim().is_empty() {
100+ if trimmed.is_empty() {
35− return Ok(ProjectConfig::default());
101+ die(&format!(
102+ "config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
103+ path.display()
104+ ));
105+ }
106+
107+ let value: toml::Value = match toml::from_str(trimmed) {
108+ Ok(value) => value,
109+ Err(err) => die(&format!("invalid config: {err}")),
110+ };
111+ let schema_errors = validate_schema(&value);
112+ if !schema_errors.is_empty() {
113+ let message = format!(
114+ "config file ({}) is missing or invalid fields:\n- {}",
115+ path.display(),
116+ schema_errors.join("\n- ")
117+ );
118+ die(&message);
36 }
119 }
37− Ok(toml::from_str::<ProjectConfig>(&raw)?)
120+
121+ let config: Config = match toml::from_str(trimmed) {
122+ Ok(config) => config,
123+ Err(err) => die(&format!("invalid config: {err}")),
124+ };
125+ validate_or_exit(&config);
126+ config
127+}
128+
129+fn validate_schema(value: &toml::Value) -> Vec<String> {
130+ let mut errors = Vec::new();
131+ let root = match value.as_table() {
132+ Some(table) => table,
133+ None => {
134+ errors.push("config must be a table".to_string());
135+ return errors;
136+ }
137+ };
138+
139+ match root.get("box") {
140+ None => errors.push("missing [box] table".to_string()),
141+ Some(value) => match value.as_table() {
142+ Some(table) => {
143+ validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
144+ validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
145+ validate_string_array(
146+ table,
147+ "mounts",
148+ "[box].mounts (array of strings)",
149+ &mut errors,
150+ );
151+ }
152+ None => errors.push("[box] must be a table".to_string()),
153+ },
154+ }
155+
156+ match root.get("supervisor") {
157+ None => errors.push("missing [supervisor] table".to_string()),
158+ Some(value) => match value.as_table() {
159+ Some(table) => {
160+ validate_int(
161+ table,
162+ "auto_shutdown_ms",
163+ "[supervisor].auto_shutdown_ms (integer)",
164+ &mut errors,
165+ );
166+ }
167+ None => errors.push("[supervisor] must be a table".to_string()),
168+ },
169+ }
170+
171+ errors
172+}
173+
174+fn validate_int(table: &toml::value::Table, key: &str, label: &str, errors: &mut Vec<String>) {
175+ match table.get(key) {
176+ None => errors.push(format!("missing {label}")),
177+ Some(value) => {
178+ if value.as_integer().is_none() {
179+ errors.push(format!("invalid {label}: expected integer"));
180+ }
181+ }
182+ }
183+}
184+
185+fn validate_string_array(
186+ table: &toml::value::Table,
187+ key: &str,
188+ label: &str,
189+ errors: &mut Vec<String>,
190+) {
191+ match table.get(key) {
192+ None => errors.push(format!("missing {label}")),
193+ Some(value) => match value.as_array() {
194+ Some(values) => {
195+ if values.iter().any(|value| !value.is_str()) {
196+ errors.push(format!("invalid {label}: expected array of strings"));
197+ }
198+ }
199+ None => errors.push(format!("invalid {label}: expected array of strings")),
200+ },
201+ }
202+}
203+
204+fn validate_or_exit(config: &Config) {
205+ if config.box_cfg.cpu_count == 0 {
206+ die("box.cpu_count must be >= 1");
207+ }
208+ if config.box_cfg.ram_mb == 0 {
209+ die("box.ram_mb must be >= 1");
210+ }
211+ if config.supervisor.auto_shutdown_ms == 0 {
212+ die("supervisor.auto_shutdown_ms must be >= 1");
213+ }
214+ for spec in &config.box_cfg.mounts {
215+ if let Err(err) = DirectoryShare::from_mount_spec(spec) {
216+ die(&format!("invalid mount spec '{spec}': {err}"));
217+ }
218+ }
219+}
220+
221+fn die(message: &str) -> ! {
222+ eprintln!("[vibebox] {message}");
223+ std::process::exit(1);
38}
224}
vibebox.toml+7
@@ -0,0 +1,7 @@
1+[box]
2+cpu_count = 2
3+ram_mb = 2048
4+mounts = []
5+
6+[supervisor]
7+auto_shutdown_ms = 20000