use crate::context::Context;
use crate::doctor::{self, Diagnosis};
use crate::error::{Error, Result};
use crate::state::{self, Account};
use crate::switch::{self, Enrolled, Outcome, Recovered, Renewal, Settled, SignIn};
use crate::{audit, status, statusline};
use std::fmt;
#[derive(Debug)]
pub enum Warning {
Recovered(Recovered),
ConfigNotUpdated(Error),
ParksPendingRemoval(usize),
ParkedLoginRefused {
label: String,
},
RenewalFailed(Error),
}
impl Warning {
pub fn code(&self) -> &'static str {
match self {
Warning::Recovered(r) => r.code(),
Warning::ConfigNotUpdated(e) | Warning::RenewalFailed(e) => e.code(),
Warning::ParksPendingRemoval(_) => "parks_pending_removal",
Warning::ParkedLoginRefused { .. } => "parked_login_refused",
}
}
}
impl fmt::Display for Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Warning::Recovered(r) => write!(f, "{r}"),
Warning::ConfigNotUpdated(e) | Warning::RenewalFailed(e) => write!(f, "{e}"),
Warning::ParksPendingRemoval(count) => write!(
f,
"{count} parked login(s) no longer in use could not be removed yet; pitboard \
tries again on its next change"
),
Warning::ParkedLoginRefused { label } => write!(
f,
"Anthropic no longer accepts the parked login for `{label}`. Run `pitboard \
enroll {label} --sign-in` to sign in to it again."
),
}
}
}
#[derive(Debug)]
pub struct Done<T> {
pub value: T,
pub warnings: Vec<Warning>,
}
#[derive(Debug)]
pub struct Failed {
pub error: Error,
pub warnings: Vec<Warning>,
}
pub type Changing<T> = std::result::Result<Done<T>, Failed>;
pub struct Pitboard {
ctx: Context,
}
impl Pitboard {
pub fn new(ctx: Context) -> Pitboard {
Pitboard { ctx }
}
pub fn status(&self) -> Result<Done<status::Report>> {
let mut warnings = Vec::new();
for (label, outcome) in switch::renew_parked(&self.ctx) {
audit::record(&self.ctx, "renew", &label, outcome.code());
match outcome {
Renewal::Refused => warnings.push(Warning::ParkedLoginRefused { label }),
Renewal::Failed(e) => warnings.push(Warning::RenewalFailed(e)),
Renewal::Renewed | Renewal::Deferred => {}
}
}
let state = state::load(&self.ctx)?;
Ok(Done {
value: status::gather(&self.ctx, &state),
warnings,
})
}
pub fn doctor(&self) -> Diagnosis {
doctor::run(&self.ctx)
}
pub fn statusline(&self, session: &str) -> statusline::StatusLine {
statusline::read(&self.ctx, session)
}
pub fn account(&self, label: &str) -> Option<Account> {
state::load(&self.ctx).ok()?.get(label).cloned()
}
pub fn switch_to(&self, label: &str) -> Changing<Outcome> {
self.changing("use", label, |settled| switch::switch(settled, label))
}
pub fn enroll_current(&self, label: &str) -> Changing<Enrolled> {
self.changing("enroll", label, |settled| {
switch::enroll(settled, label, None).map(|e| (e, Vec::new()))
})
}
pub fn sign_in(&self, label: &str) -> Result<SignIn> {
switch::sign_in(&self.ctx)
.inspect_err(|e| audit::record(&self.ctx, "enroll", label, e.code()))
}
pub fn enroll_signed_in(&self, label: &str, login: SignIn) -> Changing<Enrolled> {
self.changing("enroll", label, |settled| {
switch::enroll(settled, label, Some(login)).map(|e| (e, Vec::new()))
})
}
pub fn forget(&self, label: &str) -> Changing<String> {
self.changing("forget", label, |settled| switch::forget(settled, label))
}
pub fn rename(&self, from: &str, to: &str) -> Changing<String> {
self.changing("rename", &format!("{from} -> {to}"), |settled| {
switch::rename(settled, from, to).map(|email| (email, Vec::new()))
})
}
fn changing<T: Audited>(
&self,
verb: &str,
subject: &str,
run: impl FnOnce(Settled) -> Result<(T, Vec<Warning>)>,
) -> Changing<T> {
let (settled, recovered) = switch::settle(&self.ctx).map_err(|error| {
audit::record(&self.ctx, verb, subject, error.code());
Failed {
error,
warnings: Vec::new(),
}
})?;
let mut warnings = Vec::new();
if let Some(r) = recovered {
audit::record(&self.ctx, "recover", &r.to, r.code());
warnings.push(Warning::Recovered(r));
}
match run(settled) {
Ok((value, more)) => {
audit::record(&self.ctx, verb, subject, value.audit_code());
warnings.extend(more);
Ok(Done { value, warnings })
}
Err(error) => {
audit::record(&self.ctx, verb, subject, error.code());
Err(Failed { error, warnings })
}
}
}
}
trait Audited {
fn audit_code(&self) -> &'static str {
"ok"
}
}
impl Audited for Outcome {
fn audit_code(&self) -> &'static str {
match self {
Outcome::Switched { .. } => "ok",
Outcome::AlreadyActive { .. } => "already_active",
}
}
}
impl Audited for Enrolled {}
impl Audited for String {}