Skip to main content

systemprompt_cloud/paths/
project.rs

1use std::path::{Path, PathBuf};
2
3use crate::constants::{dir_names, paths};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum ProjectPath {
7    Root,
8    ProfilesDir,
9    DockerDir,
10    StorageDir,
11    SessionsDir,
12    Dockerfile,
13    LocalCredentials,
14    LocalTenants,
15    LocalSession,
16}
17
18impl ProjectPath {
19    #[must_use]
20    pub const fn segments(&self) -> &'static [&'static str] {
21        match self {
22            Self::Root => &[paths::ROOT_DIR],
23            Self::ProfilesDir => &[paths::ROOT_DIR, paths::PROFILES_DIR],
24            Self::DockerDir => &[paths::ROOT_DIR, paths::DOCKER_DIR],
25            Self::StorageDir => &[paths::STORAGE_DIR],
26            Self::SessionsDir => &[paths::ROOT_DIR, dir_names::SESSIONS],
27            Self::Dockerfile => &[paths::ROOT_DIR, paths::DOCKERFILE],
28            Self::LocalCredentials => &[paths::ROOT_DIR, paths::CREDENTIALS_FILE],
29            Self::LocalTenants => &[paths::ROOT_DIR, paths::TENANTS_FILE],
30            Self::LocalSession => &[paths::ROOT_DIR, paths::SESSION_FILE],
31        }
32    }
33
34    #[must_use]
35    pub const fn is_dir(&self) -> bool {
36        matches!(
37            self,
38            Self::Root | Self::ProfilesDir | Self::DockerDir | Self::StorageDir | Self::SessionsDir
39        )
40    }
41
42    #[must_use]
43    pub fn resolve(&self, project_root: &Path) -> PathBuf {
44        let mut path = project_root.to_path_buf();
45        for segment in self.segments() {
46            path.push(segment);
47        }
48        path
49    }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum ProfilePath {
54    Config,
55    Secrets,
56    DockerDir,
57    Dockerfile,
58    Entrypoint,
59    Dockerignore,
60    Compose,
61}
62
63impl ProfilePath {
64    #[must_use]
65    pub const fn filename(&self) -> &'static str {
66        match self {
67            Self::Config => paths::PROFILE_CONFIG,
68            Self::Secrets => paths::PROFILE_SECRETS,
69            Self::DockerDir => paths::PROFILE_DOCKER_DIR,
70            Self::Dockerfile => paths::DOCKERFILE,
71            Self::Entrypoint => paths::ENTRYPOINT,
72            Self::Dockerignore => paths::DOCKERIGNORE,
73            Self::Compose => paths::COMPOSE_FILE,
74        }
75    }
76
77    #[must_use]
78    pub const fn is_docker_file(&self) -> bool {
79        matches!(
80            self,
81            Self::Dockerfile | Self::Entrypoint | Self::Dockerignore | Self::Compose
82        )
83    }
84
85    #[must_use]
86    pub fn resolve(&self, profile_dir: &Path) -> PathBuf {
87        match self {
88            Self::Dockerfile | Self::Entrypoint | Self::Dockerignore | Self::Compose => profile_dir
89                .join(paths::PROFILE_DOCKER_DIR)
90                .join(self.filename()),
91            _ => profile_dir.join(self.filename()),
92        }
93    }
94}
95
96fn is_valid_project_root(path: &Path) -> bool {
97    if !path.join(paths::ROOT_DIR).is_dir() {
98        return false;
99    }
100    path.join("Cargo.toml").exists()
101        || path.join("services").is_dir()
102        || path.join("storage").is_dir()
103}
104
105#[derive(Debug, Clone)]
106pub struct ProjectContext {
107    root: PathBuf,
108}
109
110impl ProjectContext {
111    #[must_use]
112    pub const fn new(root: PathBuf) -> Self {
113        Self { root }
114    }
115
116    #[must_use]
117    pub fn discover() -> Self {
118        let cwd = match std::env::current_dir() {
119            Ok(dir) => dir,
120            Err(e) => {
121                tracing::debug!(error = %e, "Failed to get current directory, using '.'");
122                PathBuf::from(".")
123            },
124        };
125        Self::discover_from(&cwd)
126    }
127
128    #[must_use]
129    pub fn discover_from(start: &Path) -> Self {
130        let mut current = start.to_path_buf();
131        loop {
132            if is_valid_project_root(&current) {
133                return Self::new(current);
134            }
135            if !current.pop() {
136                break;
137            }
138        }
139        Self::new(start.to_path_buf())
140    }
141
142    #[must_use]
143    pub fn root(&self) -> &Path {
144        &self.root
145    }
146
147    #[must_use]
148    pub fn resolve(&self, path: ProjectPath) -> PathBuf {
149        path.resolve(&self.root)
150    }
151
152    #[must_use]
153    pub fn systemprompt_dir(&self) -> PathBuf {
154        self.resolve(ProjectPath::Root)
155    }
156
157    #[must_use]
158    pub fn profiles_dir(&self) -> PathBuf {
159        self.resolve(ProjectPath::ProfilesDir)
160    }
161
162    #[must_use]
163    pub fn profile_dir(&self, name: &str) -> PathBuf {
164        self.profiles_dir().join(name)
165    }
166
167    #[must_use]
168    pub fn profile_path(&self, name: &str, path: ProfilePath) -> PathBuf {
169        path.resolve(&self.profile_dir(name))
170    }
171
172    #[must_use]
173    pub fn profile_docker_dir(&self, name: &str) -> PathBuf {
174        self.profile_path(name, ProfilePath::DockerDir)
175    }
176
177    #[must_use]
178    pub fn profile_dockerfile(&self, name: &str) -> PathBuf {
179        self.profile_path(name, ProfilePath::Dockerfile)
180    }
181
182    #[must_use]
183    pub fn profile_entrypoint(&self, name: &str) -> PathBuf {
184        self.profile_path(name, ProfilePath::Entrypoint)
185    }
186
187    #[must_use]
188    pub fn profile_dockerignore(&self, name: &str) -> PathBuf {
189        self.profile_path(name, ProfilePath::Dockerignore)
190    }
191
192    #[must_use]
193    pub fn profile_compose(&self, name: &str) -> PathBuf {
194        self.profile_path(name, ProfilePath::Compose)
195    }
196
197    #[must_use]
198    pub fn docker_dir(&self) -> PathBuf {
199        self.resolve(ProjectPath::DockerDir)
200    }
201
202    #[must_use]
203    pub fn storage_dir(&self) -> PathBuf {
204        self.resolve(ProjectPath::StorageDir)
205    }
206
207    #[must_use]
208    pub fn dockerfile(&self) -> PathBuf {
209        self.resolve(ProjectPath::Dockerfile)
210    }
211
212    #[must_use]
213    pub fn local_credentials(&self) -> PathBuf {
214        self.resolve(ProjectPath::LocalCredentials)
215    }
216
217    #[must_use]
218    pub fn local_tenants(&self) -> PathBuf {
219        self.resolve(ProjectPath::LocalTenants)
220    }
221
222    #[must_use]
223    pub fn sessions_dir(&self) -> PathBuf {
224        self.resolve(ProjectPath::SessionsDir)
225    }
226
227    #[must_use]
228    pub fn exists(&self, path: ProjectPath) -> bool {
229        self.resolve(path).exists()
230    }
231
232    #[must_use]
233    pub fn profile_exists(&self, name: &str) -> bool {
234        self.profile_dir(name).exists()
235    }
236}