systemprompt_cli/shared/
project.rs1use 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(¤t) {
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}