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