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;