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