Skip to main content

systemprompt_cli/shared/
project.rs

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