@@ -14,7 +14,9 @@ use time::format_description::well_known::Rfc3339;
1414use tracing_subscriber::EnvFilter;
1515
1616use vibebox::tui::{AppState, VmInfo};
17−use vibebox::{SessionManager, commands, config, instance, session_manager, tui, vm, vm_manager};
17+use vibebox::{
18+ SessionManager, commands, config, explain, instance, session_manager, tui, vm, vm_manager,
19+};
1820
1921#[derive(Debug, Parser)]
2022#[command(name = "vibebox", version, about = "Vibebox CLI")]
@@ -168,8 +170,10 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path
168170 }
169171 Command::Explain => {
170172 let config = config::load_config_with_path(cwd, config_override);
171− let mounts = build_mount_rows(cwd, &config)?;
172− let networks = build_network_rows(cwd)?;
173+ let mounts = explain::build_mount_rows(cwd, &config)
174+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
175+ let networks = explain::build_network_rows(cwd)
176+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
173177 if mounts.is_empty() && networks.is_empty() {
174178 println!("No mounts or network info available.");
175179 return Ok(());
@@ -244,120 +248,6 @@ fn format_last_active(value: Option<&str>) -> String {
244248 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
245249}
246250
247−fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::MountListRow>> {
248− let mut rows = Vec::new();
249− rows.extend(default_mounts(cwd)?);
250− for spec in &config.box_cfg.mounts {
251− rows.push(parse_mount_spec(cwd, spec, false)?);
252− }
253− Ok(rows)
254−}
255−
256−fn build_network_rows(cwd: &Path) -> Result<Vec<tui::NetworkListRow>> {
257− let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME);
258− let mut vm_ip = "-".to_string();
259− if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) {
260− vm_ip = ip;
261− }
262− let host_to_vm = if vm_ip == "-" {
263− "ssh: <pending>:22".to_string()
264− } else {
265− format!("ssh: {vm_ip}:22")
266− };
267− let row = tui::NetworkListRow {
268− network_type: "NAT".to_string(),
269− vm_ip: vm_ip.clone(),
270− host_to_vm,
271− vm_to_host: "none".to_string(),
272− };
273− Ok(vec![row])
274−}
275−
276−fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
277− let project_name = cwd
278− .file_name()
279− .and_then(|name| name.to_str())
280− .unwrap_or("project");
281− let project_guest = format!("/root/{project_name}");
282− let project_host = relative_to_home(&cwd.to_path_buf());
283− let mut rows = vec![tui::MountListRow {
284− host: project_host,
285− guest: project_guest,
286− mode: "read-write".to_string(),
287− default_mount: "yes".to_string(),
288− }];
289−
290− let home = env::var("HOME")
291− .map(PathBuf::from)
292− .unwrap_or_else(|_| PathBuf::from("/"));
293− let cache_home = env::var("XDG_CACHE_HOME")
294− .map(PathBuf::from)
295− .unwrap_or_else(|_| home.join(".cache"));
296− let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME);
297− let guest_mise_cache = cache_dir.join(".guest-mise-cache");
298− rows.push(tui::MountListRow {
299− host: relative_to_home(&guest_mise_cache),
300− guest: "/root/.local/share/mise".to_string(),
301− mode: "read-write".to_string(),
302− default_mount: "yes".to_string(),
303− });
304− Ok(rows)
305−}
306−
307−fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result<tui::MountListRow> {
308− let parts: Vec<&str> = spec.split(':').collect();
309− if parts.len() < 2 || parts.len() > 3 {
310− return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}"));
311− }
312− let host_part = parts[0];
313− let guest_part = parts[1];
314− let mode = if parts.len() == 3 {
315− match parts[2] {
316− "read-only" => "read-only",
317− "read-write" => "read-write",
318− other => {
319− return Err(color_eyre::eyre::eyre!(
320− "invalid mount mode '{}'; expected read-only or read-write",
321− other
322− ));
323− }
324− }
325− } else {
326− "read-write"
327− };
328− let host_path = resolve_host_path(cwd, host_part);
329− let host_display = relative_to_home(&host_path);
330− let guest_display = if Path::new(guest_part).is_absolute() {
331− guest_part.to_string()
332− } else {
333− format!("/root/{guest_part}")
334− };
335− Ok(tui::MountListRow {
336− host: host_display,
337− guest: guest_display,
338− mode: mode.to_string(),
339− default_mount: if default_mount { "yes" } else { "no" }.to_string(),
340− })
341−}
342−
343−fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf {
344− if let Some(stripped) = host.strip_prefix("~/") {
345− if let Ok(home) = env::var("HOME") {
346− return PathBuf::from(home).join(stripped);
347− }
348− } else if host == "~" {
349− if let Ok(home) = env::var("HOME") {
350− return PathBuf::from(home);
351− }
352− }
353− let host_path = PathBuf::from(host);
354− if host_path.is_absolute() {
355− host_path
356− } else {
357− cwd.join(host_path)
358− }
359−}
360−
361251fn init_tracing() {
362252 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
363253 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();