use super::{paths as claude, slot};
use crate::context::Context;
use crate::host::{OS, Os};
use crate::service::Permit;
use crate::store::{self, Backend, Error, Live, RawStore, Unbuilt};
use serde_json::Value;
use std::path::{Path, PathBuf};
pub(crate) fn credential_file(ctx: &Context) -> PathBuf {
PathBuf::from(claude::storage_dir(ctx)).join(slot::CRED_FILE)
}
fn not_on_windows_yet() -> Unbuilt {
Unbuilt::new(
Backend::Unknown,
"Pitboard does not switch Claude Code on Windows yet",
)
}
pub(crate) fn chain(ctx: &Context) -> Live {
match OS {
Os::MacOs | Os::Linux => {
let host = ctx.host();
let mut backends: Vec<Box<dyn RawStore>> = Vec::new();
if let Some(keychain) = host.foreign_secrets(ctx, &slot::account_name(ctx)) {
backends.push(keychain);
}
backends.push(host.file(credential_file(ctx)));
Live::of(backends)
}
Os::Windows => Live::of(vec![Box::new(not_on_windows_yet())]),
}
}
pub(crate) fn behind(ctx: &Context) -> Option<Result<String, Error>> {
let (live, service) = (chain(ctx), claude::live_service(ctx));
let file = store::behind(&live, &service).ok()??;
file.read(&service).transpose()
}
pub(crate) fn document_in(held: &str) -> Value {
serde_json::from_str::<Value>(held)
.ok()
.filter(Value::is_object)
.unwrap_or_else(|| Value::Object(serde_json::Map::new()))
}
pub(crate) fn holds_a_login(document: &Value) -> bool {
let oauth = document.get("claudeAiOauth");
["accessToken", "refreshToken"].into_iter().any(|token| {
oauth
.and_then(|oauth| oauth.get(token))
.and_then(Value::as_str)
.is_some_and(|token| !token.is_empty())
})
}
pub(crate) fn login_in(held: &str) -> Option<Value> {
Some(document_in(held)).filter(holds_a_login)
}
pub(crate) fn read_signin(ctx: &Context, dir: &Path) -> Result<Option<String>, Error> {
match OS {
Os::MacOs | Os::Linux => {}
Os::Windows => return not_on_windows_yet().read(""),
}
match ctx.host().foreign_secrets(ctx, &slot::account_name(ctx)) {
Some(keychain) => keychain.read(&slot::service_for_dir(&dir.to_string_lossy())),
None => ctx.host().file(dir.join(slot::CRED_FILE)).read(""),
}
}
pub(crate) fn discard_signin(ctx: &Context, permit: Permit, dir: &Path) -> Result<(), Error> {
match OS {
Os::MacOs | Os::Linux => {}
Os::Windows => return not_on_windows_yet().delete(permit, ""),
}
let service = slot::service_for_dir(&dir.to_string_lossy());
if service == slot::LIVE_SERVICE || service == claude::live_service(ctx) {
return Err(Error::Write(format!("refusing to delete {service}")));
}
match ctx.host().foreign_secrets(ctx, &slot::account_name(ctx)) {
Some(keychain) => keychain.delete(permit, &service),
None => ctx
.host()
.file(dir.join(slot::CRED_FILE))
.delete(permit, ""),
}
}