use std::path::{Path, PathBuf};
use anyhow::Context as _;
pub fn discover_claude_mpm_projects(registry_path: &Path) -> anyhow::Result<Vec<PathBuf>> {
if !registry_path.exists() {
return Ok(vec![]);
}
let conn = rusqlite::Connection::open_with_flags(
registry_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.with_context(|| format!("opening registry DB at {}", registry_path.display()))?;
let mut stmt = conn.prepare(
"SELECT project_path, MAX(last_active) AS la \
FROM sessions \
GROUP BY project_path \
ORDER BY la DESC",
)?;
let raw: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(0))?
.filter_map(|r| r.ok())
.collect();
let results = raw
.into_iter()
.map(PathBuf::from)
.filter(|p| p.is_dir() && has_live_pause_file(p))
.collect();
Ok(results)
}
pub fn default_registry_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude-mpm")
.join("session-registry.db")
}
fn has_live_pause_file(project_dir: &Path) -> bool {
let sessions_dir = project_dir.join(".claude-mpm").join("sessions");
if !sessions_dir.is_dir() {
return false;
}
if sessions_dir.join("LATEST-SESSION.txt").exists() {
return true;
}
std::fs::read_dir(&sessions_dir)
.map(|mut rd| {
rd.any(|e| {
e.ok()
.and_then(|e| e.file_name().into_string().ok())
.map(|n| n.starts_with("session-") && n.ends_with(".json"))
.unwrap_or(false)
})
})
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
use tempfile::TempDir;
fn make_registry(entries: &[(&str, &str)]) -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("session-registry.db");
let conn = Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE sessions (
session_id TEXT,
project_path TEXT,
project_name TEXT,
started_at TEXT,
last_active TEXT,
status TEXT,
pid INTEGER
)",
)
.unwrap();
for (path, last_active) in entries {
conn.execute(
"INSERT INTO sessions (session_id, project_path, last_active, status) \
VALUES (?1, ?2, ?3, 'paused')",
rusqlite::params![format!("sess-{path}"), path, last_active],
)
.unwrap();
}
(dir, db_path)
}
fn make_pause_file(project_dir: &Path) {
let sessions = project_dir.join(".claude-mpm").join("sessions");
std::fs::create_dir_all(&sessions).unwrap();
std::fs::write(sessions.join("LATEST-SESSION.txt"), "pointer").unwrap();
}
#[test]
fn discover_returns_empty_when_db_missing() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("nonexistent.db");
let result = discover_claude_mpm_projects(&missing).unwrap();
assert!(result.is_empty(), "missing DB should return empty vec");
}
#[test]
fn discover_filters_missing_dirs() {
let base = TempDir::new().unwrap();
let real_dir = base.path().join("real-project");
std::fs::create_dir_all(&real_dir).unwrap();
make_pause_file(&real_dir);
let ghost = base.path().join("deleted-project");
let (_db_dir, db_path) = make_registry(&[
(real_dir.to_str().unwrap(), "2026-06-27T10:00:00Z"),
(ghost.to_str().unwrap(), "2026-06-27T09:00:00Z"),
]);
let result = discover_claude_mpm_projects(&db_path).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0], real_dir);
}
#[test]
fn discover_orders_newest_first() {
let base = TempDir::new().unwrap();
let older = base.path().join("older");
let newer = base.path().join("newer");
std::fs::create_dir_all(&older).unwrap();
std::fs::create_dir_all(&newer).unwrap();
make_pause_file(&older);
make_pause_file(&newer);
let (_db_dir, db_path) = make_registry(&[
(older.to_str().unwrap(), "2026-06-26T08:00:00Z"),
(newer.to_str().unwrap(), "2026-06-27T10:00:00Z"),
]);
let result = discover_claude_mpm_projects(&db_path).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], newer);
assert_eq!(result[1], older);
}
#[test]
fn discover_deduplicates_same_path() {
let base = TempDir::new().unwrap();
let proj = base.path().join("project");
std::fs::create_dir_all(&proj).unwrap();
make_pause_file(&proj);
let path_str = proj.to_str().unwrap();
let (_db_dir, db_path) = make_registry(&[
(path_str, "2026-06-27T10:00:00Z"),
(path_str, "2026-06-26T09:00:00Z"),
]);
let result = discover_claude_mpm_projects(&db_path).unwrap();
assert_eq!(result.len(), 1, "duplicate paths must be deduplicated");
}
}