Skip to main content

wowlab_common/
cli.rs

1//! Shared CLI bootstrap utilities.
2
3use wowlab_fs::{
4    directory::{self, EntryKind},
5    path::{Path, PathBuf},
6    working_directory,
7};
8
9use super::output;
10
11trait WorkspaceEnvironment {
12    fn var(&self, key: &str) -> Option<String>;
13    fn current_dir(&self) -> Option<PathBuf>;
14}
15
16struct ProcessWorkspaceEnvironment;
17
18impl WorkspaceEnvironment for ProcessWorkspaceEnvironment {
19    fn var(&self, key: &str) -> Option<String> {
20        // #t(rust_ambient_syscall) process-environment adapter boundary
21        std::env::var(key).ok()
22    }
23
24    fn current_dir(&self) -> Option<PathBuf> {
25        working_directory::current().ok()
26    }
27}
28
29/// Shared CLI context holding the workspace root and quiet-mode flag.
30#[derive(Debug)]
31pub struct CliApp {
32    pub root: PathBuf,
33    pub quiet: bool,
34}
35
36impl CliApp {
37    #[must_use]
38    pub fn crates_dir(&self) -> PathBuf {
39        self.root.join("crates")
40    }
41}
42
43/// Bootstrap a CLI tool: print the banner (unless quiet) and resolve the workspace root.
44#[must_use]
45pub fn boot(name: &str, version: &str, quiet: bool, root_env_var: &str) -> CliApp {
46    if !quiet {
47        output::banner(name, version);
48    }
49
50    let root = workspace_root(root_env_var);
51
52    CliApp { root, quiet }
53}
54
55/// Find the workspace root by checking an explicit env var, `CARGO_MANIFEST_DIR`, then walking up from cwd.
56///
57/// # Panics
58///
59/// Panics when the process working directory cannot be determined.
60#[must_use]
61pub fn workspace_root(env_var: &str) -> PathBuf {
62    workspace_root_with(&ProcessWorkspaceEnvironment, env_var)
63        .expect("cannot determine working directory")
64}
65
66fn workspace_root_with(env: &impl WorkspaceEnvironment, env_var: &str) -> Option<PathBuf> {
67    if let Some(dir) = env.var(env_var) {
68        let root = PathBuf::from(dir);
69
70        if is_directory(&root.join("crates")) {
71            return Some(root);
72        }
73    }
74
75    if let Some(dir) = env.var("CARGO_MANIFEST_DIR")
76        && let Some(root) = PathBuf::from(dir)
77            .parent()
78            .and_then(|p| p.parent())
79            .filter(|root| is_directory(&root.join("crates")))
80    {
81        return Some(root.to_path_buf());
82    }
83
84    let current_dir = env.current_dir()?;
85    let mut candidate = current_dir.clone();
86
87    loop {
88        if entry_exists(&candidate.join("Cargo.toml")) && is_directory(&candidate.join("crates")) {
89            return Some(candidate);
90        }
91
92        if !candidate.pop() {
93            break;
94        }
95    }
96
97    Some(current_dir)
98}
99
100fn entry_exists(path: &Path) -> bool {
101    directory::inspect(path).is_ok_and(|entry| entry.is_some())
102}
103
104fn is_directory(path: &Path) -> bool {
105    directory::inspect(path)
106        .is_ok_and(|entry| entry.is_some_and(|entry| entry.kind() == EntryKind::Directory))
107}
108
109#[cfg(test)]
110mod tests {
111    use googletest::prelude::*;
112
113    use super::*;
114
115    struct TestEnvironment {
116        explicit_root: Option<PathBuf>,
117        manifest_dir: Option<PathBuf>,
118        current_dir: PathBuf,
119    }
120
121    impl WorkspaceEnvironment for TestEnvironment {
122        fn var(&self, key: &str) -> Option<String> {
123            match key {
124                "TEST_ROOT" => self
125                    .explicit_root
126                    .as_ref()
127                    .map(|path| path.display().to_string()),
128                "CARGO_MANIFEST_DIR" => self
129                    .manifest_dir
130                    .as_ref()
131                    .map(|path| path.display().to_string()),
132                _ => None,
133            }
134        }
135
136        fn current_dir(&self) -> Option<PathBuf> {
137            Some(self.current_dir.clone())
138        }
139    }
140
141    fn workspace(directory: &wowlab_fs::temporary::Directory, name: &str) -> Result<PathBuf> {
142        let root = directory.path().join(name);
143
144        directory::ensure(&root.join("crates")).or_fail()?;
145        wowlab_fs::file::write_text(&root.join("Cargo.toml"), "[workspace]\n").or_fail()?;
146
147        Ok(root)
148    }
149
150    #[gtest]
151    fn explicit_root_has_precedence() -> Result<()> {
152        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
153        let explicit_root = workspace(&directory, "explicit").or_fail()?;
154        let manifest_root = workspace(&directory, "manifest").or_fail()?;
155        let current_root = workspace(&directory, "current").or_fail()?;
156        let environment = TestEnvironment {
157            explicit_root: Some(explicit_root.clone()),
158            manifest_dir: Some(manifest_root.join("crates/common")),
159            current_dir: current_root,
160        };
161
162        verify_that!(
163            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
164            some(eq(&*explicit_root))
165        )
166    }
167
168    #[gtest]
169    fn manifest_directory_is_used_when_explicit_root_is_invalid() -> Result<()> {
170        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
171        let manifest_root = workspace(&directory, "manifest").or_fail()?;
172        let current_root = workspace(&directory, "current").or_fail()?;
173        let environment = TestEnvironment {
174            explicit_root: Some(directory.path().join("missing")),
175            manifest_dir: Some(manifest_root.join("crates/common")),
176            current_dir: current_root,
177        };
178
179        verify_that!(
180            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
181            some(eq(&*manifest_root))
182        )
183    }
184
185    #[gtest]
186    fn current_directory_walks_to_workspace_ancestor() -> Result<()> {
187        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
188        let root = workspace(&directory, "current").or_fail()?;
189        let nested = root.join("crates/common/src");
190
191        directory::ensure(&nested).or_fail()?;
192        let environment = TestEnvironment {
193            explicit_root: None,
194            manifest_dir: None,
195            current_dir: nested,
196        };
197
198        verify_that!(
199            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
200            some(eq(&*root))
201        )
202    }
203
204    #[gtest]
205    fn current_directory_is_the_fallback_without_workspace_markers() -> Result<()> {
206        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
207        let current_dir = directory.path().join("plain/directory");
208
209        directory::ensure(&current_dir).or_fail()?;
210        let environment = TestEnvironment {
211            explicit_root: None,
212            manifest_dir: None,
213            current_dir: current_dir.clone(),
214        };
215
216        verify_that!(
217            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
218            some(eq(&*current_dir))
219        )
220    }
221}