on main help sign in
feat: VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
commit68145e662a244d42cb14eb8b25a66a23813b3527 authorrobcholz <84130577+robcholz@users.noreply.github.com> dateSat 7 Feb 2026, 13:14 (8 months ago) parentfc7e95f
8 files changed+169−349
docs/tasks.md+6−6 src/bin/vibebox-cli.rs+12−20 src/bin/vibebox-supervisor.rs+8−2 src/instance.rs+24−4 src/lib.rs+1 src/session_manager.rs+116−315 src/vm.rs+2−2 vibebox.toml
docs/tasks.md+6−6
@@ -40,7 +40,7 @@
40403. [x] allow multi vibebox to connect to the same vm.
41414. [x] use vm.lock to ensure process concurrency safety.
42425. [ ] wire up SessionManager.
43−6. [ ] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
43+6. [x] VM should be separated by a per-session VM daemon process (only accepts if to shut down vm and itself).
44447. [ ] setup vibebox commands
45458. [ ] setup cli commands.
46469. [ ] fix ui overlap.
@@ -53,8 +53,8 @@
5353
5454## Stage 2
5555
56−1. [ ] Redirect vm output to log.
57−2. [ ] Redirect vm output to vibebox starting it.
58−3. [ ] use anyhow to sync api.
59−
60−[ ]
56+1. [ ] retouch the cli ux.
57+2. [ ] refactor the code.
58+3. [ ] Redirect vm output to log.
59+4. [ ] Redirect vm output to vibebox starting it.
60+5. [ ] use anyhow to sync api.
src/bin/vibebox-cli.rs+12−20
@@ -1,26 +1,18 @@
11use std::{
22 env,
33 ffi::OsString,
4− fs,
54 io::{self, IsTerminal, Write},
6− path::Path,
75 sync::{Arc, Mutex},
86};
97
108use color_eyre::Result;
11−use serde::Deserialize;
129use tracing_subscriber::EnvFilter;
1310
1411use vibebox::tui::{AppState, VmInfo};
15−use vibebox::{instance, tui, vm, vm_manager};
12+use vibebox::{SessionManager, config, instance, tui, vm, vm_manager};
1613
1714const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000;
1815
19−#[derive(Debug, Default, Deserialize)]
20−struct ProjectConfig {
21− auto_shutdown_ms: Option<u64>,
22−}
23−
2416fn main() -> Result<()> {
2517 init_tracing();
2618 color_eyre::install()?;
@@ -62,6 +54,13 @@ fn main() -> Result<()> {
6254 };
6355 let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
6456 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
57+ if let Ok(manager) = SessionManager::new() {
58+ if let Err(err) = manager.update_global_sessions(&cwd) {
59+ tracing::warn!(error = %err, "failed to update global session list");
60+ }
61+ } else {
62+ tracing::warn!("failed to initialize session manager");
63+ }
6564 let app = Arc::new(Mutex::new(AppState::new(cwd.clone(), vm_info)));
6665
6766 {
@@ -74,7 +73,10 @@ fn main() -> Result<()> {
7473 stdout.flush()?;
7574 }
7675
77− let auto_shutdown_ms = load_auto_shutdown_ms(&cwd)?;
76+ let auto_shutdown_ms = config::load_config(&cwd)
77+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?
78+ .auto_shutdown_ms
79+ .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
7880 tracing::info!(auto_shutdown_ms, "auto shutdown config");
7981 let manager_conn = vm_manager::ensure_manager(&raw_args, auto_shutdown_ms)
8082 .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
@@ -84,16 +86,6 @@ fn main() -> Result<()> {
8486 Ok(())
8587}
8688
87−fn load_auto_shutdown_ms(project_root: &Path) -> Result<u64> {
88− let path = project_root.join("vibebox.toml");
89− let config = match fs::read_to_string(&path) {
90− Ok(raw) => toml::from_str::<ProjectConfig>(&raw)?,
91− Err(err) if err.kind() == io::ErrorKind::NotFound => ProjectConfig::default(),
92− Err(err) => return Err(err.into()),
93− };
94− Ok(config.auto_shutdown_ms.unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS))
95−}
96−
9789fn init_tracing() {
9890 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
9991 let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
src/bin/vibebox-supervisor.rs+8−2
@@ -6,7 +6,7 @@ use std::{
66use color_eyre::Result;
77use tracing_subscriber::EnvFilter;
88
9−use vibebox::{vm, vm_manager};
9+use vibebox::{instance, vm, vm_manager};
1010
1111const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 3000;
1212
@@ -15,6 +15,10 @@ fn main() -> Result<()> {
1515 color_eyre::install()?;
1616
1717 tracing::info!("starting vm supervisor");
18+ let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
19+ let instance_dir = instance::ensure_instance_dir(&cwd)
20+ .map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
21+ let _ = instance::touch_last_active(&instance_dir);
1822 let args = vm::parse_cli().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
1923 let auto_shutdown_ms = env::var("VIBEBOX_AUTO_SHUTDOWN_MS")
2024 .ok()
@@ -22,7 +26,9 @@ fn main() -> Result<()> {
2226 .unwrap_or(DEFAULT_AUTO_SHUTDOWN_MS);
2327 tracing::info!(auto_shutdown_ms, "vm supervisor config");
2428
25− if let Err(err) = vm_manager::run_manager(args, auto_shutdown_ms) {
29+ let result = vm_manager::run_manager(args, auto_shutdown_ms);
30+ let _ = instance::touch_last_active(&instance_dir);
31+ if let Err(err) = result {
2632 tracing::error!(error = %err, "vm supervisor exited");
2733 return Err(color_eyre::eyre::eyre!(err.to_string()));
2834 }
src/instance.rs+24−4
@@ -1,3 +1,4 @@
1+use crate::session_manager::INSTANCE_TOML_FILENAME;
12use std::{
23 env, fs,
34 io::{self, Write},
@@ -15,6 +16,7 @@ use std::{
1516
1617use serde::{Deserialize, Serialize};
1718use std::sync::mpsc::Sender;
19+use time::{OffsetDateTime, format_description::well_known::Rfc3339};
1820use uuid::Uuid;
1921
2022use crate::{
@@ -23,7 +25,6 @@ use crate::{
2325 vm::{self, LoginAction, VmInput},
2426};
2527
26−const INSTANCE_TOML: &str = "instance.toml";
2728const SSH_KEY_NAME: &str = "ssh_key";
2829#[allow(dead_code)]
2930const SERIAL_LOG_NAME: &str = "serial.log";
@@ -34,11 +35,15 @@ const SSH_SETUP_SCRIPT: &str = include_str!("ssh.sh");
3435
3536#[derive(Debug, Clone, Serialize, Deserialize)]
3637pub(crate) struct InstanceConfig {
38+ #[serde(default)]
39+ id: String,
3740 #[serde(default = "default_ssh_user")]
3841 ssh_user: String,
3942 #[serde(default)]
4043 sudo_password: String,
4144 #[serde(default)]
45+ last_active: Option<String>,
46+ #[serde(default)]
4247 pub(crate) vm_ipv4: Option<String>,
4348}
4449
@@ -69,7 +74,7 @@ pub fn run_with_ssh(manager_conn: UnixStream) -> Result<(), Box<dyn std::error::
6974 run_ssh_session(ssh_key, ssh_user, ip)
7075}
7176
72−pub(crate) fn ensure_instance_dir(project_root: &Path) -> Result<PathBuf, io::Error> {
77+pub fn ensure_instance_dir(project_root: &Path) -> Result<PathBuf, io::Error> {
7378 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
7479 fs::create_dir_all(&instance_dir)?;
7580 Ok(instance_dir)
@@ -121,14 +126,16 @@ pub(crate) fn ensure_ssh_keypair(
121126pub(crate) fn load_or_create_instance_config(
122127 instance_dir: &Path,
123128) -> Result<InstanceConfig, Box<dyn std::error::Error>> {
124− let config_path = instance_dir.join(INSTANCE_TOML);
129+ let config_path = instance_dir.join(INSTANCE_TOML_FILENAME);
125130 let mut config = if config_path.exists() {
126131 let raw = fs::read_to_string(&config_path)?;
127132 toml::from_str::<InstanceConfig>(&raw)?
128133 } else {
129134 InstanceConfig {
135+ id: String::new(),
130136 ssh_user: default_ssh_user(),
131137 sudo_password: String::new(),
138+ last_active: None,
132139 vm_ipv4: None,
133140 }
134141 };
@@ -139,6 +146,11 @@ pub(crate) fn load_or_create_instance_config(
139146 changed = true;
140147 }
141148
149+ if config.id.trim().is_empty() {
150+ config.id = Uuid::now_v7().to_string();
151+ changed = true;
152+ }
153+
142154 if config.sudo_password.trim().is_empty() {
143155 config.sudo_password = generate_password();
144156 changed = true;
@@ -151,6 +163,14 @@ pub(crate) fn load_or_create_instance_config(
151163 Ok(config)
152164}
153165
166+pub fn touch_last_active(instance_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
167+ let mut config = load_or_create_instance_config(instance_dir)?;
168+ let now = OffsetDateTime::now_utc().format(&Rfc3339)?;
169+ config.last_active = Some(now);
170+ write_instance_config(&instance_dir.join(INSTANCE_TOML_FILENAME), &config)?;
171+ Ok(())
172+}
173+
154174pub(crate) fn write_instance_config(
155175 path: &Path,
156176 config: &InstanceConfig,
@@ -357,7 +377,7 @@ fn spawn_ssh_io(
357377 let ssh_ready = Arc::new(AtomicBool::new(false));
358378 let input_tx_holder: Arc<Mutex<Option<Sender<VmInput>>>> = Arc::new(Mutex::new(None));
359379
360− let instance_path = instance_dir.join(INSTANCE_TOML);
380+ let instance_path = instance_dir.join(INSTANCE_TOML_FILENAME);
361381 let config_for_output = config.clone();
362382 let log_for_output = log_file.clone();
363383 let ssh_connected_for_output = ssh_connected.clone();
src/lib.rs+1
@@ -5,3 +5,4 @@ pub mod vm;
55pub mod vm_manager;
66
77pub use session_manager::{SessionError, SessionManager, SessionRecord};
8+pub mod config;
src/session_manager.rs+116−315
@@ -5,35 +5,43 @@ use std::{
55};
66
77use serde::{Deserialize, Serialize};
8−use time::OffsetDateTime;
9−use uuid::Uuid;
108
119pub const INSTANCE_DIR_NAME: &str = ".vibebox";
10+pub const GLOBAL_CACHE_DIR_NAME: &str = "vibebox";
1211pub const GLOBAL_DIR_NAME: &str = ".vibebox";
13−pub const SESSION_INDEX_FILENAME: &str = "sessions.toml";
12+pub const GLOBAL_SESSION_FILENAME: &str = "session.toml";
1413pub const SESSION_TEMP_PREFIX: &str = "sessions";
1514pub const SESSION_TOML_SUFFIX: &str = ".toml";
15+use crate::config::CONFIG_FILENAME;
16+pub const INSTANCE_TOML_FILENAME: &str = "instance.toml";
1617
1718#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1819pub struct SessionRecord {
19− pub id: Uuid,
2020 pub directory: PathBuf,
21− #[serde(with = "time::serde::rfc3339")]
22− pub last_active: OffsetDateTime,
2321 #[serde(default)]
24− pub ref_count: u64,
22+ pub id: Option<String>,
23+ #[serde(default)]
24+ pub last_active: Option<String>,
2525}
2626
2727#[derive(Debug, Default, Serialize, Deserialize)]
28−struct SessionIndex {
28+struct GlobalSessionIndex {
29+ #[serde(default)]
30+ directories: Vec<PathBuf>,
31+}
32+
33+#[derive(Debug, Default, Deserialize)]
34+struct InstanceMetadata {
2935 #[serde(default)]
30− sessions: Vec<SessionRecord>,
36+ id: Option<String>,
37+ #[serde(default)]
38+ last_active: Option<String>,
3139}
3240
3341#[derive(Debug)]
3442pub struct SessionManager {
3543 global_dir: PathBuf,
36− index_path: PathBuf,
44+ session_path: PathBuf,
3745}
3846
3947#[derive(Debug, thiserror::Error)]
@@ -44,12 +52,6 @@ pub enum SessionError {
4452 NonAbsoluteDirectory(PathBuf),
4553 #[error("Session directory does not exist: {0}")]
4654 MissingDirectory(PathBuf),
47− #[error("Session already exists for directory: {0}")]
48− DirectoryAlreadyHasSession(PathBuf),
49− #[error("Session not found: {0}")]
50− SessionNotFound(Uuid),
51− #[error("Ref count underflow for session: {0}")]
52− RefCountUnderflow(Uuid),
5355 #[error(transparent)]
5456 Io(#[from] std::io::Error),
5557 #[error(transparent)]
@@ -67,163 +69,55 @@ impl SessionManager {
6769 }
6870
6971 pub fn with_global_dir(global_dir: PathBuf) -> Self {
70− let index_path = global_dir.join(SESSION_INDEX_FILENAME);
72+ let session_path = global_dir.join(GLOBAL_SESSION_FILENAME);
7173 Self {
7274 global_dir,
73− index_path,
75+ session_path,
7476 }
7577 }
7678
7779 pub fn index_path(&self) -> &Path {
78− &self.index_path
79− }
80−
81− pub fn create_session(&self, directory: &Path) -> Result<SessionRecord, SessionError> {
82− let directory = self.normalize_directory(directory)?;
83− let mut index = self.read_index()?;
84−
85− if index.sessions.iter().any(|s| s.directory == directory) {
86− return Err(SessionError::DirectoryAlreadyHasSession(directory));
87− }
88−
89− let now = OffsetDateTime::now_utc();
90− let session = SessionRecord {
91− id: Uuid::now_v7(),
92− directory: directory.clone(),
93− last_active: now,
94− ref_count: 1,
95− };
96−
97− fs::create_dir_all(self.instance_dir_for(&directory))?;
98− index.sessions.push(session.clone());
99− self.write_index(&index)?;
100−
101− Ok(session)
80+ &self.session_path
10281 }
10382
104− pub fn get_or_create_session(&self, directory: &Path) -> Result<SessionRecord, SessionError> {
83+ pub fn update_global_sessions(&self, directory: &Path) -> Result<Vec<PathBuf>, SessionError> {
10584 let directory = self.normalize_directory(directory)?;
106− let mut index = self.read_index()?;
107−
108− if let Some(pos) = index.sessions.iter().position(|s| s.directory == directory) {
109− if !self
110− .instance_dir_for(&index.sessions[pos].directory)
111− .is_dir()
112− {
113− index.sessions.remove(pos);
114− } else {
115− let now = OffsetDateTime::now_utc();
116− let session = &mut index.sessions[pos];
117− session.ref_count = session.ref_count.saturating_add(1);
118− session.last_active = now;
119− let updated = session.clone();
120− self.write_index(&index)?;
121− return Ok(updated);
122− }
123− }
124−
125− let now = OffsetDateTime::now_utc();
126− let session = SessionRecord {
127− id: Uuid::now_v7(),
128− directory: directory.clone(),
129− last_active: now,
130− ref_count: 1,
131− };
132−
133− fs::create_dir_all(self.instance_dir_for(&directory))?;
134− index.sessions.push(session.clone());
135− self.write_index(&index)?;
85+ let mut index = self.read_global_index()?;
86+ let mut changed = false;
13687
137− Ok(session)
138− }
139−
140− pub fn list_sessions(&self) -> Result<Vec<SessionRecord>, SessionError> {
141− let mut index = self.read_index()?;
142− let removed = self.remove_orphans(&mut index);
88+ let removed = prune_invalid_dirs(&mut index);
14389 if removed > 0 {
144− self.write_index(&index)?;
90+ changed = true;
14591 }
146− Ok(index.sessions)
147− }
14892
149− pub fn delete_session(&self, id: Uuid) -> Result<bool, SessionError> {
150− let mut index = self.read_index()?;
151− let Some(pos) = index.sessions.iter().position(|s| s.id == id) else {
152− return Ok(false);
153− };
154−
155− let session = index.sessions.remove(pos);
156− let instance_dir = self.instance_dir_for(&session.directory);
157− match fs::remove_dir_all(&instance_dir) {
158− Ok(_) => {}
159− Err(err) if err.kind() == io::ErrorKind::NotFound => {}
160− Err(err) => return Err(err.into()),
93+ if is_vibebox_dir(&directory) && !index.directories.iter().any(|dir| dir == &directory) {
94+ index.directories.push(directory);
95+ changed = true;
16196 }
16297
163− self.write_index(&index)?;
164− Ok(true)
165− }
166−
167− pub fn bump_last_active(&self, id: Uuid) -> Result<SessionRecord, SessionError> {
168− let now = OffsetDateTime::now_utc();
169− self.update_session(id, |session| {
170− session.last_active = now;
171− Ok(())
172− })
173− }
174−
175− pub fn increment_ref_count(&self, id: Uuid) -> Result<SessionRecord, SessionError> {
176− let now = OffsetDateTime::now_utc();
177− self.update_session(id, |session| {
178− session.ref_count = session.ref_count.saturating_add(1);
179− session.last_active = now;
180− Ok(())
181− })
182− }
98+ if changed {
99+ self.write_global_index(&index)?;
100+ }
183101
184− pub fn decrement_ref_count(&self, id: Uuid) -> Result<SessionRecord, SessionError> {
185− let now = OffsetDateTime::now_utc();
186− self.update_session(id, |session| {
187− if session.ref_count == 0 {
188− return Err(SessionError::RefCountUnderflow(id));
189− }
190− session.ref_count -= 1;
191− session.last_active = now;
192− Ok(())
193− })
102+ Ok(index.directories)
194103 }
195104
196− pub fn cleanup_orphans(&self) -> Result<usize, SessionError> {
197− let mut index = self.read_index()?;
198− let removed = self.remove_orphans(&mut index);
105+ pub fn list_sessions(&self) -> Result<Vec<SessionRecord>, SessionError> {
106+ let mut index = self.read_global_index()?;
107+ let removed = prune_invalid_dirs(&mut index);
199108 if removed > 0 {
200− self.write_index(&index)?;
109+ self.write_global_index(&index)?;
201110 }
202− Ok(removed)
203− }
204−
205− fn update_session<F>(&self, id: Uuid, mut update: F) -> Result<SessionRecord, SessionError>
206− where
207− F: FnMut(&mut SessionRecord) -> Result<(), SessionError>,
208− {
209− let mut index = self.read_index()?;
210− let Some(pos) = index.sessions.iter().position(|s| s.id == id) else {
211− return Err(SessionError::SessionNotFound(id));
212− };
213−
214− if !self
215− .instance_dir_for(&index.sessions[pos].directory)
216− .is_dir()
217− {
218− index.sessions.remove(pos);
219− self.write_index(&index)?;
220− return Err(SessionError::SessionNotFound(id));
111+ let mut sessions = Vec::with_capacity(index.directories.len());
112+ for directory in index.directories {
113+ let meta = read_instance_metadata(&directory)?;
114+ sessions.push(SessionRecord {
115+ directory,
116+ id: meta.id,
117+ last_active: meta.last_active,
118+ });
221119 }
222−
223− update(&mut index.sessions[pos])?;
224− let updated = index.sessions[pos].clone();
225− self.write_index(&index)?;
226− Ok(updated)
120+ Ok(sessions)
227121 }
228122
229123 fn normalize_directory(&self, directory: &Path) -> Result<PathBuf, SessionError> {
@@ -236,34 +130,57 @@ impl SessionManager {
236130 Ok(directory.canonicalize()?)
237131 }
238132
239− fn instance_dir_for(&self, directory: &Path) -> PathBuf {
240− directory.join(INSTANCE_DIR_NAME)
241− }
242−
243− fn remove_orphans(&self, index: &mut SessionIndex) -> usize {
244− let before = index.sessions.len();
245− index
246− .sessions
247− .retain(|s| self.instance_dir_for(&s.directory).is_dir());
248− before - index.sessions.len()
249− }
250−
251− fn read_index(&self) -> Result<SessionIndex, SessionError> {
252− if !self.index_path.exists() {
253− return Ok(SessionIndex::default());
133+ fn read_global_index(&self) -> Result<GlobalSessionIndex, SessionError> {
134+ if !self.session_path.exists() {
135+ return Ok(GlobalSessionIndex::default());
254136 }
255− let content = fs::read_to_string(&self.index_path)?;
137+ let content = fs::read_to_string(&self.session_path)?;
256138 Ok(toml::from_str(&content)?)
257139 }
258140
259− fn write_index(&self, index: &SessionIndex) -> Result<(), SessionError> {
141+ fn write_global_index(&self, index: &GlobalSessionIndex) -> Result<(), SessionError> {
260142 fs::create_dir_all(&self.global_dir)?;
261143 let content = toml::to_string_pretty(index)?;
262− atomic_write(&self.index_path, content.as_bytes())?;
144+ atomic_write(&self.session_path, content.as_bytes())?;
263145 Ok(())
264146 }
265147}
266148
149+fn is_vibebox_dir(directory: &Path) -> bool {
150+ if !directory.is_absolute() {
151+ return false;
152+ }
153+ directory.join(CONFIG_FILENAME).is_file()
154+}
155+
156+fn prune_invalid_dirs(index: &mut GlobalSessionIndex) -> usize {
157+ let before = index.directories.len();
158+ index.directories.retain(|dir| is_vibebox_dir(dir));
159+ before - index.directories.len()
160+}
161+
162+fn read_instance_metadata(directory: &Path) -> Result<InstanceMetadata, SessionError> {
163+ let instance_path = directory
164+ .join(INSTANCE_DIR_NAME)
165+ .join(INSTANCE_TOML_FILENAME);
166+ if !instance_path.exists() {
167+ return Ok(InstanceMetadata::default());
168+ }
169+ let raw = fs::read_to_string(&instance_path)?;
170+ let mut meta: InstanceMetadata = toml::from_str(&raw)?;
171+ if let Some(id) = &meta.id {
172+ if id.trim().is_empty() {
173+ meta.id = None;
174+ }
175+ }
176+ if let Some(last_active) = &meta.last_active {
177+ if last_active.trim().is_empty() {
178+ meta.last_active = None;
179+ }
180+ }
181+ Ok(meta)
182+}
183+
267184fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
268185 let Some(parent) = path.parent() else {
269186 return Err(io::Error::new(
@@ -288,7 +205,6 @@ mod tests {
288205 use super::*;
289206 use std::fs;
290207 use tempfile::TempDir;
291− use time::OffsetDateTime;
292208
293209 fn manager(temp: &TempDir) -> SessionManager {
294210 SessionManager::with_global_dir(temp.path().join("global"))
@@ -301,104 +217,17 @@ mod tests {
301217 }
302218
303219 #[test]
304− fn create_session_writes_index_and_instance_dir() {
220+ fn update_global_sessions_adds_directory() {
305221 let temp = TempDir::new().unwrap();
306222 let mgr = manager(&temp);
307223 let project_dir = create_project_dir(&temp);
224+ fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap();
308225
309− let session = mgr.create_session(&project_dir).unwrap();
226+ let dirs = mgr.update_global_sessions(&project_dir).unwrap();
310227
228+ assert_eq!(dirs.len(), 1);
229+ assert_eq!(dirs[0], project_dir.canonicalize().unwrap());
311230 assert!(mgr.index_path().exists());
312− assert_eq!(session.directory, project_dir.canonicalize().unwrap());
313− assert_eq!(session.ref_count, 1);
314− assert!(project_dir.join(INSTANCE_DIR_NAME).is_dir());
315− }
316−
317− #[test]
318− fn create_session_rejects_non_absolute_directory() {
319− let temp = TempDir::new().unwrap();
320− let mgr = manager(&temp);
321−
322− let err = mgr.create_session(Path::new("relative/path")).unwrap_err();
323−
324− assert!(matches!(err, SessionError::NonAbsoluteDirectory(_)));
325− }
326−
327− #[test]
328− fn create_session_rejects_missing_directory() {
329− let temp = TempDir::new().unwrap();
330− let mgr = manager(&temp);
331− let missing = temp.path().join("missing");
332−
333− let err = mgr.create_session(&missing).unwrap_err();
334−
335− assert!(matches!(err, SessionError::MissingDirectory(_)));
336− }
337−
338− #[test]
339− fn create_session_rejects_duplicate_directory() {
340− let temp = TempDir::new().unwrap();
341− let mgr = manager(&temp);
342− let project_dir = create_project_dir(&temp);
343−
344− let _session = mgr.create_session(&project_dir).unwrap();
345− let err = mgr.create_session(&project_dir).unwrap_err();
346−
347− assert!(matches!(err, SessionError::DirectoryAlreadyHasSession(_)));
348− }
349−
350− #[test]
351− fn get_or_create_increments_ref_count_for_existing_session() {
352− let temp = TempDir::new().unwrap();
353− let mgr = manager(&temp);
354− let project_dir = create_project_dir(&temp);
355−
356− let first = mgr.create_session(&project_dir).unwrap();
357− let second = mgr.get_or_create_session(&project_dir).unwrap();
358−
359− assert_eq!(first.id, second.id);
360− assert_eq!(second.ref_count, 2);
361− }
362−
363− #[test]
364− fn decrement_ref_count_errors_on_underflow() {
365− let temp = TempDir::new().unwrap();
366− let mgr = manager(&temp);
367− let project_dir = create_project_dir(&temp);
368−
369− let session = mgr.create_session(&project_dir).unwrap();
370− let session = mgr.decrement_ref_count(session.id).unwrap();
371− assert_eq!(session.ref_count, 0);
372−
373− let err = mgr.decrement_ref_count(session.id).unwrap_err();
374− assert!(matches!(err, SessionError::RefCountUnderflow(_)));
375− }
376−
377− #[test]
378− fn list_sessions_cleans_orphans() {
379− let temp = TempDir::new().unwrap();
380− let mgr = manager(&temp);
381− let project_dir = create_project_dir(&temp);
382−
383− let _session = mgr.create_session(&project_dir).unwrap();
384− fs::remove_dir_all(project_dir.join(INSTANCE_DIR_NAME)).unwrap();
385−
386− let sessions = mgr.list_sessions().unwrap();
387− assert!(sessions.is_empty());
388− }
389−
390− #[test]
391− fn delete_session_removes_instance_dir_and_index() {
392− let temp = TempDir::new().unwrap();
393− let mgr = manager(&temp);
394− let project_dir = create_project_dir(&temp);
395−
396− let session = mgr.create_session(&project_dir).unwrap();
397− let removed = mgr.delete_session(session.id).unwrap();
398−
399− assert!(removed);
400− assert!(!project_dir.join(INSTANCE_DIR_NAME).exists());
401− assert!(mgr.list_sessions().unwrap().is_empty());
402231 }
403232
404233 #[test]
@@ -415,73 +244,45 @@ mod tests {
415244 }
416245
417246 #[test]
418− fn bump_last_active_updates_timestamp() {
419− let temp = TempDir::new().unwrap();
420− let mgr = manager(&temp);
421− let project_dir = create_project_dir(&temp);
422−
423− let session = mgr.create_session(&project_dir).unwrap();
424− let before = session.last_active;
425− std::thread::sleep(std::time::Duration::from_millis(5));
426− let updated = mgr.bump_last_active(session.id).unwrap();
427− let now = OffsetDateTime::now_utc();
428−
429− assert!(updated.last_active >= before);
430− assert!(updated.last_active <= now);
431− }
432−
433− #[test]
434− fn cleanup_orphans_returns_removed_count() {
247+ fn update_global_sessions_removes_missing_vibebox_toml() {
435248 let temp = TempDir::new().unwrap();
436249 let mgr = manager(&temp);
437250 let project_dir = create_project_dir(&temp);
251+ fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap();
438252
439− let _session = mgr.create_session(&project_dir).unwrap();
440− fs::remove_dir_all(project_dir.join(INSTANCE_DIR_NAME)).unwrap();
253+ let _ = mgr.update_global_sessions(&project_dir).unwrap();
254+ fs::remove_file(project_dir.join(VIBEBOX_CONFIG_FILENAME)).unwrap();
441255
442− let removed = mgr.cleanup_orphans().unwrap();
443− assert_eq!(removed, 1);
444− assert!(mgr.list_sessions().unwrap().is_empty());
256+ let dirs = mgr.update_global_sessions(&project_dir).unwrap();
257+ assert!(dirs.is_empty());
445258 }
446259
447260 #[test]
448− fn increment_ref_count_updates_last_active() {
261+ fn list_sessions_reads_instance_metadata() {
449262 let temp = TempDir::new().unwrap();
450263 let mgr = manager(&temp);
451264 let project_dir = create_project_dir(&temp);
265+ fs::write(project_dir.join(VIBEBOX_CONFIG_FILENAME), "").unwrap();
452266
453− let session = mgr.create_session(&project_dir).unwrap();
454− std::thread::sleep(std::time::Duration::from_millis(5));
455− let updated = mgr.increment_ref_count(session.id).unwrap();
456−
457− assert_eq!(updated.ref_count, session.ref_count + 1);
458− assert!(updated.last_active >= session.last_active);
459− }
460−
461− #[test]
462− fn decrement_ref_count_updates_last_active() {
463− let temp = TempDir::new().unwrap();
464− let mgr = manager(&temp);
465− let project_dir = create_project_dir(&temp);
466−
467− let session = mgr.create_session(&project_dir).unwrap();
468− std::thread::sleep(std::time::Duration::from_millis(5));
469− let updated = mgr.decrement_ref_count(session.id).unwrap();
470−
471− assert_eq!(updated.ref_count, session.ref_count - 1);
472− assert!(updated.last_active >= session.last_active);
473− }
474−
475− #[test]
476− fn list_sessions_returns_active_sessions() {
477− let temp = TempDir::new().unwrap();
478− let mgr = manager(&temp);
479− let project_dir = create_project_dir(&temp);
267+ let instance_dir = project_dir.join(INSTANCE_DIR_NAME);
268+ fs::create_dir_all(&instance_dir).unwrap();
269+ fs::write(
270+ instance_dir.join(INSTANCE_TOML_FILENAME),
271+ "id = \"019bf290-cccc-7c23-ba1d-dce7e6d40693\"\nlast_active = \"2026-02-07T05:00:00Z\"\n",
272+ )
273+ .unwrap();
480274
481− let session = mgr.create_session(&project_dir).unwrap();
275+ let _ = mgr.update_global_sessions(&project_dir).unwrap();
482276 let sessions = mgr.list_sessions().unwrap();
483277
484278 assert_eq!(sessions.len(), 1);
485− assert_eq!(sessions[0].id, session.id);
279+ assert_eq!(
280+ sessions[0].id.as_deref(),
281+ Some("019bf290-cccc-7c23-ba1d-dce7e6d40693")
282+ );
283+ assert_eq!(
284+ sessions[0].last_active.as_deref(),
285+ Some("2026-02-07T05:00:00Z")
286+ );
486287 }
487288}
src/vm.rs+2−2
@@ -1,4 +1,4 @@
1−use crate::session_manager::INSTANCE_DIR_NAME;
1+use crate::session_manager::{GLOBAL_CACHE_DIR_NAME, INSTANCE_DIR_NAME};
22use std::{
33 env,
44 ffi::OsString,
@@ -146,7 +146,7 @@ where
146146 let cache_home = env::var("XDG_CACHE_HOME")
147147 .map(PathBuf::from)
148148 .unwrap_or_else(|_| home.join(".cache"));
149− let cache_dir = cache_home.join("vibe");
149+ let cache_dir = cache_home.join(GLOBAL_CACHE_DIR_NAME);
150150 let guest_mise_cache = cache_dir.join(".guest-mise-cache");
151151
152152 let instance_dir = project_root.join(INSTANCE_DIR_NAME);
vibebox.tomlnew file

No changes to the content.