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// Resource directories.
31//
32// Layout under the global base directory:
33//
34//   ~/.zig/
35//     resources/
36//       _shared/                    files advertised to every workflow
37//       <workflow-name>/            files advertised to a single named workflow
38//
39// Project-local resources live under `<git-root>/.zig/resources/` and are
40// discovered by walking up from the current working directory until a git
41// root is found (matching the convention used for session storage).
42// =====================================================================
43
44/// Return the global resources directory derived from a given home directory.
45pub fn global_resources_dir_from(home: &Path) -> PathBuf {
46    home.join(".zig").join("resources")
47}
48
49/// Return the global resources directory: `~/.zig/resources/`.
50/// Returns `None` if the HOME environment variable is not set.
51pub fn global_resources_dir() -> Option<PathBuf> {
52    std::env::var("HOME")
53        .ok()
54        .map(|h| global_resources_dir_from(Path::new(&h)))
55}
56
57/// Return the per-workflow global resources directory: `~/.zig/resources/<name>/`.
58pub fn global_resources_for(workflow: &str) -> Option<PathBuf> {
59    global_resources_dir().map(|d| d.join(workflow))
60}
61
62/// Return the shared global resources directory: `~/.zig/resources/_shared/`.
63///
64/// Files placed here are advertised to every workflow regardless of name.
65pub fn global_shared_resources_dir() -> Option<PathBuf> {
66    global_resources_dir().map(|d| d.join("_shared"))
67}
68
69/// Ensure the global resources directory (or a child of it) exists.
70pub fn ensure_global_resources_dir(child: Option<&str>) -> Result<PathBuf, ZigError> {
71    let mut dir = global_resources_dir()
72        .ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
73    if let Some(c) = child {
74        dir = dir.join(c);
75    }
76    if !dir.exists() {
77        std::fs::create_dir_all(&dir)
78            .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", dir.display())))?;
79    }
80    Ok(dir)
81}
82
83/// Walk up from `start` looking for a `.zig/resources` directory. Stops at the
84/// containing git repository root (matching `find_git_root`'s discovery
85/// boundary), or returns the directory in `start` itself if it exists.
86///
87/// Returns `None` if no such directory is found before hitting the git root
88/// or the filesystem root.
89pub fn cwd_resources_dir_from(start: &Path) -> Option<PathBuf> {
90    let mut current = start;
91    let stop = find_git_root(start);
92
93    loop {
94        let candidate = current.join(".zig").join("resources");
95        if candidate.is_dir() {
96            return Some(candidate);
97        }
98        if let Some(ref root) = stop {
99            if current == root.as_path() {
100                return None;
101            }
102        }
103        match current.parent() {
104            Some(p) => current = p,
105            None => return None,
106        }
107    }
108}
109
110/// Walk up from the process's current working directory looking for a
111/// `.zig/resources` directory. See [`cwd_resources_dir_from`].
112pub fn cwd_resources_dir() -> Option<PathBuf> {
113    let cwd = std::env::current_dir().ok()?;
114    cwd_resources_dir_from(&cwd)
115}
116
117// =====================================================================
118// Session storage paths.
119//
120// Layout mirrors zag (`zag-agent/src/config.rs:183` `resolve_project_dir`):
121//
122//   ~/.zig/
123//     projects/<sanitized-project-path>/logs/
124//                                       index.json
125//                                       sessions/<id>.jsonl
126//     sessions_index.json     (global cross-project index)
127//
128// Keeping this layout byte-for-byte aligned with `~/.zag/` is intentional
129// so future changes to zag's session/listen architecture can be mirrored
130// into zig with minimal churn.
131// =====================================================================
132
133/// Return the global zig base directory: `~/.zig/`.
134pub fn global_base_dir() -> Option<PathBuf> {
135    std::env::var("HOME")
136        .ok()
137        .map(|h| Path::new(&h).join(".zig"))
138}
139
140/// Sanitize an absolute path into a directory name.
141///
142/// Strips leading `/` and replaces remaining `/` with `-`. Mirrors zag's
143/// `Config::sanitize_path` (`zag-agent/src/config.rs:179`).
144pub fn sanitize_project_path(path: &str) -> String {
145    path.trim_start_matches('/').replace('/', "-")
146}
147
148/// Find the git repository root containing `start`, walking parents.
149fn find_git_root(start: &Path) -> Option<PathBuf> {
150    let mut current = start;
151    loop {
152        if current.join(".git").exists() {
153            return Some(current.to_path_buf());
154        }
155        current = current.parent()?;
156    }
157}
158
159/// Resolve the project directory for session storage.
160///
161/// Mirrors zag's `Config::resolve_project_dir` (`zag-agent/src/config.rs:188`):
162///   1. If `root` is provided, sanitize it directly.
163///   2. Otherwise locate the git repository root containing `cwd`.
164///   3. Otherwise fall back to the global base directory (no project subdir).
165pub fn project_dir(root: Option<&str>) -> Option<PathBuf> {
166    let base = global_base_dir()?;
167    if let Some(r) = root {
168        return Some(base.join("projects").join(sanitize_project_path(r)));
169    }
170    let cwd = std::env::current_dir().ok()?;
171    if let Some(git_root) = find_git_root(&cwd) {
172        let sanitized = sanitize_project_path(&git_root.to_string_lossy());
173        return Some(base.join("projects").join(sanitized));
174    }
175    Some(base)
176}
177
178/// Return the per-project logs directory: `<project>/logs/`.
179pub fn project_logs_dir(root: Option<&str>) -> Option<PathBuf> {
180    project_dir(root).map(|p| p.join("logs"))
181}
182
183/// Return the per-project sessions directory: `<project>/logs/sessions/`.
184pub fn project_sessions_dir(root: Option<&str>) -> Option<PathBuf> {
185    project_logs_dir(root).map(|p| p.join("sessions"))
186}
187
188/// Return the per-project index file: `<project>/logs/index.json`.
189pub fn project_index_path(root: Option<&str>) -> Option<PathBuf> {
190    project_logs_dir(root).map(|p| p.join("index.json"))
191}
192
193/// Return the global cross-project session index: `~/.zig/sessions_index.json`.
194pub fn global_sessions_index_path() -> Option<PathBuf> {
195    global_base_dir().map(|p| p.join("sessions_index.json"))
196}
197
198/// Ensure the per-project sessions directory exists, creating it if needed.
199pub fn ensure_project_sessions_dir(root: Option<&str>) -> Result<PathBuf, ZigError> {
200    let dir = project_sessions_dir(root)
201        .ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
202    if !dir.exists() {
203        std::fs::create_dir_all(&dir)
204            .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", dir.display())))?;
205    }
206    Ok(dir)
207}
208
209#[cfg(test)]
210#[path = "paths_tests.rs"]
211mod tests;