1use crate::context::Context;
6use crate::doctor::{self, Diagnosis};
7use crate::error::{Error, Result};
8use crate::state::{self, Account};
9use crate::switch::{self, Enrolled, Outcome, Recovered, Renewal, Settled, SignIn};
10use crate::{audit, status, statusline};
11use std::fmt;
12
13#[derive(Debug)]
15pub enum Warning {
16 Recovered(Recovered),
18 ConfigNotUpdated(Error),
20 ParksPendingRemoval(usize),
22 ParkedLoginRefused {
24 label: String,
25 },
26 RenewalFailed(Error),
27}
28
29impl Warning {
30 pub fn code(&self) -> &'static str {
32 match self {
33 Warning::Recovered(r) => r.code(),
34 Warning::ConfigNotUpdated(e) | Warning::RenewalFailed(e) => e.code(),
35 Warning::ParksPendingRemoval(_) => "parks_pending_removal",
36 Warning::ParkedLoginRefused { .. } => "parked_login_refused",
37 }
38 }
39}
40
41impl fmt::Display for Warning {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Warning::Recovered(r) => write!(f, "{r}"),
45 Warning::ConfigNotUpdated(e) | Warning::RenewalFailed(e) => write!(f, "{e}"),
46 Warning::ParksPendingRemoval(count) => write!(
47 f,
48 "{count} parked login(s) no longer in use could not be removed yet; pitboard \
49 tries again on its next change"
50 ),
51 Warning::ParkedLoginRefused { label } => write!(
52 f,
53 "Anthropic no longer accepts the parked login for `{label}`. Run `pitboard \
54 enroll {label} --sign-in` to sign in to it again."
55 ),
56 }
57 }
58}
59
60#[derive(Debug)]
61pub struct Done<T> {
62 pub value: T,
63 pub warnings: Vec<Warning>,
64}
65
66#[derive(Debug)]
69pub struct Failed {
70 pub error: Error,
71 pub warnings: Vec<Warning>,
72}
73
74pub type Changing<T> = std::result::Result<Done<T>, Failed>;
75
76pub struct Pitboard {
77 ctx: Context,
78}
79
80impl Pitboard {
81 pub fn new(ctx: Context) -> Pitboard {
82 Pitboard { ctx }
83 }
84
85 pub fn status(&self) -> Result<Done<status::Report>> {
88 let mut warnings = Vec::new();
89 for (label, outcome) in switch::renew_parked(&self.ctx) {
90 audit::record(&self.ctx, "renew", &label, outcome.code());
91 match outcome {
92 Renewal::Refused => warnings.push(Warning::ParkedLoginRefused { label }),
93 Renewal::Failed(e) => warnings.push(Warning::RenewalFailed(e)),
94 Renewal::Renewed | Renewal::Deferred => {}
95 }
96 }
97 let state = state::load(&self.ctx)?;
100 Ok(Done {
101 value: status::gather(&self.ctx, &state),
102 warnings,
103 })
104 }
105
106 pub fn doctor(&self) -> Diagnosis {
107 doctor::run(&self.ctx)
108 }
109
110 pub fn statusline(&self, session: &str) -> statusline::StatusLine {
112 statusline::read(&self.ctx, session)
113 }
114
115 pub fn account(&self, label: &str) -> Option<Account> {
117 state::load(&self.ctx).ok()?.get(label).cloned()
118 }
119
120 pub fn switch_to(&self, label: &str) -> Changing<Outcome> {
121 self.changing("use", label, |settled| switch::switch(settled, label))
122 }
123
124 pub fn enroll_current(&self, label: &str) -> Changing<Enrolled> {
125 self.changing("enroll", label, |settled| {
126 switch::enroll(settled, label, None).map(|e| (e, Vec::new()))
127 })
128 }
129
130 pub fn sign_in(&self, label: &str) -> Result<SignIn> {
133 switch::sign_in(&self.ctx)
134 .inspect_err(|e| audit::record(&self.ctx, "enroll", label, e.code()))
135 }
136
137 pub fn enroll_signed_in(&self, label: &str, login: SignIn) -> Changing<Enrolled> {
138 self.changing("enroll", label, |settled| {
139 switch::enroll(settled, label, Some(login)).map(|e| (e, Vec::new()))
140 })
141 }
142
143 pub fn forget(&self, label: &str) -> Changing<String> {
145 self.changing("forget", label, |settled| switch::forget(settled, label))
146 }
147
148 pub fn rename(&self, from: &str, to: &str) -> Changing<String> {
150 self.changing("rename", &format!("{from} -> {to}"), |settled| {
151 switch::rename(settled, from, to).map(|email| (email, Vec::new()))
152 })
153 }
154
155 fn changing<T: Audited>(
157 &self,
158 verb: &str,
159 subject: &str,
160 run: impl FnOnce(Settled) -> Result<(T, Vec<Warning>)>,
161 ) -> Changing<T> {
162 let (settled, recovered) = switch::settle(&self.ctx).map_err(|error| {
163 audit::record(&self.ctx, verb, subject, error.code());
164 Failed {
165 error,
166 warnings: Vec::new(),
167 }
168 })?;
169 let mut warnings = Vec::new();
170 if let Some(r) = recovered {
171 audit::record(&self.ctx, "recover", &r.to, r.code());
172 warnings.push(Warning::Recovered(r));
173 }
174 match run(settled) {
175 Ok((value, more)) => {
176 audit::record(&self.ctx, verb, subject, value.audit_code());
177 warnings.extend(more);
178 Ok(Done { value, warnings })
179 }
180 Err(error) => {
181 audit::record(&self.ctx, verb, subject, error.code());
182 Err(Failed { error, warnings })
183 }
184 }
185 }
186}
187
188trait Audited {
190 fn audit_code(&self) -> &'static str {
191 "ok"
192 }
193}
194
195impl Audited for Outcome {
196 fn audit_code(&self) -> &'static str {
197 match self {
198 Outcome::Switched { .. } => "ok",
199 Outcome::AlreadyActive { .. } => "already_active",
200 }
201 }
202}
203
204impl Audited for Enrolled {}
205impl Audited for String {}