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