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        if 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
85    let current_dir = env.current_dir()?;
86    let mut candidate = current_dir.clone();
87
88    loop {
89        if entry_exists(&candidate.join("Cargo.toml")) && is_directory(&candidate.join("crates")) {
90            return Some(candidate);
91        }
92
93        if !candidate.pop() {
94            break;
95        }
96    }
97
98    Some(current_dir)
99}
100
101fn entry_exists(path: &Path) -> bool {
102    directory::inspect(path).is_ok_and(|entry| entry.is_some())
103}
104
105fn is_directory(path: &Path) -> bool {
106    directory::inspect(path)
107        .is_ok_and(|entry| entry.is_some_and(|entry| entry.kind() == EntryKind::Directory))
108}
109
110#[cfg(test)]
111mod tests {
112    use googletest::prelude::*;
113
114    use super::*;
115
116    struct TestEnvironment {
117        explicit_root: Option<PathBuf>,
118        manifest_dir: Option<PathBuf>,
119        current_dir: PathBuf,
120    }
121
122    impl WorkspaceEnvironment for TestEnvironment {
123        fn var(&self, key: &str) -> Option<String> {
124            match key {
125                "TEST_ROOT" => self
126                    .explicit_root
127                    .as_ref()
128                    .map(|path| path.display().to_string()),
129                "CARGO_MANIFEST_DIR" => self
130                    .manifest_dir
131                    .as_ref()
132                    .map(|path| path.display().to_string()),
133                _ => None,
134            }
135        }
136
137        fn current_dir(&self) -> Option<PathBuf> {
138            Some(self.current_dir.clone())
139        }
140    }
141
142    fn workspace(directory: &wowlab_fs::temporary::Directory, name: &str) -> Result<PathBuf> {
143        let root = directory.path().join(name);
144
145        directory::ensure(&root.join("crates")).or_fail()?;
146        wowlab_fs::file::write_text(&root.join("Cargo.toml"), "[workspace]\n").or_fail()?;
147
148        Ok(root)
149    }
150
151    #[gtest]
152    fn explicit_root_has_precedence() -> Result<()> {
153        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
154        let explicit_root = workspace(&directory, "explicit").or_fail()?;
155        let manifest_root = workspace(&directory, "manifest").or_fail()?;
156        let current_root = workspace(&directory, "current").or_fail()?;
157        let environment = TestEnvironment {
158            explicit_root: Some(explicit_root.clone()),
159            manifest_dir: Some(manifest_root.join("crates/common")),
160            current_dir: current_root,
161        };
162
163        verify_that!(
164            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
165            some(eq(&*explicit_root))
166        )
167    }
168
169    #[gtest]
170    fn manifest_directory_is_used_when_explicit_root_is_invalid() -> Result<()> {
171        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
172        let manifest_root = workspace(&directory, "manifest").or_fail()?;
173        let current_root = workspace(&directory, "current").or_fail()?;
174        let environment = TestEnvironment {
175            explicit_root: Some(directory.path().join("missing")),
176            manifest_dir: Some(manifest_root.join("crates/common")),
177            current_dir: current_root,
178        };
179
180        verify_that!(
181            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
182            some(eq(&*manifest_root))
183        )
184    }
185
186    #[gtest]
187    fn current_directory_walks_to_workspace_ancestor() -> Result<()> {
188        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
189        let root = workspace(&directory, "current").or_fail()?;
190        let nested = root.join("crates/common/src");
191
192        directory::ensure(&nested).or_fail()?;
193        let environment = TestEnvironment {
194            explicit_root: None,
195            manifest_dir: None,
196            current_dir: nested,
197        };
198
199        verify_that!(
200            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
201            some(eq(&*root))
202        )
203    }
204
205    #[gtest]
206    fn current_directory_is_the_fallback_without_workspace_markers() -> Result<()> {
207        let directory = wowlab_fs::temporary::Directory::new().or_fail()?;
208        let current_dir = directory.path().join("plain/directory");
209
210        directory::ensure(&current_dir).or_fail()?;
211        let environment = TestEnvironment {
212            explicit_root: None,
213            manifest_dir: None,
214            current_dir: current_dir.clone(),
215        };
216
217        verify_that!(
218            workspace_root_with(&environment, "TEST_ROOT").as_deref(),
219            some(eq(&*current_dir))
220        )
221    }
222}