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 if let Ok(home) = std::env::var("OXICODE_HOME") {
63 return Some(PathBuf::from(home));
64 }
65 dirs::home_dir().map(|h| h.join(".oxicode"))
66}
67
68pub const FOUNDATION_ROOT_SUFFIX: &str = "oxi/foundation/v1";
71
72pub mod files {
74 pub const FOUNDATION: &str = "foundation.json";
76 pub const PROFILES: &str = "profiles.json";
78 pub const PACKAGES_LOCK: &str = "packages.lock";
80 pub const PACKAGES_DIR: &str = "packages";
82}
83
84pub fn foundation_root() -> Option<PathBuf> {
88 if let Ok(home) = std::env::var("OXI_FOUNDATION_HOME") {
89 let trimmed = home.trim();
90 if !trimmed.is_empty() {
91 return Some(PathBuf::from(trimmed));
92 }
93 }
94 dirs::home_dir().map(|h| h.join(FOUNDATION_ROOT_SUFFIX))
95}
96
97pub fn foundation_present(root: &Path) -> bool {
101 root.is_dir() && root.join(files::FOUNDATION).is_file() && root.join(files::PROFILES).is_file()
102}
103
104pub fn discover(root: &Path) -> Result<FoundationSnapshot, FoundationError> {
109 let compatibility = compatibility::read(&root.join(files::FOUNDATION))?;
110 let profiles = profiles::read(&root.join(files::PROFILES))?;
111 let packages = packages::read(
112 &root.join(files::PACKAGES_LOCK),
113 &root.join(files::PACKAGES_DIR),
114 )?;
115 Ok(FoundationSnapshot {
116 root: root.to_path_buf(),
117 compatibility,
118 profiles,
119 packages,
120 })
121}
122
123#[derive(Debug, Clone)]
126pub struct FoundationSnapshot {
127 pub root: PathBuf,
129 pub compatibility: compatibility::FoundationManifest,
131 pub profiles: profiles::ProfilesFile,
133 pub packages: packages::PackagesFile,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum FoundationError {
140 UnsupportedSchema(u32),
142 IncompatibleHost(String),
144 Parse(String),
146 SecretNotAllowed(String),
148 DuplicateProfileId(String),
150 UnsupportedRequirement(String),
152 DigestMismatch {
154 package: String,
155 expected: String,
156 actual: String,
157 },
158 TargetMismatch {
160 package: String,
161 targets: Vec<String>,
162 },
163 UnknownProfile(String),
165 UnknownRole(String),
167 AmbiguousRole(String),
169 KeychainUnavailable(String),
171 KeychainLocked(String),
173 KeychainNotFound { service: String, account: String },
175 BrainUnavailable(String),
177 Io(String),
179}
180
181impl std::fmt::Display for FoundationError {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
187 Self::UnsupportedSchema(v) => write!(f, "unsupported foundation schema_version {v}"),
188 Self::IncompatibleHost(s) => write!(f, "host compatibility check failed: {s}"),
189 Self::Parse(s) => write!(f, "foundation parse error: {s}"),
190 Self::SecretNotAllowed(s) => write!(f, "secret not allowed in foundation file: {s}"),
191 Self::DuplicateProfileId(id) => write!(f, "duplicate profile id: {id}"),
192 Self::UnsupportedRequirement(req) => {
193 write!(f, "unsupported package requirement: {req}")
194 }
195 Self::DigestMismatch {
196 package,
197 expected,
198 actual,
199 } => write!(
200 f,
201 "package {package} digest mismatch: expected {expected}, got {actual}"
202 ),
203 Self::TargetMismatch { package, targets } => write!(
204 f,
205 "package {package} targets do not include `oxicode`: {targets:?}"
206 ),
207 Self::UnknownProfile(id) => write!(f, "unknown profile id: {id}"),
208 Self::UnknownRole(r) => write!(f, "no profile matches requested role: {r}"),
209 Self::AmbiguousRole(r) => write!(f, "multiple profiles match role {r}"),
210 Self::KeychainUnavailable(s) => write!(f, "keychain unavailable: {s}"),
211 Self::KeychainLocked(s) => write!(f, "keychain locked: {s}"),
212 Self::KeychainNotFound { service, account } => {
213 write!(f, "keychain entry not found for {service}:{account}")
214 }
215 Self::BrainUnavailable(s) => write!(f, "brain daemon unavailable: {s}"),
216 Self::Io(s) => write!(f, "foundation I/O error: {s}"),
217 }
218 }
219}
220
221impl std::error::Error for FoundationError {}
222
223impl From<std::io::Error> for FoundationError {
224 fn from(e: std::io::Error) -> Self {
225 Self::Io(e.to_string())
226 }
227}
228
229impl From<serde_json::Error> for FoundationError {
230 fn from(e: serde_json::Error) -> Self {
231 Self::Parse(e.to_string())
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum CredentialSource {
238 Environment,
240 Profile,
242 Role,
244 CompatibilityImport,
246 Unavailable,
248}
249
250impl std::fmt::Display for CredentialSource {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 Self::Environment => f.write_str("environment"),
254 Self::Profile => f.write_str("profile"),
255 Self::Role => f.write_str("role"),
256 Self::CompatibilityImport => f.write_str("compatibility_import"),
257 Self::Unavailable => f.write_str("unavailable"),
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn foundation_root_honors_env_override() {
268 let tmp = tempfile::tempdir().unwrap();
269 let original = std::env::var("OXI_FOUNDATION_HOME").ok();
275 unsafe {
276 std::env::set_var("OXI_FOUNDATION_HOME", tmp.path());
277 }
278 let root = foundation_root().unwrap();
279 unsafe {
280 std::env::remove_var("OXI_FOUNDATION_HOME");
281 }
282 if let Some(value) = original {
283 unsafe {
284 std::env::set_var("OXI_FOUNDATION_HOME", value);
285 }
286 }
287 assert_eq!(root, tmp.path());
288 }
289
290 #[test]
291 fn foundation_present_detects_layout() {
292 let tmp = tempfile::tempdir().unwrap();
293 assert!(!foundation_present(tmp.path()));
294 std::fs::write(tmp.path().join(files::FOUNDATION), "{}").unwrap();
295 std::fs::write(tmp.path().join(files::PROFILES), "{}").unwrap();
296 assert!(foundation_present(tmp.path()));
297 }
298
299 #[test]
300 fn credential_source_display_roundtrip() {
301 assert_eq!(CredentialSource::Environment.to_string(), "environment");
302 assert_eq!(CredentialSource::Profile.to_string(), "profile");
303 assert_eq!(CredentialSource::Role.to_string(), "role");
304 assert_eq!(
305 CredentialSource::CompatibilityImport.to_string(),
306 "compatibility_import"
307 );
308 assert_eq!(CredentialSource::Unavailable.to_string(), "unavailable");
309 }
310}