Skip to main content

zig_core/
paths.rs

1use std::path::{Path, PathBuf};
2
3use crate::error::ZigError;
4
5/// Return the global workflows directory derived from a given home directory.
6pub fn global_workflows_dir_from(home: &Path) -> PathBuf {
7    home.join(".zig").join("workflows")
8}
9
10/// Return the global workflows directory: `~/.zig/workflows/`.
11/// Returns `None` if the HOME environment variable is not set.
12pub fn global_workflows_dir() -> Option<PathBuf> {
13    std::env::var("HOME")
14        .ok()
15        .map(|home| global_workflows_dir_from(Path::new(&home)))
16}
17
18/// Ensure the global workflows directory exists, creating it if necessary.
19pub fn ensure_global_workflows_dir() -> Result<PathBuf, ZigError> {
20    let dir = global_workflows_dir()
21        .ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
22    if !dir.exists() {
23        std::fs::create_dir_all(&dir)
24            .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", dir.display())))?;
25    }
26    Ok(dir)
27}
28
29// =====================================================================
30// Session storage paths.
31//
32// Layout mirrors zag (`zag-agent/src/config.rs:183` `resolve_project_dir`):
33//
34//   ~/.zig/
35//     projects/<sanitized-project-path>/logs/
36//                                       index.json
37//                                       sessions/<id>.jsonl
38//     sessions_index.json     (global cross-project index)
39//
40// Keeping this layout byte-for-byte aligned with `~/.zag/` is intentional
41// so future changes to zag's session/listen architecture can be mirrored
42// into zig with minimal churn.
43// =====================================================================
44
45/// Return the global zig base directory: `~/.zig/`.
46pub fn global_base_dir() -> Option<PathBuf> {
47    std::env::var("HOME")
48        .ok()
49        .map(|h| Path::new(&h).join(".zig"))
50}
51
52/// Sanitize an absolute path into a directory name.
53///
54/// Strips leading `/` and replaces remaining `/` with `-`. Mirrors zag's
55/// `Config::sanitize_path` (`zag-agent/src/config.rs:179`).
56pub fn sanitize_project_path(path: &str) -> String {
57    path.trim_start_matches('/').replace('/', "-")
58}
59
60/// Find the git repository root containing `start`, walking parents.
61fn find_git_root(start: &Path) -> Option<PathBuf> {
62    let mut current = start;
63    loop {
64        if current.join(".git").exists() {
65            return Some(current.to_path_buf());
66        }
67        current = current.parent()?;
68    }
69}
70
71/// Resolve the project directory for session storage.
72///
73/// Mirrors zag's `Config::resolve_project_dir` (`zag-agent/src/config.rs:188`):
74///   1. If `root` is provided, sanitize it directly.
75///   2. Otherwise locate the git repository root containing `cwd`.
76///   3. Otherwise fall back to the global base directory (no project subdir).
77pub fn project_dir(root: Option<&str>) -> Option<PathBuf> {
78    let base = global_base_dir()?;
79    if let Some(r) = root {
80        return Some(base.join("projects").join(sanitize_project_path(r)));
81    }
82    let cwd = std::env::current_dir().ok()?;
83    if let Some(git_root) = find_git_root(&cwd) {
84        let sanitized = sanitize_project_path(&git_root.to_string_lossy());
85        return Some(base.join("projects").join(sanitized));
86    }
87    Some(base)
88}
89
90/// Return the per-project logs directory: `<project>/logs/`.
91pub fn project_logs_dir(root: Option<&str>) -> Option<PathBuf> {
92    project_dir(root).map(|p| p.join("logs"))
93}
94
95/// Return the per-project sessions directory: `<project>/logs/sessions/`.
96pub fn project_sessions_dir(root: Option<&str>) -> Option<PathBuf> {
97    project_logs_dir(root).map(|p| p.join("sessions"))
98}
99
100/// Return the per-project index file: `<project>/logs/index.json`.
101pub fn project_index_path(root: Option<&str>) -> Option<PathBuf> {
102    project_logs_dir(root).map(|p| p.join("index.json"))
103}
104
105/// Return the global cross-project session index: `~/.zig/sessions_index.json`.
106pub fn global_sessions_index_path() -> Option<PathBuf> {
107    global_base_dir().map(|p| p.join("sessions_index.json"))
108}
109
110/// Ensure the per-project sessions directory exists, creating it if needed.
111pub fn ensure_project_sessions_dir(root: Option<&str>) -> Result<PathBuf, ZigError> {
112    let dir = project_sessions_dir(root)
113        .ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
114    if !dir.exists() {
115        std::fs::create_dir_all(&dir)
116            .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", dir.display())))?;
117    }
118    Ok(dir)
119}
120
121#[cfg(test)]
122#[path = "paths_tests.rs"]
123mod tests;