@@ -2,7 +2,7 @@ use std::{
3 ffi::OsString,
3 ffi::OsString,
4 io::{self, IsTerminal, Write},
4 io::{self, IsTerminal, Write},
5− path::PathBuf,
5+ path::{Path, PathBuf},
6 sync::{Arc, Mutex},
6 sync::{Arc, Mutex},
@@ -32,6 +32,8 @@ enum Command {
33 /// Delete the current project's .vibebox directory
33 /// Delete the current project's .vibebox directory
35+ /// Explain mounts and mappings
37fn main() -> Result<()> {
39fn main() -> Result<()> {
@@ -42,7 +44,7 @@ fn main() -> Result<()> {
42 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
44 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
43 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
45 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
44 if let Some(command) = cli.command {
46 if let Some(command) = cli.command {
45− return handle_command(command, &cwd);
47+ return handle_command(command, &cwd, cli.config.as_deref());
48 let config_override = cli.config.clone();
50 let config_override = cli.config.clone();
@@ -111,7 +113,7 @@ fn main() -> Result<()> {
114−fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
116+fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path>) -> Result<()> {
115 match command {
117 match command {
116 Command::List => {
118 Command::List => {
117 let manager = SessionManager::new()?;
119 let manager = SessionManager::new()?;
@@ -164,6 +166,16 @@ fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
169+ Command::Explain => {
170+ let config = config::load_config_with_path(cwd, config_override);
171+ let rows = build_mount_rows(cwd, &config)?;
172+ if rows.is_empty() {
173+ println!("No mounts configured.");
176+ tui::render_mounts_table(&rows)?;
@@ -231,6 +243,100 @@ fn format_last_active(value: Option<&str>) -> String {
231 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
243 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
246+fn build_mount_rows(cwd: &Path, config: &config::Config) -> Result<Vec<tui::MountListRow>> {
247+ let mut rows = Vec::new();
248+ rows.extend(default_mounts(cwd)?);
249+ for spec in &config.box_cfg.mounts {
250+ rows.push(parse_mount_spec(cwd, spec, false)?);
255+fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
256+ let project_name = cwd
258+ .and_then(|name| name.to_str())
259+ .unwrap_or("project");
260+ let project_guest = format!("/root/{project_name}");
261+ let project_host = relative_to_home(&cwd.to_path_buf());
262+ let mut rows = vec![tui::MountListRow {
264+ guest: project_guest,
265+ mode: "read-write".to_string(),
266+ default_mount: "yes".to_string(),
269+ let home = env::var("HOME")
271+ .unwrap_or_else(|_| PathBuf::from("/"));
272+ let cache_home = env::var("XDG_CACHE_HOME")
274+ .unwrap_or_else(|_| home.join(".cache"));
275+ let cache_dir = cache_home.join(session_manager::GLOBAL_CACHE_DIR_NAME);
276+ let guest_mise_cache = cache_dir.join(".guest-mise-cache");
277+ rows.push(tui::MountListRow {
278+ host: relative_to_home(&guest_mise_cache),
279+ guest: "/root/.local/share/mise".to_string(),
280+ mode: "read-write".to_string(),
281+ default_mount: "yes".to_string(),
286+fn parse_mount_spec(cwd: &Path, spec: &str, default_mount: bool) -> Result<tui::MountListRow> {
287+ let parts: Vec<&str> = spec.split(':').collect();
288+ if parts.len() < 2 || parts.len() > 3 {
289+ return Err(color_eyre::eyre::eyre!("invalid mount spec: {spec}"));
291+ let host_part = parts[0];
292+ let guest_part = parts[1];
293+ let mode = if parts.len() == 3 {
295+ "read-only" => "read-only",
296+ "read-write" => "read-write",
298+ return Err(color_eyre::eyre::eyre!(
299+ "invalid mount mode '{}'; expected read-only or read-write",
307+ let host_path = resolve_host_path(cwd, host_part);
308+ let host_display = relative_to_home(&host_path);
309+ let guest_display = if Path::new(guest_part).is_absolute() {
310+ guest_part.to_string()
312+ format!("/root/{guest_part}")
314+ Ok(tui::MountListRow {
316+ guest: guest_display,
317+ mode: mode.to_string(),
318+ default_mount: if default_mount { "yes" } else { "no" }.to_string(),
322+fn resolve_host_path(cwd: &Path, host: &str) -> PathBuf {
323+ if let Some(stripped) = host.strip_prefix("~/") {
324+ if let Ok(home) = env::var("HOME") {
325+ return PathBuf::from(home).join(stripped);
327+ } else if host == "~" {
328+ if let Ok(home) = env::var("HOME") {
329+ return PathBuf::from(home);
332+ let host_path = PathBuf::from(host);
333+ if host_path.is_absolute() {
234fn init_tracing() {
340fn init_tracing() {
235 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
341 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
236 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
342 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();