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