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