1use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7pub fn kaizen_dir() -> Option<PathBuf> {
9 std::env::var("KAIZEN_HOME")
10 .ok()
11 .map(PathBuf::from)
12 .or_else(|| {
13 std::env::var("HOME")
14 .ok()
15 .map(|home| PathBuf::from(home).join(".kaizen"))
16 })
17}
18
19pub fn workspace_slug(path: &Path) -> String {
23 path.to_string_lossy()
24 .trim_start_matches('/')
25 .replace('/', "-")
26}
27
28pub fn cursor_slug(path: &Path) -> String {
33 path.to_string_lossy()
34 .trim_start_matches('/')
35 .replace(['/', '.'], "-")
36}
37
38pub fn claude_code_slug(path: &Path) -> String {
43 let s = path.to_string_lossy();
44 let with_leading = if let Some(rest) = s.strip_prefix('/') {
45 format!("-{rest}")
46 } else {
47 s.into_owned()
48 };
49 with_leading.replace(['/', '.'], "-")
50}
51
52pub fn project_data_dir(workspace: &Path) -> Result<PathBuf> {
54 let home = kaizen_dir().ok_or_else(|| anyhow::anyhow!("KAIZEN_HOME / HOME unset"))?;
55 let canon = std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf());
56 let slug = workspace_slug(&canon);
57 let dir = home.join("projects").join(slug);
58 std::fs::create_dir_all(&dir)?;
59 Ok(dir)
60}
61
62pub fn canonical(path: &Path) -> PathBuf {
63 std::fs::canonicalize(path).unwrap_or_else(|_| absolute(path))
64}
65
66fn absolute(path: &Path) -> PathBuf {
67 if path.is_absolute() {
68 return path.to_path_buf();
69 }
70 std::env::current_dir()
71 .map(|cwd| cwd.join(path))
72 .unwrap_or_else(|_| path.to_path_buf())
73}
74
75#[cfg(test)]
76pub(crate) mod test_lock {
77 use std::sync::{Mutex, OnceLock};
78
79 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
80
81 pub fn global() -> &'static Mutex<()> {
82 LOCK.get_or_init(|| Mutex::new(()))
83 }
84}