Skip to main content

systemprompt_cli/shared/
project.rs

1use std::path::{Path, PathBuf};
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum ProjectError {
6    #[error(
7        "Not a systemprompt.io project: {path}\n\nLooking for .systemprompt directory alongside \
8         Cargo.toml, services/, or storage/"
9    )]
10    ProjectNotFound { path: PathBuf },
11
12    #[error("Failed to resolve path {path}: {source}")]
13    PathResolution {
14        path: PathBuf,
15        #[source]
16        source: std::io::Error,
17    },
18}
19
20fn is_valid_project_root(path: &Path) -> bool {
21    if !path.join(".systemprompt").is_dir() {
22        return false;
23    }
24    path.join("Cargo.toml").exists()
25        || path.join("services").is_dir()
26        || path.join("storage").is_dir()
27}
28
29#[derive(Debug, Clone)]
30pub struct ProjectRoot(PathBuf);
31
32impl ProjectRoot {
33    pub fn discover() -> Result<Self, ProjectError> {
34        let current = std::env::current_dir().map_err(|e| ProjectError::PathResolution {
35            path: PathBuf::from("."),
36            source: e,
37        })?;
38
39        if is_valid_project_root(&current) {
40            return Ok(Self(current));
41        }
42
43        let mut search = current.as_path();
44        while let Some(parent) = search.parent() {
45            if is_valid_project_root(parent) {
46                return Ok(Self(parent.to_path_buf()));
47            }
48            search = parent;
49        }
50
51        Err(ProjectError::ProjectNotFound { path: current })
52    }
53
54    #[must_use]
55    pub fn as_path(&self) -> &Path {
56        &self.0
57    }
58}
59
60impl AsRef<Path> for ProjectRoot {
61    fn as_ref(&self) -> &Path {
62        &self.0
63    }
64}