@@ -14,7 +14,9 @@ use time::format_description::well_known::Rfc3339;
14use tracing_subscriber::EnvFilter;
14use tracing_subscriber::EnvFilter;
16use vibebox::tui::{AppState, VmInfo};
16use vibebox::tui::{AppState, VmInfo};
17−use vibebox::{SessionManager, commands, config, instance, session_manager, tui, vm, vm_manager};
18+ SessionManager, commands, config, explain, instance, session_manager, tui, vm, vm_manager,
19#[derive(Debug, Parser)]
21#[derive(Debug, Parser)]
20#[command(name = "vibebox", version, about = "Vibebox CLI")]
22#[command(name = "vibebox", version, about = "Vibebox CLI")]
@@ -168,8 +170,10 @@ fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path
169 Command::Explain => {
171 Command::Explain => {
170 let config = config::load_config_with_path(cwd, config_override);
172 let config = config::load_config_with_path(cwd, config_override);
171− let mounts = build_mount_rows(cwd, &config)?;
173+ let mounts = explain::build_mount_rows(cwd, &config)
174+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
172− let networks = build_network_rows(cwd)?;
175+ let networks = explain::build_network_rows(cwd)
176+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
173 if mounts.is_empty() && networks.is_empty() {
177 if mounts.is_empty() && networks.is_empty() {
174 println!("No mounts or network info available.");
178 println!("No mounts or network info available.");
175 return Ok(());
179 return Ok(());
@@ -244,120 +248,6 @@ fn format_last_active(value: Option<&str>) -> String {
244 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
248 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
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)?);
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) {
262− let host_to_vm = if vm_ip == "-" {
263− "ssh: <pending>:22".to_string()
265− format!("ssh: {vm_ip}:22")
267− let row = tui::NetworkListRow {
268− network_type: "NAT".to_string(),
269− vm_ip: vm_ip.clone(),
271− vm_to_host: "none".to_string(),
276−fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
277− let project_name = cwd
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 {
285− guest: project_guest,
286− mode: "read-write".to_string(),
287− default_mount: "yes".to_string(),
290− let home = env::var("HOME")
292− .unwrap_or_else(|_| PathBuf::from("/"));
293− let cache_home = env::var("XDG_CACHE_HOME")
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(),
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}"));
312− let host_part = parts[0];
313− let guest_part = parts[1];
314− let mode = if parts.len() == 3 {
316− "read-only" => "read-only",
317− "read-write" => "read-write",
319− return Err(color_eyre::eyre::eyre!(
320− "invalid mount mode '{}'; expected read-only or read-write",
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()
333− format!("/root/{guest_part}")
335− Ok(tui::MountListRow {
337− guest: guest_display,
338− mode: mode.to_string(),
339− default_mount: if default_mount { "yes" } else { "no" }.to_string(),
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);
348− } else if host == "~" {
349− if let Ok(home) = env::var("HOME") {
350− return PathBuf::from(home);
353− let host_path = PathBuf::from(host);
354− if host_path.is_absolute() {
361fn init_tracing() {
251fn init_tracing() {
362 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
252 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
363 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
253 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();