on main help sign in
feat: added explain command to display mounts
commit94d941c21aa2ae8d1e067f8475ac59d320bea33f authorrobcholz <84130577+robcholz@users.noreply.github.com> dateSat 7 Feb 2026, 16:31 (8 months ago) parentc98bc78
3 files changed+171−5
docs/tasks.md+2−2 src/bin/vibebox-cli.rs+109−3 src/tui.rs+60
docs/tasks.md+2−2
@@ -42,11 +42,11 @@
42425. [x] wire up SessionManager.
43436. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
44447. [x] setup vibebox commands
45−8. [ ] setup cli commands.
45+8. [x] setup cli commands.
4646 1. [x] Organize all the params.
4747 2. [x] Remove old cli.
4848 3. [x] add an actual config file.
49− 4. [ ] set up the cli.
49+ 4. [x] set up the cli.
50509. [ ] fix ui overlap, and consistency issue.
515110. [ ] intensive integration test.
5252
src/bin/vibebox-cli.rs+109−3
@@ -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();
src/tui.rs+60
@@ -107,6 +107,14 @@ pub struct SessionListRow {
107107 pub id: String,
108108}
109109
110+#[derive(Debug, Clone)]
111+pub struct MountListRow {
112+ pub host: String,
113+ pub guest: String,
114+ pub mode: String,
115+ pub default_mount: String,
116+}
117+
110118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
111119struct PageLayout {
112120 header: Rect,
@@ -235,6 +243,58 @@ pub fn render_sessions_table(rows: &[SessionListRow]) -> Result<()> {
235243 Ok(())
236244}
237245
246+pub fn render_mounts_table(rows: &[MountListRow]) -> Result<()> {
247+ let (width, _) = crossterm::terminal::size()?;
248+ if width == 0 {
249+ return Ok(());
250+ }
251+
252+ let height = (rows.len() as u16).saturating_add(3);
253+ let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
254+ let area = Rect::new(0, 0, width, height);
255+
256+ let header = Row::new(vec![
257+ Cell::from("Host"),
258+ Cell::from("Guest"),
259+ Cell::from("Mode"),
260+ Cell::from(""),
261+ Cell::from("Default"),
262+ ])
263+ .style(Style::default().fg(Color::Cyan));
264+
265+ let table_rows = rows.iter().map(|row| {
266+ Row::new(vec![
267+ Cell::from(row.host.clone()),
268+ Cell::from(row.guest.clone()),
269+ Cell::from(row.mode.clone()),
270+ Cell::from(""),
271+ Cell::from(row.default_mount.clone()),
272+ ])
273+ });
274+
275+ let table = Table::new(
276+ table_rows,
277+ [
278+ Constraint::Min(24),
279+ Constraint::Min(24),
280+ Constraint::Length(10),
281+ Constraint::Length(1),
282+ Constraint::Length(8),
283+ ],
284+ )
285+ .header(header)
286+ .block(Block::default().title("Mounts").borders(Borders::ALL))
287+ .column_spacing(1);
288+
289+ table.render(area, &mut buffer);
290+
291+ let mut stdout = io::stdout();
292+ execute!(stdout, Clear(ClearType::All), MoveTo(0, 0), Show)?;
293+ write_buffer_with_style(&buffer, &mut stdout)?;
294+ stdout.flush()?;
295+ Ok(())
296+}
297+
238298pub fn passthrough_vm_io(
239299 app: Arc<Mutex<AppState>>,
240300 output_monitor: Arc<vm::OutputMonitor>,