@@ -2,7 +2,7 @@ use std::{
22 env,
33 ffi::OsString,
44 io::{self, IsTerminal, Write},
5− path::PathBuf,
5+ path::{Path, PathBuf},
66 sync::{Arc, Mutex},
77};
88
@@ -32,6 +32,8 @@ enum Command {
3232 List,
3333 /// Delete the current project's .vibebox directory
3434 Clean,
35+ /// Explain mounts and mappings
36+ Explain,
3537}
3638
3739fn main() -> Result<()> {
@@ -42,7 +44,7 @@ fn main() -> Result<()> {
4244 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
4345 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
4446 if let Some(command) = cli.command {
45− return handle_command(command, &cwd);
47+ return handle_command(command, &cwd, cli.config.as_deref());
4648 }
4749
4850 let config_override = cli.config.clone();
@@ -111,7 +113,7 @@ fn main() -> Result<()> {
111113 Ok(())
112114}
113115
114−fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
116+fn handle_command(command: Command, cwd: &PathBuf, config_override: Option<&Path>) -> Result<()> {
115117 match command {
116118 Command::List => {
117119 let manager = SessionManager::new()?;
@@ -164,6 +166,16 @@ fn handle_command(command: Command, cwd: &PathBuf) -> Result<()> {
164166 );
165167 Ok(())
166168 }
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.");
174+ return Ok(());
175+ }
176+ tui::render_mounts_table(&rows)?;
177+ Ok(())
178+ }
167179 }
168180}
169181
@@ -231,6 +243,100 @@ fn format_last_active(value: Option<&str>) -> String {
231243 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
232244}
233245
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)?);
251+ }
252+ Ok(rows)
253+}
254+
255+fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>> {
256+ let project_name = cwd
257+ .file_name()
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 {
263+ host: project_host,
264+ guest: project_guest,
265+ mode: "read-write".to_string(),
266+ default_mount: "yes".to_string(),
267+ }];
268+
269+ let home = env::var("HOME")
270+ .map(PathBuf::from)
271+ .unwrap_or_else(|_| PathBuf::from("/"));
272+ let cache_home = env::var("XDG_CACHE_HOME")
273+ .map(PathBuf::from)
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(),
282+ });
283+ Ok(rows)
284+}
285+
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}"));
290+ }
291+ let host_part = parts[0];
292+ let guest_part = parts[1];
293+ let mode = if parts.len() == 3 {
294+ match parts[2] {
295+ "read-only" => "read-only",
296+ "read-write" => "read-write",
297+ other => {
298+ return Err(color_eyre::eyre::eyre!(
299+ "invalid mount mode '{}'; expected read-only or read-write",
300+ other
301+ ));
302+ }
303+ }
304+ } else {
305+ "read-write"
306+ };
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()
311+ } else {
312+ format!("/root/{guest_part}")
313+ };
314+ Ok(tui::MountListRow {
315+ host: host_display,
316+ guest: guest_display,
317+ mode: mode.to_string(),
318+ default_mount: if default_mount { "yes" } else { "no" }.to_string(),
319+ })
320+}
321+
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);
326+ }
327+ } else if host == "~" {
328+ if let Ok(home) = env::var("HOME") {
329+ return PathBuf::from(home);
330+ }
331+ }
332+ let host_path = PathBuf::from(host);
333+ if host_path.is_absolute() {
334+ host_path
335+ } else {
336+ cwd.join(host_path)
337+ }
338+}
339+
234340fn init_tracing() {
235341 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
236342 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();