Skip to main content

guild_cli/
discovery.rs

1use std::path::{Path, PathBuf};
2
3use walkdir::WalkDir;
4
5use crate::config::{ProjectConfig, WorkspaceConfig};
6use crate::error::ConfigError;
7
8/// Find the workspace root by searching for `guild.toml` with a `[workspace]` section,
9/// starting from `start_dir` and walking up to parent directories.
10pub fn find_workspace_root(start_dir: &Path) -> Result<PathBuf, ConfigError> {
11    let mut current = start_dir.to_path_buf();
12    loop {
13        let candidate = current.join("guild.toml");
14        if candidate.exists() {
15            let content =
16                std::fs::read_to_string(&candidate).map_err(|e| ConfigError::ReadFile {
17                    path: candidate.clone(),
18                    source: e,
19                })?;
20            if content.contains("[workspace]") {
21                return Ok(current);
22            }
23        }
24        if !current.pop() {
25            return Err(ConfigError::WorkspaceNotFound {
26                path: start_dir.to_path_buf(),
27            });
28        }
29    }
30}
31
32/// Discover all project `guild.toml` files within the workspace.
33///
34/// Uses the workspace's project glob patterns to find project directories,
35/// then looks for `guild.toml` in each matching directory.
36pub fn discover_projects(workspace: &WorkspaceConfig) -> Result<Vec<ProjectConfig>, ConfigError> {
37    let root = workspace.root();
38    let mut projects = Vec::new();
39
40    for pattern in workspace.project_patterns() {
41        let full_pattern = root.join(pattern).to_string_lossy().to_string();
42        let matches = glob::glob(&full_pattern).map_err(|e| ConfigError::ReadFile {
43            path: PathBuf::from(&full_pattern),
44            source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()),
45        })?;
46
47        for entry in matches {
48            let path = entry.map_err(|e| ConfigError::ReadFile {
49                path: PathBuf::from(&full_pattern),
50                source: std::io::Error::other(e.to_string()),
51            })?;
52            if path.is_dir() {
53                let toml_path = path.join("guild.toml");
54                if toml_path.exists() {
55                    projects.push(ProjectConfig::from_file(&toml_path)?);
56                }
57            }
58        }
59    }
60
61    // If no glob patterns matched, fall back to walking the directory tree
62    if projects.is_empty() && workspace.project_patterns().is_empty() {
63        for entry in WalkDir::new(root)
64            .min_depth(1)
65            .max_depth(3)
66            .into_iter()
67            .filter_map(|e| e.ok())
68        {
69            if entry.file_name() == "guild.toml" && entry.path() != root.join("guild.toml") {
70                let content =
71                    std::fs::read_to_string(entry.path()).map_err(|e| ConfigError::ReadFile {
72                        path: entry.path().to_path_buf(),
73                        source: e,
74                    })?;
75                if content.contains("[project]") {
76                    projects.push(ProjectConfig::from_file(entry.path())?);
77                }
78            }
79        }
80    }
81
82    Ok(projects)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::fs;
89
90    #[test]
91    fn test_find_workspace_root() {
92        let dir = tempfile::tempdir().unwrap();
93        let root = dir.path();
94
95        // Create workspace guild.toml
96        fs::write(
97            root.join("guild.toml"),
98            "[workspace]\nname = \"test\"\nprojects = [\"apps/*\"]\n",
99        )
100        .unwrap();
101
102        // Create nested dir
103        let nested = root.join("apps").join("my-app");
104        fs::create_dir_all(&nested).unwrap();
105
106        let found = find_workspace_root(&nested).unwrap();
107        assert_eq!(found, root);
108    }
109
110    #[test]
111    fn test_find_workspace_root_not_found() {
112        let dir = tempfile::tempdir().unwrap();
113        assert!(find_workspace_root(dir.path()).is_err());
114    }
115
116    #[test]
117    fn test_discover_projects() {
118        let dir = tempfile::tempdir().unwrap();
119        let root = dir.path();
120
121        // Create workspace config
122        fs::write(
123            root.join("guild.toml"),
124            "[workspace]\nname = \"test\"\nprojects = [\"apps/*\"]\n",
125        )
126        .unwrap();
127
128        // Create a project
129        let app_dir = root.join("apps").join("my-app");
130        fs::create_dir_all(&app_dir).unwrap();
131        fs::write(
132            app_dir.join("guild.toml"),
133            "[project]\nname = \"my-app\"\n\n[targets.build]\ncommand = \"echo build\"\n",
134        )
135        .unwrap();
136
137        let workspace = WorkspaceConfig::from_file(&root.join("guild.toml")).unwrap();
138        let projects = discover_projects(&workspace).unwrap();
139
140        assert_eq!(projects.len(), 1);
141        assert_eq!(projects[0].name().as_str(), "my-app");
142    }
143}