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