diff --git a/src/bin/vibebox-cli.rs b/src/bin/vibebox-cli.rs index 377b062..9dff02d 100644 --- a/src/bin/vibebox-cli.rs +++ b/src/bin/vibebox-cli.rs @@ -14,7 +14,9 @@ use time::format_description::well_known::Rfc3339; use tracing_subscriber::EnvFilter; use vibebox::tui::{AppState, VmInfo}; -use vibebox::{SessionManager, commands, config, instance, session_manager, tui, vm, vm_manager}; +use vibebox::{ + SessionManager, commands, config, explain, instance, session_manager, tui, vm, vm_manager, +}; #[derive(Debug, Parser)] #[command(name = "vibebox", version, about = "Vibebox CLI")] @@ -168,8 +170,10 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path } Command::Explain => { let config = config::load_config_with_path(cwd, config_override); - let mounts = build_mount_rows(cwd, &config)?; - let networks = build_network_rows(cwd)?; + let mounts = explain::build_mount_rows(cwd, &config) + .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; + let networks = explain::build_network_rows(cwd) + .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?; if mounts.is_empty() && networks.is_empty() { println!("No mounts or network info available."); return Ok(()); @@ -244,120 +248,6 @@ fn format_last_active(value: Option<&str>) -> String { format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) } -fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result> { - let mut rows = Vec::new(); - rows.extend(default_mounts(cwd)?); - for spec in &config.box_cfg.mounts { - rows.push(parse_mount_spec(cwd, spec, false)?); - } - Ok(rows) -} - -fn build_network_rows(cwd: &Path) -> Result> { - let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME); - let mut vm_ip = "-".to_string(); - if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) { - vm_ip = ip; - } - let host_to_vm = if vm_ip == "-" { - "ssh: :22".to_string() - } else { - format!("ssh: {vm_ip}:22") - }; - let row = tui::NetworkListRow { - network_type: "NAT".to_string(), - vm_ip: vm_ip.clone(), - host_to_vm, - vm_to_host: "none".to_string(), - }; - Ok(vec![row]) -} - -fn default_mounts(cwd: &Path) -> Result> { - let project_name = cwd - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("project"); - let project_guest = format!("/root/{project_name}"); - let project_host = relative_to_home(&cwd.to_path_buf()); - let mut rows = vec![tui::MountListRow { - host: project_host, - guest: project_guest, - mode: "read-write".to_string(), - default_mount: "yes".to_string(), - }]; - - let home = env::var("HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/")); - let cache_home = env::var("XDG_CACHE_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home.join(".cache")); - let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME); - let guest_mise_cache = cache_dir.join(".guest-mise-cache"); - rows.push(tui::MountListRow { - host: relative_to_home(&guest_mise_cache), - guest: "/root/.local/share/mise".to_string(), - mode: "read-write".to_string(), - default_mount: "yes".to_string(), - }); - Ok(rows) -} - -fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result { - let parts: Vec<&str> = spec.split(':').collect(); - if parts.len() < 2 || parts.len() > 3 { - return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}")); - } - let host_part = parts[0]; - let guest_part = parts[1]; - let mode = if parts.len() == 3 { - match parts[2] { - "read-only" => "read-only", - "read-write" => "read-write", - other => { - return Err(color_eyre::eyre::eyre!( - "invalid mount mode '{}'; expected read-only or read-write", - other - )); - } - } - } else { - "read-write" - }; - let host_path = resolve_host_path(cwd, host_part); - let host_display = relative_to_home(&host_path); - let guest_display = if Path::new(guest_part).is_absolute() { - guest_part.to_string() - } else { - format!("/root/{guest_part}") - }; - Ok(tui::MountListRow { - host: host_display, - guest: guest_display, - mode: mode.to_string(), - default_mount: if default_mount { "yes" } else { "no" }.to_string(), - }) -} - -fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf { - if let Some(stripped) = host.strip_prefix("~/") { - if let Ok(home) = env::var("HOME") { - return PathBuf::from(home).join(stripped); - } - } else if host == "~" { - if let Ok(home) = env::var("HOME") { - return PathBuf::from(home); - } - } - let host_path = PathBuf::from(host); - if host_path.is_absolute() { - host_path - } else { - cwd.join(host_path) - } -} - fn init_tracing() { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err(); diff --git a/src/explain.rs b/src/explain.rs new file mode 100644 index 0000000..eb1068b --- /dev/null +++ b/src/explain.rs @@ -0,0 +1,143 @@ +use std::{ + env, + error::Error, + path::{Path, PathBuf}, +}; + +use crate::{config, instance, session_manager, tui}; + +pub fn build_mount_rows( + cwd: &Path, + config: &config::Config, +) -> Result, Box> { + let mut rows = Vec::new(); + rows.extend(default_mounts(cwd)?); + for spec in &config.box_cfg.mounts { + rows.push(parse_mount_spec(cwd, spec, false)?); + } + Ok(rows) +} + +pub fn build_network_rows( + cwd: &Path, +) -> Result, Box> { + let instance_dir = cwd.join(session_manager::INSTANCE_DIR_NAME); + let mut vm_ip = "-".to_string(); + if let Ok(Some(ip)) = instance::read_instance_vm_ip(&instance_dir) { + vm_ip = ip; + } + let host_to_vm = if vm_ip == "-" { + "ssh: :22".to_string() + } else { + format!("ssh: {vm_ip}:22") + }; + let row = tui::NetworkListRow { + network_type: "NAT".to_string(), + vm_ip: vm_ip.clone(), + host_to_vm, + vm_to_host: "none".to_string(), + }; + Ok(vec![row]) +} + +fn default_mounts(cwd: &Path) -> Result, Box> { + let project_name = cwd + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("project"); + let project_guest = format!("/root/{project_name}"); + let project_host = display_path(cwd); + let mut rows = vec![tui::MountListRow { + host: project_host, + guest: project_guest, + mode: "read-write".to_string(), + default_mount: "yes".to_string(), + }]; + + let home = env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/")); + let cache_home = env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home.join(".cache")); + let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME); + let guest_mise_cache = cache_dir.join(".guest-mise-cache"); + rows.push(tui::MountListRow { + host: display_path(&guest_mise_cache), + guest: "/root/.local/share/mise".to_string(), + mode: "read-write".to_string(), + default_mount: "yes".to_string(), + }); + Ok(rows) +} + +fn parse_mount_spec( + cwd: &Path, + spec: &str, + default_mount: bool, +) -> Result> { + let parts: Vec<&str> = spec.split(':').collect(); + if parts.len() < 2 || parts.len() > 3 { + return Err(format!("invalid mount spec: {spec}").into()); + } + let host_part = parts[0]; + let guest_part = parts[1]; + let mode = if parts.len() == 3 { + match parts[2] { + "read-only" => "read-only", + "read-write" => "read-write", + other => { + return Err(format!( + "invalid mount mode '{}'; expected read-only or read-write", + other + ) + .into()); + } + } + } else { + "read-write" + }; + + let host_display = display_host_spec(cwd, host_part); + let guest_display = if Path::new(guest_part).is_absolute() { + guest_part.to_string() + } else { + format!("/root/{guest_part}") + }; + Ok(tui::MountListRow { + host: host_display, + guest: guest_display, + mode: mode.to_string(), + default_mount: if default_mount { "yes" } else { "no" }.to_string(), + }) +} + +fn display_host_spec(cwd: &Path, host: &str) -> String { + if host == "~" || host.starts_with("~/") { + return host.to_string(); + } + let host_path = PathBuf::from(host); + if host_path.is_absolute() { + return display_path(&host_path); + } + let candidate = cwd.join(&host_path); + if candidate.is_absolute() { + display_path(&candidate) + } else { + host.to_string() + } +} + +fn display_path(path: &Path) -> String { + let Ok(home) = env::var("HOME") else { + return path.display().to_string(); + }; + let home_path = PathBuf::from(home); + if let Ok(stripped) = path.strip_prefix(&home_path) { + if stripped.components().next().is_none() { + return "~".to_string(); + } + return format!("~/{}", stripped.display()); + } + path.display().to_string() +} diff --git a/src/lib.rs b/src/lib.rs index 7f4f82d..8acb606 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod commands; +pub mod explain; pub mod instance; pub mod session_manager; pub mod tui;