omne_cli/volume.rs
1//! Volume root detection + v2 path helpers.
2//!
3//! `find_omne_root` walks up from a starting directory looking for an
4//! ancestor that contains a `.omne/` subdirectory. `upgrade` and
5//! `validate` use this so they work from any subdirectory of a volume;
6//! `init` deliberately does **not** walk up (R13 — `init` creates in
7//! the current directory only).
8//!
9//! The remaining helpers derive v2 volume paths from a root: static
10//! `lib/{cfg,docs}` content, runtime `var/runs/<run_id>/{events.jsonl,
11//! nodes/*}`, and per-run worktrees under `wt/<run_id>`. Centralizing
12//! the layout here keeps the scaffold, runner, validator, and
13//! event-log modules from drifting on directory names.
14//!
15//! The Python module's `is_mounted()` helper is **not ported** per R12:
16//! the submodule/mount model was removed when releases-based
17//! distribution replaced the submodule architecture.
18
19#![allow(dead_code)]
20
21use std::path::{Path, PathBuf};
22
23/// `.omne/` root under the volume.
24pub fn omne_dir(root: &Path) -> PathBuf {
25 root.join(".omne")
26}
27
28/// `.omne/core/` — kernel tarball extraction target.
29pub fn core_dir(root: &Path) -> PathBuf {
30 omne_dir(root).join("core")
31}
32
33/// `.omne/dist/` — distro tarball extraction target (v1-renamed from
34/// `image/`). Distro skill discovery and docs-baseline seeding both
35/// key on this path.
36pub fn dist_dir(root: &Path) -> PathBuf {
37 omne_dir(root).join("dist")
38}
39
40/// `.omne/lib/cfg/` — per-volume static configuration.
41pub fn cfg_dir(root: &Path) -> PathBuf {
42 omne_dir(root).join("lib").join("cfg")
43}
44
45/// `.omne/lib/docs/` — knowledge-base root.
46pub fn docs_dir(root: &Path) -> PathBuf {
47 omne_dir(root).join("lib").join("docs")
48}
49
50/// `.omne/lib/docs/index.md` — the docs baseline entry point.
51pub fn docs_baseline(root: &Path) -> PathBuf {
52 docs_dir(root).join("index.md")
53}
54
55/// `.omne/var/` — runtime state root. Contains `runs/` and the
56/// monotonic-ULID lock file.
57pub fn var_dir(root: &Path) -> PathBuf {
58 omne_dir(root).join("var")
59}
60
61/// `.omne/var/.ulid-last` — advisory-locked file holding the last
62/// allocated ULID for cross-process monotonicity.
63pub fn ulid_lock_path(root: &Path) -> PathBuf {
64 var_dir(root).join(".ulid-last")
65}
66
67/// `.omne/var/runs/` — per-run directories live under this.
68pub fn runs_dir(root: &Path) -> PathBuf {
69 var_dir(root).join("runs")
70}
71
72/// `.omne/var/runs/<run_id>/` — a specific run's state directory.
73pub fn run_dir(root: &Path, run_id: &str) -> PathBuf {
74 runs_dir(root).join(run_id)
75}
76
77/// `.omne/var/runs/<run_id>/events.jsonl` — the per-run event log.
78pub fn events_log_path(root: &Path, run_id: &str) -> PathBuf {
79 run_dir(root, run_id).join("events.jsonl")
80}
81
82/// `.omne/var/runs/<run_id>/nodes/` — per-node capture directory.
83pub fn nodes_dir(root: &Path, run_id: &str) -> PathBuf {
84 run_dir(root, run_id).join("nodes")
85}
86
87/// Forward-slash, volume-root-relative wire path for a node's capture
88/// file. Always `.omne/var/runs/<run_id>/nodes/<node_id>.out` with
89/// forward slashes regardless of host OS — written into the
90/// `output_path` field of `node.completed` events so Windows and Unix
91/// readers see the same wire shape.
92pub fn node_capture_wire_path(run_id: &str, node_id: &str) -> String {
93 format!(".omne/var/runs/{run_id}/nodes/{node_id}.out")
94}
95
96/// `.omne/wt/` — worktree root.
97pub fn wt_dir(root: &Path) -> PathBuf {
98 omne_dir(root).join("wt")
99}
100
101/// `.omne/wt/<run_id>/` — the detached worktree for a specific run.
102pub fn wt_for(root: &Path, run_id: &str) -> PathBuf {
103 wt_dir(root).join(run_id)
104}
105
106/// Walk up from `start` looking for an ancestor that contains a
107/// `.omne/` subdirectory. Returns the first match, or `None` when the
108/// walk reaches the filesystem root without finding one.
109///
110/// `start` is canonicalized first so relative paths, `..` segments,
111/// and symlinks are resolved before the walk begins. If `start` does
112/// not exist or cannot be canonicalized, the function returns `None`
113/// — the caller surfaces this as `CliError::NotAVolume` at the
114/// command boundary.
115pub fn find_omne_root(start: &Path) -> Option<PathBuf> {
116 let mut current = start.canonicalize().ok()?;
117 loop {
118 if current.join(".omne").is_dir() {
119 return Some(current);
120 }
121 if !current.pop() {
122 return None;
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::fs;
130
131 use tempfile::TempDir;
132
133 use super::*;
134
135 #[test]
136 fn finds_root_when_omne_exists_at_start() {
137 let tmp = TempDir::new().expect("create tempdir");
138 fs::create_dir(tmp.path().join(".omne")).expect("create .omne");
139
140 let found = find_omne_root(tmp.path());
141 let expected = tmp.path().canonicalize().expect("canonicalize tmpdir");
142 assert_eq!(found, Some(expected));
143 }
144
145 #[test]
146 fn finds_root_from_subdirectory() {
147 let tmp = TempDir::new().expect("create tempdir");
148 fs::create_dir(tmp.path().join(".omne")).expect("create .omne");
149
150 let deep = tmp.path().join("src").join("deep");
151 fs::create_dir_all(&deep).expect("create deep subdir");
152
153 let found = find_omne_root(&deep);
154 let expected = tmp.path().canonicalize().expect("canonicalize tmpdir");
155 assert_eq!(found, Some(expected));
156 }
157
158 #[test]
159 fn walk_up_matches_actual_ancestor_chain() {
160 // Do NOT create `.omne` in the tempdir itself. Derive the
161 // expected result from the canonicalized ancestor chain so the
162 // test stays hermetic even if some ambient ancestor (e.g.
163 // `/tmp/.omne` on a developer machine) already contains a
164 // `.omne` directory. In a clean environment this still asserts
165 // `None`; on a contaminated environment it asserts the
166 // contaminating ancestor's path instead of failing.
167 let tmp = TempDir::new().expect("create tempdir");
168 let canonical_start = tmp.path().canonicalize().expect("canonicalize tmpdir");
169 let expected = canonical_start
170 .ancestors()
171 .find(|ancestor| ancestor.join(".omne").is_dir())
172 .map(Path::to_path_buf);
173
174 let found = find_omne_root(tmp.path());
175 assert_eq!(found, expected);
176 }
177
178 #[test]
179 fn returns_none_when_start_does_not_exist() {
180 let tmp = TempDir::new().expect("create tempdir");
181 let missing = tmp.path().join("does-not-exist");
182
183 let found = find_omne_root(&missing);
184 assert_eq!(found, None);
185 }
186}