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