1use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7pub use crate::core::paths::{canonical, kaizen_dir, project_data_dir};
8
9pub fn resolve(path: Option<&Path>) -> Result<PathBuf> {
10 let root = path
11 .map(Path::to_path_buf)
12 .map(Ok)
13 .unwrap_or_else(std::env::current_dir)?;
14 let canonical = canonical(&root);
15 let _ = crate::core::machine_registry::upsert_from_resolve(&canonical);
16 if let Ok(data_dir) = project_data_dir(&canonical)
17 && let Err(e) = crate::core::migrate_home::migrate_legacy_in_repo(&canonical, &data_dir)
18 {
19 tracing::warn!("legacy migration failed: {e}");
20 }
21 Ok(canonical)
22}
23
24pub fn machine_workspaces(seed: Option<&Path>) -> Result<Vec<PathBuf>> {
25 let seed = seed.map(canonical);
26 let mut roots = registry_entries()?;
27 if let Some(path) = seed.as_ref() {
28 push_unique(&mut roots, path.clone());
29 }
30 roots.retain(|p| {
31 if seed.as_ref() == Some(p) {
32 return true;
33 }
34 p.exists()
35 && (db_path(p).ok().is_some_and(|d| d.exists())
36 || crate::core::machine_registry::is_registered(p))
37 });
38 if roots.is_empty()
39 && let Some(path) = seed
40 {
41 roots.push(path);
42 }
43 Ok(roots)
44}
45
46pub fn db_path(workspace: &Path) -> Result<PathBuf> {
47 Ok(project_data_dir(workspace)?.join("kaizen.db"))
48}
49
50fn registry_entries() -> Result<Vec<PathBuf>> {
51 crate::core::machine_registry::list_paths()
52}
53
54fn push_unique(roots: &mut Vec<PathBuf>, path: PathBuf) {
55 if !roots.iter().any(|row| row == &path) {
56 roots.push(path);
57 }
58}
59
60fn slug_match(paths: &[PathBuf], name: &str) -> Vec<PathBuf> {
61 paths
62 .iter()
63 .filter(|p| crate::core::paths::workspace_slug(p) == name)
64 .cloned()
65 .collect()
66}
67
68fn seg_match(paths: &[PathBuf], name: &str) -> Vec<PathBuf> {
69 paths
70 .iter()
71 .filter(|p| p.file_name().and_then(|n| n.to_str()) == Some(name))
72 .cloned()
73 .collect()
74}
75
76fn ambiguous_error(name: &str, matches: &[PathBuf]) -> anyhow::Error {
77 let list = matches
78 .iter()
79 .map(|p| format!(" {}", p.display()))
80 .collect::<Vec<_>>()
81 .join("\n");
82 anyhow::anyhow!(
83 "ambiguous project '{name}'. matches:\n{list}\nuse --workspace <path> or the slug."
84 )
85}
86
87pub fn resolve_project_name(name: &str) -> Result<PathBuf> {
95 let paths = crate::core::machine_registry::list_paths()?;
96 let slugs = slug_match(&paths, name);
97 if slugs.len() == 1 {
98 return Ok(slugs.into_iter().next().unwrap());
99 }
100 let segs = seg_match(&paths, name);
101 match segs.len() {
102 1 => Ok(segs.into_iter().next().unwrap()),
103 0 => anyhow::bail!(
104 "unknown project '{name}'. run 'kaizen projects list' to see registered projects."
105 ),
106 _ => Err(ambiguous_error(name, &segs)),
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::core::paths::test_lock;
114 use tempfile::TempDir;
115
116 #[test]
117 fn registry_round_trip() {
118 let _guard = test_lock::global().lock().unwrap();
119 let home = TempDir::new().unwrap();
120 let ws = home.path().join("repo");
121 std::fs::create_dir_all(&ws).unwrap();
122 unsafe { std::env::set_var("KAIZEN_HOME", home.path().join(".kaizen")) };
123 let first = resolve(Some(&ws)).unwrap();
124 let rows = machine_workspaces(Some(&first)).unwrap();
125 assert_eq!(rows, vec![first]);
126 unsafe { std::env::remove_var("KAIZEN_HOME") };
127 }
128
129 #[test]
130 fn resolve_project_name_no_match() {
131 let _guard = test_lock::global().lock().unwrap();
132 let home = TempDir::new().unwrap();
133 unsafe { std::env::set_var("KAIZEN_HOME", home.path().join(".kaizen")) };
134 let err = resolve_project_name("nonexistent").unwrap_err();
135 assert!(err.to_string().contains("unknown project"));
136 unsafe { std::env::remove_var("KAIZEN_HOME") };
137 }
138}