1pub mod brain;
44pub mod compat_import;
45pub mod compatibility;
46pub mod credentials;
47pub mod fixtures;
48pub mod packages;
49pub mod profiles;
50
51pub mod migrate;
52
53use std::path::{Path, PathBuf};
54
55pub fn fetch_oxicode_home() -> Option<PathBuf> {
61 if let Ok(home) = std::env::var("OXICODE_HOME") {
62 return Some(PathBuf::from(home));
63 }
64 dirs::home_dir().map(|h| h.join(".oxicode"))
65}
66
67pub const FOUNDATION_ROOT_SUFFIX: &str = "oxi/foundation/v1";
70
71pub mod files {
73 pub const FOUNDATION: &str = "foundation.json";
75 pub const PROFILES: &str = "profiles.json";
77 pub const PACKAGES_LOCK: &str = "packages.lock";
79 pub const PACKAGES_DIR: &str = "packages";
81}
82
83pub fn foundation_root() -> Option<PathBuf> {
87 if let Ok(home) = std::env::var("OXI_FOUNDATION_HOME") {
88 let trimmed = home.trim();
89 if !trimmed.is_empty() {
90 return Some(PathBuf::from(trimmed));
91 }
92 }
93 dirs::home_dir().map(|h| h.join(FOUNDATION_ROOT_SUFFIX))
94}
95
96pub fn foundation_present(root: &Path) -> bool {
100 root.is_dir() && root.join(files::FOUNDATION).is_file() && root.join(files::PROFILES).is_file()
101}
102
103pub fn discover(root: &Path) -> Result<FoundationSnapshot, FoundationError> {
108 let compatibility = compatibility::read(&root.join(files::FOUNDATION))?;
109 let profiles = profiles::read(&root.join(files::PROFILES))?;
110 let packages = packages::read(
111 &root.join(files::PACKAGES_LOCK),
112 &root.join(files::PACKAGES_DIR),
113 )?;
114 Ok(FoundationSnapshot {
115 root: root.to_path_buf(),
116 compatibility,
117 profiles,
118 packages,
119 })
120}
121
122#[derive(Debug, Clone)]
125pub struct FoundationSnapshot {
126 pub root: PathBuf,
128 pub compatibility: compatibility::FoundationManifest,
130 pub profiles: profiles::ProfilesFile,
132 pub packages: packages::PackagesFile,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum FoundationError {
139 UnsupportedSchema(u32),
141 IncompatibleHost(String),
143 Parse(String),
145 SecretNotAllowed(String),
147 DuplicateProfileId(String),
149 UnsupportedRequirement(String),
151 DigestMismatch {
153 package: String,
154 expected: String,
155 actual: String,
156 },
157 TargetMismatch {
159 package: String,
160 targets: Vec<String>,
161 },
162 UnknownProfile(String),
164 UnknownRole(String),
166 AmbiguousRole(String),
168 KeychainUnavailable(String),
170 KeychainLocked(String),
172 KeychainNotFound { service: String, account: String },
174 BrainUnavailable(String),
176 Io(String),
178}
179
180impl std::fmt::Display for FoundationError {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 match self {
186 Self::UnsupportedSchema(v) => write!(f, "unsupported foundation schema_version {v}"),
187 Self::IncompatibleHost(s) => write!(f, "host compatibility check failed: {s}"),
188 Self::Parse(s) => write!(f, "foundation parse error: {s}"),
189 Self::SecretNotAllowed(s) => write!(f, "secret not allowed in foundation file: {s}"),
190 Self::DuplicateProfileId(id) => write!(f, "duplicate profile id: {id}"),
191 Self::UnsupportedRequirement(req) => {
192 write!(f, "unsupported package requirement: {req}")
193 }
194 Self::DigestMismatch {
195 package,
196 expected,
197 actual,
198 } => write!(
199 f,
200 "package {package} digest mismatch: expected {expected}, got {actual}"
201 ),
202 Self::TargetMismatch { package, targets } => write!(
203 f,
204 "package {package} targets do not include `oxicode`: {targets:?}"
205 ),
206 Self::UnknownProfile(id) => write!(f, "unknown profile id: {id}"),
207 Self::UnknownRole(r) => write!(f, "no profile matches requested role: {r}"),
208 Self::AmbiguousRole(r) => write!(f, "multiple profiles match role {r}"),
209 Self::KeychainUnavailable(s) => write!(f, "keychain unavailable: {s}"),
210 Self::KeychainLocked(s) => write!(f, "keychain locked: {s}"),
211 Self::KeychainNotFound { service, account } => {
212 write!(f, "keychain entry not found for {service}:{account}")
213 }
214 Self::BrainUnavailable(s) => write!(f, "brain daemon unavailable: {s}"),
215 Self::Io(s) => write!(f, "foundation I/O error: {s}"),
216 }
217 }
218}
219
220impl std::error::Error for FoundationError {}
221
222impl From<std::io::Error> for FoundationError {
223 fn from(e: std::io::Error) -> Self {
224 Self::Io(e.to_string())
225 }
226}
227
228impl From<serde_json::Error> for FoundationError {
229 fn from(e: serde_json::Error) -> Self {
230 Self::Parse(e.to_string())
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CredentialSource {
237 Environment,
239 Profile,
241 Role,
243 CompatibilityImport,
245 Unavailable,
247}
248
249impl std::fmt::Display for CredentialSource {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match self {
252 Self::Environment => f.write_str("environment"),
253 Self::Profile => f.write_str("profile"),
254 Self::Role => f.write_str("role"),
255 Self::CompatibilityImport => f.write_str("compatibility_import"),
256 Self::Unavailable => f.write_str("unavailable"),
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn foundation_root_honors_env_override() {
267 let tmp = tempfile::tempdir().unwrap();
268 let original = std::env::var("OXI_FOUNDATION_HOME").ok();
274 unsafe {
275 std::env::set_var("OXI_FOUNDATION_HOME", tmp.path());
276 }
277 let root = foundation_root().unwrap();
278 unsafe {
279 std::env::remove_var("OXI_FOUNDATION_HOME");
280 }
281 if let Some(value) = original {
282 unsafe {
283 std::env::set_var("OXI_FOUNDATION_HOME", value);
284 }
285 }
286 assert_eq!(root, tmp.path());
287 }
288
289 #[test]
290 fn foundation_present_detects_layout() {
291 let tmp = tempfile::tempdir().unwrap();
292 assert!(!foundation_present(tmp.path()));
293 std::fs::write(tmp.path().join(files::FOUNDATION), "{}").unwrap();
294 std::fs::write(tmp.path().join(files::PROFILES), "{}").unwrap();
295 assert!(foundation_present(tmp.path()));
296 }
297
298 #[test]
299 fn credential_source_display_roundtrip() {
300 assert_eq!(CredentialSource::Environment.to_string(), "environment");
301 assert_eq!(CredentialSource::Profile.to_string(), "profile");
302 assert_eq!(CredentialSource::Role.to_string(), "role");
303 assert_eq!(
304 CredentialSource::CompatibilityImport.to_string(),
305 "compatibility_import"
306 );
307 assert_eq!(CredentialSource::Unavailable.to_string(), "unavailable");
308 }
309}