use std::fs;
use std::io::{self, Write as _};
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
use std::path::{Path, PathBuf};
use brazen::{
AmbientFormat, AmbientSpec, CachedModels, Cred, CredStore, ModelCache, parse_ambient,
};
use crate::xdg::Env;
pub(crate) struct WallCredStore {
dir: PathBuf,
env: Env,
}
impl WallCredStore {
pub(crate) fn new(dir: PathBuf, env: Env) -> Self {
Self { dir, env }
}
fn path(&self, provider: &str) -> PathBuf {
self.dir.join(format!("{provider}.json"))
}
fn expand_home(&self, path: &str) -> PathBuf {
match path.strip_prefix("~/") {
Some(rest) => self.env.home_dir().join(rest),
None => PathBuf::from(path),
}
}
}
impl CredStore for WallCredStore {
fn get(&self, provider: &str) -> Option<Cred> {
serde_json::from_slice(&fs::read(self.path(provider)).ok()?).ok()
}
fn put(&self, provider: &str, cred: &Cred) -> io::Result<()> {
let bytes = serde_json::to_vec_pretty(cred)?;
fs::create_dir_all(&self.dir)?;
fs::set_permissions(&self.dir, fs::Permissions::from_mode(0o700))?;
write_atomic(&self.path(provider), &bytes, 0o600)
}
fn discover(&self, spec: &AmbientSpec) -> Option<Cred> {
let bytes = match spec.format {
AmbientFormat::ApiKeyEnv => self.env.var(&spec.path)?.into_bytes(),
AmbientFormat::ClaudeCode | AmbientFormat::Codex => {
fs::read(self.expand_home(&spec.path)).ok()?
}
};
parse_ambient(spec.format, &bytes)
}
}
pub(crate) struct WallModelCache {
dir: PathBuf,
}
impl WallModelCache {
pub(crate) fn new(dir: PathBuf) -> Self {
Self { dir }
}
fn path(&self, provider: &str) -> PathBuf {
self.dir.join(format!("{provider}.json"))
}
fn write(&self, provider: &str, cached: &CachedModels) -> io::Result<()> {
let bytes = serde_json::to_vec_pretty(cached)?;
fs::create_dir_all(&self.dir)?;
write_atomic(&self.path(provider), &bytes, 0o600)
}
}
impl ModelCache for WallModelCache {
fn get(&self, provider: &str) -> Option<CachedModels> {
serde_json::from_slice(&fs::read(self.path(provider)).ok()?).ok()
}
fn put(&self, provider: &str, cached: &CachedModels) {
drop(self.write(provider, cached));
}
}
fn write_atomic(dest: &Path, bytes: &[u8], mode: u32) -> io::Result<()> {
let name = dest
.file_name()
.map_or_else(|| "cred".to_owned(), |n| n.to_string_lossy().into_owned());
let dir = dest.parent().unwrap_or_else(|| Path::new("."));
let tmp = crate::scratch::temp_in(dir, &name);
let mut f = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
fs::rename(&tmp, dest)
}
#[cfg(test)]
mod tests;