pub(crate) mod claude;
pub(crate) mod codex;
pub(crate) mod desktop;
pub(crate) mod folder;
pub(crate) mod jwt;
pub mod names;
mod printed;
use crate::context::Context;
use crate::service::Permit;
use crate::usage;
use serde_json::Value;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ProviderId {
Claude,
Codex,
Desktop,
}
#[derive(Debug, Clone, Copy)]
pub struct Cli {
pub program_variable: &'static str,
pub install_places: fn(&crate::context::Environment) -> Vec<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignsIn {
ByItsProgram,
InItsApp,
}
impl ProviderId {
pub const ALL: &'static [ProviderId] =
&[ProviderId::Claude, ProviderId::Codex, ProviderId::Desktop];
pub fn code(self) -> &'static str {
match self {
ProviderId::Claude => "claude",
ProviderId::Codex => "codex",
ProviderId::Desktop => "desktop",
}
}
pub const fn service(self) -> &'static str {
match self {
ProviderId::Claude | ProviderId::Desktop => "Anthropic",
ProviderId::Codex => "OpenAI",
}
}
pub fn name(self) -> &'static str {
match self {
ProviderId::Claude => "Claude Code",
ProviderId::Codex => "Codex",
ProviderId::Desktop => "Claude Desktop",
}
}
pub fn program(self) -> &'static str {
match self {
ProviderId::Claude => "claude",
ProviderId::Codex => "codex",
ProviderId::Desktop => "Claude",
}
}
pub fn cli(self) -> Option<Cli> {
match self {
ProviderId::Claude => Some(Cli {
program_variable: "PITBOARD_CLAUDE",
install_places: claude::paths::install_places,
}),
ProviderId::Codex => Some(Cli {
program_variable: "PITBOARD_CODEX",
install_places: codex::paths::install_places,
}),
ProviderId::Desktop => None,
}
}
pub fn signs_in(self) -> SignsIn {
match of(self) {
LoginKind::Document(_) => SignsIn::ByItsProgram,
LoginKind::Folder(_) => SignsIn::InItsApp,
}
}
pub fn reads_usage(self) -> bool {
match of(self) {
LoginKind::Document(_) => true,
LoginKind::Folder(_) => false,
}
}
pub fn supported_on(self, os: crate::host::Os) -> bool {
use crate::host::Os;
match (self, os) {
(ProviderId::Claude | ProviderId::Codex, _) => true,
(ProviderId::Desktop, Os::MacOs) => true,
(ProviderId::Desktop, Os::Linux | Os::Windows) => false,
}
}
pub fn parse(code: &str) -> Option<ProviderId> {
match code {
"claude" => Some(ProviderId::Claude),
"codex" => Some(ProviderId::Codex),
"desktop" => Some(ProviderId::Desktop),
_ => None,
}
}
}
impl std::fmt::Display for ProviderId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Credential {
pub(crate) provider: ProviderId,
pub(crate) raw: Value,
pub(crate) named: Option<Owner>,
}
impl Credential {
pub(crate) fn new(provider: ProviderId, raw: Value) -> Credential {
Credential {
provider,
raw,
named: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Identity {
pub(crate) account_uuid: String,
pub(crate) organization_uuid: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Owner {
pub account_uuid: String,
pub email: String,
pub organization_uuid: String,
}
impl Owner {
pub(crate) fn named(identity: Identity, email: String) -> Owner {
Owner {
account_uuid: identity.account_uuid,
email,
organization_uuid: identity.organization_uuid,
}
}
pub(crate) fn identity(&self) -> Identity {
Identity {
account_uuid: self.account_uuid.clone(),
organization_uuid: self.organization_uuid.clone(),
}
}
pub(crate) fn same_login(&self, other: &Owner) -> bool {
self.identity() == other.identity()
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ProviderError {
#[error("the session has expired")]
Unauthorized,
#[error("{service} is rate limiting this request")]
RateLimited {
service: &'static str,
retry_after: Option<i64>,
},
#[error("could not reach {service}: {detail}")]
Network {
service: &'static str,
detail: String,
},
#[error("{service} answered {refusal}")]
Refused {
service: &'static str,
refusal: Box<crate::api::Refusal>,
},
#[error("{service} answered {refusal}")]
Unavailable {
service: &'static str,
refusal: Box<crate::api::Refusal>,
},
#[error("{error}")]
Unnamed { error: Box<ProviderError> },
#[error("{service} has put this account on hold{}", link.as_ref().map(|link| format!(" ({link})")).unwrap_or_default())]
OnHold {
service: &'static str,
link: Option<String>,
},
#[error("{service}'s answer was not understood: {detail}")]
Malformed {
service: &'static str,
detail: String,
},
#[error("{service} no longer accepts this login")]
InvalidGrant { service: &'static str },
#[error("the stored login is not the shape {provider} keeps: {detail}")]
ShapeUnexpected {
provider: ProviderId,
detail: String,
},
#[error("{reason}")]
Unsupported {
provider: ProviderId,
reason: String,
},
#[error("nothing is signed in")]
NoLogin { provider: ProviderId },
#[error("{}", crate::error::codex_home_missing(path))]
CodexHomeMissing { path: std::path::PathBuf },
}
impl ProviderError {
pub(crate) fn answered(service: &'static str, refusal: crate::api::Refusal) -> ProviderError {
let refusal = Box::new(refusal);
if refusal.is_final() {
ProviderError::Refused { service, refusal }
} else {
ProviderError::Unavailable { service, refusal }
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adoption {
PollingWithin(u32),
AtRenewal {
file: std::path::PathBuf,
},
RestartRequired {
program: &'static str,
holders: &'static [crate::holder::Holder],
},
NextOpen {
app: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Behind {
pub path: std::path::PathBuf,
pub held: Held,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Held {
Login,
NoLogin,
Unreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ParkSemantics {
CopyWhileLive,
MoveOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Isolation {
Isolated,
NotIsolated { reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Expiry {
pub(crate) access_expires_at: Option<i64>,
pub(crate) refresh_expires_at: Option<i64>,
}
pub(crate) struct LiveStore {
pub(crate) chain: crate::store::Live,
pub(crate) service: String,
}
pub(crate) fn store_error(error: crate::store::Error) -> ProviderError {
const STORE: &str = "this machine's credential store";
match error {
crate::store::Error::Malformed(detail) => ProviderError::Malformed {
service: STORE,
detail,
},
other => ProviderError::Network {
service: STORE,
detail: other.to_string(),
},
}
}
pub(crate) trait Provider: Send + Sync + std::fmt::Debug {
fn id(&self) -> ProviderId;
fn live(&self, ctx: &Context) -> Result<LiveStore, ProviderError>;
fn read_live(&self, ctx: &Context) -> Result<Option<Credential>, ProviderError> {
let live = self.live(ctx)?;
crate::store::read(&live.chain, &live.service)
.map(|found| found.map(|raw| Credential::new(self.id(), raw)))
.map_err(store_error)
}
fn identity(&self, ctx: &Context, credential: &Credential) -> Result<Identity, ProviderError>;
fn name(
&self,
ctx: &Context,
credential: &Credential,
identity: &Identity,
) -> Result<String, ProviderError>;
fn identify(&self, ctx: &Context, credential: &Credential) -> Result<Owner, ProviderError> {
let identity = self.identity(ctx, credential)?;
let email = self.name(ctx, credential, &identity)?;
Ok(Owner::named(identity, email))
}
fn identifies_by_itself(&self) -> bool;
fn verify(&self, ctx: &Context, credential: &Credential) -> Result<Identity, ProviderError> {
self.identity(ctx, credential)
}
fn usage(
&self,
ctx: &Context,
credential: &Credential,
) -> Result<usage::Snapshot, ProviderError>;
fn renew(
&self,
ctx: &Context,
permit: Permit,
credential: &Credential,
) -> Result<Credential, ProviderError>;
fn slot(&self, ctx: &Context) -> String;
fn write_lock(&self, ctx: &Context) -> Option<std::path::PathBuf>;
fn refresh_lock(&self, ctx: &Context) -> Option<(std::path::PathBuf, std::path::PathBuf)>;
fn own_record(&self, ctx: &Context) -> Option<Owner>;
fn after_switch(
&self,
ctx: &Context,
permit: Permit,
incoming: &crate::state::Account,
outgoing: &Owner,
) -> Result<(), crate::error::Error>;
fn program(&self, ctx: &Context) -> Option<std::path::PathBuf>;
fn sign_in(
&self,
ctx: &Context,
permit: Permit,
dir: &std::path::Path,
) -> std::io::Result<std::process::Command>;
fn read_signin(
&self,
ctx: &Context,
dir: &std::path::Path,
) -> Result<Option<String>, crate::store::Error>;
fn discard_signin(&self, ctx: &Context, permit: Permit, dir: &std::path::Path);
fn read_sign_in(&self, said: &str) -> SignInView;
fn refused_code(&self, since: &str) -> bool;
fn overridden_by(&self, ctx: &Context) -> Vec<String>;
fn behind(&self, ctx: &Context) -> Option<Behind>;
fn adoption(&self, behind: Option<&Behind>) -> Adoption;
fn park_semantics(&self) -> ParkSemantics;
fn private_signin_isolation(&self, ctx: &Context) -> Isolation;
fn slice(&self, live: &Value) -> Result<Value, ProviderError>;
fn plan(&self, document: &Value) -> Option<String>;
fn splice(&self, live: &Value, incoming: &Value) -> Result<Value, ProviderError>;
fn fingerprint(&self, slice: &Value) -> String;
fn expiry(&self, slice: &Value) -> Expiry;
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SignInView {
pub url: Option<String>,
pub wants_code: bool,
}
pub fn sign_in_view(provider: ProviderId, said: &str, pasted: bool) -> SignInView {
match of(provider) {
LoginKind::Document(tool) => {
let read = tool.read_sign_in(said);
SignInView {
wants_code: read.wants_code && !pasted,
..read
}
}
LoginKind::Folder(_) => SignInView::default(),
}
}
pub fn refused_code(provider: ProviderId, since: &str) -> bool {
match of(provider) {
LoginKind::Document(tool) => tool.refused_code(since),
LoginKind::Folder(_) => false,
}
}
pub(crate) fn https_address(said: &str) -> Option<String> {
printed::read(said).find_map(|piece| match piece {
printed::Printed::Text(text) => bare_https_address(text),
printed::Printed::Link(target) => target
.strip_prefix(HTTPS)
.is_some_and(|rest| !rest.is_empty() && !rest.contains(ends_an_address))
.then(|| target.to_owned()),
})
}
const HTTPS: &str = "https://";
fn bare_https_address(text: &str) -> Option<String> {
text.match_indices(HTTPS).find_map(|(at, _)| {
let rest = &text[at + HTTPS.len()..];
let length = rest.find(ends_an_address).unwrap_or(rest.len());
(length > 0).then(|| text[at..at + HTTPS.len() + length].to_owned())
})
}
fn ends_an_address(c: char) -> bool {
c.is_whitespace() || matches!(c, '"' | '\'' | '<' | '>')
}
pub(crate) fn program_of(ctx: &Context, tool: ProviderId) -> Option<std::path::PathBuf> {
program_among(ctx, tool, crate::host::proc::can_run)
}
pub(crate) fn program_among(
ctx: &Context,
tool: ProviderId,
runnable: impl FnMut(&std::path::Path) -> bool,
) -> Option<std::path::PathBuf> {
let installers = ctx
.installer_places
.get(&tool)
.into_iter()
.flatten()
.cloned();
let search = ctx.search_path();
let dirs = std::env::split_paths(&search).chain(installers);
crate::host::program::find_in(ctx.program_for(tool)?, dirs, &ctx.lookup(), runnable)
}
pub(crate) fn installed_among(
ctx: &Context,
mut runnable: impl FnMut(&std::path::Path) -> bool,
) -> Vec<ProviderId> {
ProviderId::ALL
.iter()
.copied()
.filter(|&tool| match of(tool) {
LoginKind::Document(_) => program_among(ctx, tool, &mut runnable).is_some(),
LoginKind::Folder(app) => app.root(ctx).is_ok_and(|root| root.is_dir()),
})
.collect()
}
pub(crate) fn command(
ctx: &Context,
_: Permit,
tool: ProviderId,
) -> std::io::Result<std::process::Command> {
let search = ctx.search_path();
let Some(program) = program_of(ctx, tool) else {
if ctx.os == crate::host::Os::Windows {
return Err(std::io::ErrorKind::NotFound.into());
}
let named = ctx
.program_for(tool)
.unwrap_or(std::path::Path::new(tool.program()));
let mut command = std::process::Command::new(named);
command.env("PATH", search);
return Ok(command);
};
let entries: Vec<std::path::PathBuf> = if search.is_empty() {
Vec::new()
} else {
std::env::split_paths(&search).collect()
};
let path = match program
.parent()
.filter(|dir| !entries.iter().any(|entry| entry == dir))
{
Some(dir) => std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(entries))
.unwrap_or(search),
None => search,
};
let (started, set) = behind_npms_shim(tool, &program).unwrap_or((program, Vec::new()));
let mut command = std::process::Command::new(started);
command.env("PATH", path);
for (name, value) in set {
match value {
Some(value) => command.env(name, value),
None => command.env_remove(name),
};
}
Ok(command)
}
pub(crate) type Launched = Vec<(&'static str, Option<std::ffi::OsString>)>;
fn behind_npms_shim(
tool: ProviderId,
shim: &std::path::Path,
) -> Option<(std::path::PathBuf, Launched)> {
let target = crate::host::program::npm_shim_target(shim)?;
let a_program = target
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("exe"));
if a_program {
return crate::host::proc::can_run(&target).then(|| (target, Vec::new()));
}
match tool {
ProviderId::Codex => codex::paths::npm_native(&target),
ProviderId::Claude | ProviderId::Desktop => None,
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum LoginKind {
Document(&'static dyn Provider),
Folder(&'static dyn folder::Folder),
}
pub(crate) fn of(tool: ProviderId) -> LoginKind {
match tool {
ProviderId::Claude => LoginKind::Document(&claude::engine::Claude),
ProviderId::Codex => LoginKind::Document(&codex::engine::Codex),
ProviderId::Desktop => LoginKind::Folder(&desktop::Desktop),
}
}
pub(crate) fn claude_code() -> &'static dyn Provider {
&claude::engine::Claude
}
pub(crate) fn codex() -> &'static dyn Provider {
&codex::engine::Codex
}
pub(crate) fn slot(ctx: &Context, tool: ProviderId) -> Option<String> {
match of(tool) {
LoginKind::Document(found) => Some(found.slot(ctx)),
LoginKind::Folder(app) => app
.root(ctx)
.ok()
.map(|root| root.to_string_lossy().into_owned()),
}
}
#[cfg(test)]
pub(crate) fn document(tool: ProviderId) -> &'static dyn Provider {
match of(tool) {
LoginKind::Document(found) => found,
LoginKind::Folder(_) => panic!("{tool} keeps no login document"),
}
}
#[cfg(test)]
pub(crate) fn documents() -> impl Iterator<Item = &'static dyn Provider> {
ProviderId::ALL.iter().filter_map(|&tool| match of(tool) {
LoginKind::Document(found) => Some(found),
LoginKind::Folder(_) => None,
})
}
#[cfg(test)]
#[allow(
clippy::disallowed_methods,
reason = "a test sets up its scratch files as a person or a tool would"
)]
mod tests {
use super::*;
#[test]
fn every_provider_code_parses_back_to_itself() {
for &id in ProviderId::ALL {
assert_eq!(ProviderId::parse(id.code()), Some(id), "{id}");
assert!(
id.code().chars().all(|c| c.is_ascii_lowercase()),
"{id} is not a plain lowercase code"
);
}
assert_eq!(ProviderId::parse("nothing"), None);
}
#[test]
fn every_provider_is_in_all() {
for &id in ProviderId::ALL {
match id {
ProviderId::Claude | ProviderId::Codex | ProviderId::Desktop => {}
}
}
assert_eq!(ProviderId::ALL.len(), 3, "add the new provider to ALL");
}
#[test]
fn desktop_parses_as_a_tool_and_keeps_no_command_line() {
assert_eq!(ProviderId::parse("desktop"), Some(ProviderId::Desktop));
assert_eq!(ProviderId::Desktop.name(), "Claude Desktop");
assert_eq!(ProviderId::Desktop.program(), "Claude");
assert!(ProviderId::Desktop.cli().is_none());
assert_eq!(ProviderId::Desktop.signs_in(), SignsIn::InItsApp);
assert!(!ProviderId::Desktop.reads_usage());
assert!(matches!(of(ProviderId::Desktop), LoginKind::Folder(_)));
for tool in [ProviderId::Claude, ProviderId::Codex] {
assert!(tool.cli().is_some(), "{tool}");
assert_eq!(tool.signs_in(), SignsIn::ByItsProgram, "{tool}");
assert!(tool.reads_usage(), "{tool}");
}
assert_eq!(
documents().map(|tool| tool.id()).collect::<Vec<_>>(),
[ProviderId::Claude, ProviderId::Codex]
);
assert_eq!(
sign_in_view(ProviderId::Desktop, "https://claude.ai/login", false),
SignInView::default()
);
assert!(!refused_code(ProviderId::Desktop, "Invalid code"));
}
#[test]
fn the_tool_is_absent_off_macos() {
use crate::host::Os;
for os in [Os::MacOs, Os::Linux, Os::Windows] {
assert_eq!(
ProviderId::Desktop.supported_on(os),
os == Os::MacOs,
"{os:?}"
);
assert!(ProviderId::Claude.supported_on(os), "{os:?}");
assert!(ProviderId::Codex.supported_on(os), "{os:?}");
}
}
#[test]
fn the_code_is_what_serde_writes() {
for &id in ProviderId::ALL {
let written = serde_json::to_value(id).expect("a provider id serialises");
assert_eq!(written, serde_json::json!(id.code()), "{id}");
}
}
#[test]
fn every_implementation_agrees_about_which_tool_it_is() {
for &id in ProviderId::ALL {
let said = match of(id) {
LoginKind::Document(tool) => tool.id(),
LoginKind::Folder(app) => app.id(),
};
assert_eq!(said, id, "{id} is registered against another tool");
}
}
#[test]
fn claude_code_follows_a_switch_on_its_own_and_tolerates_a_copy() {
let claude = document(ProviderId::Claude);
assert_eq!(
claude.adoption(None),
Adoption::PollingWithin(33),
"measured: a session serves its credential from a 30 second cache"
);
assert_eq!(
claude.park_semantics(),
ParkSemantics::CopyWhileLive,
"nothing of Claude Code's revokes for presenting either copy, and the live document holds the machine's other keys"
);
assert_eq!(
claude.private_signin_isolation(&Context::for_unit_test()),
Isolation::Isolated,
"CLAUDE_CONFIG_DIR picks the keychain item by hashing the directory, and there is no second backend that escapes it"
);
}
#[test]
fn a_restart_is_not_a_countdown_of_zero() {
let restart = document(ProviderId::Codex).adoption(None);
assert_ne!(restart, Adoption::PollingWithin(0));
assert!(matches!(Adoption::PollingWithin(33), Adoption::PollingWithin(s) if s == 33));
}
struct Prefix(std::path::PathBuf);
fn file_of(program: &str) -> String {
format!("{program}{}", std::env::consts::EXE_SUFFIX)
}
fn joined(dirs: &[&std::path::Path]) -> std::ffi::OsString {
std::env::join_paths(dirs).expect("a search path")
}
impl Prefix {
fn new(name: &str) -> Prefix {
let root = std::env::temp_dir().join(format!(
"pitboard-search-path-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
let bin = root.join("npm").join("bin");
std::fs::create_dir_all(&bin).expect("a scratch prefix");
for tool in documents().map(|tool| tool.id()) {
let program = bin.join(file_of(tool.program()));
std::fs::write(&program, "").expect("a program");
crate::host::fs::testing::make_runnable(&program).expect("runnable");
}
Prefix(root)
}
fn bin(&self) -> std::path::PathBuf {
self.0.join("npm").join("bin")
}
fn program(&self, tool: ProviderId) -> std::path::PathBuf {
self.bin().join(file_of(tool.program()))
}
fn nowhere(&self) -> std::path::PathBuf {
self.0.join("nowhere")
}
fn decoys(&self) -> (std::path::PathBuf, std::path::PathBuf) {
let (dirs, files) = (self.0.join("dirs"), self.0.join("files"));
for tool in documents().map(|tool| tool.id()) {
std::fs::create_dir_all(dirs.join(file_of(tool.program()))).expect("a directory");
std::fs::create_dir_all(&files).expect("a directory");
let file = files.join(file_of(tool.program()));
std::fs::write(&file, "").expect("a file");
crate::host::fs::testing::deny_running(&file).expect("nobody may run it");
}
(dirs, files)
}
}
impl Drop for Prefix {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn env_of<'a>(command: &'a std::process::Command, name: &str) -> Option<&'a std::ffi::OsStr> {
command
.get_envs()
.find(|(key, _)| *key == name)
.and_then(|(_, value)| value)
}
#[test]
fn a_program_is_looked_for_on_the_search_path_it_is_given() {
let prefix = Prefix::new("find");
let search = joined(&[&prefix.nowhere(), &prefix.bin()]);
let lookup = crate::host::program::Lookup::here();
let found = crate::host::program::find(std::path::Path::new("codex"), &search, &lookup);
assert_eq!(found, Some(prefix.program(ProviderId::Codex)));
let everywhere = match crate::host::OS {
crate::host::Os::MacOs | crate::host::Os::Linux => "ls",
crate::host::Os::Windows => "cmd",
};
assert_eq!(
crate::host::program::find(std::path::Path::new(everywhere), &search, &lookup),
None,
"{everywhere} is on this process's PATH and not on the one given"
);
}
#[test]
fn what_cannot_be_run_is_passed_over() {
let prefix = Prefix::new("decoys");
let (dirs, files) = prefix.decoys();
let search = joined(&[&dirs, &files, &prefix.bin()]);
let lookup = crate::host::program::Lookup::here();
assert_eq!(
crate::host::program::find(std::path::Path::new("codex"), &search, &lookup),
Some(prefix.program(ProviderId::Codex))
);
for decoy in [dirs.join(file_of("codex")), files.join(file_of("codex"))] {
assert_eq!(
crate::host::program::find(&decoy, "".as_ref(), &lookup),
None,
"{}",
decoy.display()
);
}
}
#[test]
fn a_sign_in_runs_the_program_found_with_the_search_path_as_it_is() {
let prefix = Prefix::new("sign-in");
let search = joined(&[&prefix.nowhere(), &prefix.bin()]);
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_search_path(search.to_string_lossy().into_owned());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for tool in documents().map(|tool| tool.id()) {
let command = document(tool)
.sign_in(&ctx, Permit::for_a_test(), dir)
.expect("found");
let program = prefix.program(tool);
assert_eq!(command.get_program(), program.as_os_str(), "{tool}");
assert_eq!(env_of(&command, "PATH"), Some(search.as_os_str()), "{tool}");
assert_eq!(document(tool).program(&ctx), Some(program), "{tool}");
}
}
#[test]
fn a_program_named_outright_is_run_with_its_own_directory_on_path() {
let prefix = Prefix::new("named");
let (system, other) = (prefix.0.join("system"), prefix.0.join("other"));
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_claude_program(prefix.program(ProviderId::Claude))
.with_codex_program(prefix.program(ProviderId::Codex))
.with_search_path(joined(&[&system, &other]).to_string_lossy().into_owned());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for tool in documents().map(|tool| tool.id()) {
let command = document(tool)
.sign_in(&ctx, Permit::for_a_test(), dir)
.expect("found");
assert_eq!(
command.get_program(),
prefix.program(tool).as_os_str(),
"{tool}"
);
assert_eq!(
env_of(&command, "PATH"),
Some(joined(&[&prefix.bin(), &system, &other]).as_os_str()),
"{tool}"
);
}
let mut bin = prefix.bin().into_os_string();
bin.push(std::path::MAIN_SEPARATOR_STR);
let on_it = joined(&[&system, std::path::Path::new(&bin)]);
let ctx = ctx.with_search_path(on_it.to_string_lossy().into_owned());
for tool in documents().map(|tool| tool.id()) {
let command = document(tool)
.sign_in(&ctx, Permit::for_a_test(), dir)
.expect("found");
assert_eq!(env_of(&command, "PATH"), Some(on_it.as_os_str()), "{tool}");
}
}
#[test]
fn a_program_found_nowhere_is_started_by_its_bare_name_only_off_windows() {
let prefix = Prefix::new("nowhere");
let nowhere = prefix.nowhere();
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
for os in [
crate::host::Os::MacOs,
crate::host::Os::Linux,
crate::host::Os::Windows,
] {
let env = crate::context::Environment::on(
os,
[
("HOME", prefix.0.as_os_str()),
("USERPROFILE", prefix.0.as_os_str()),
("PATH", nowhere.as_os_str()),
],
);
let ctx = Context::for_command_line(&env);
for tool in documents().map(|tool| tool.id()) {
let command = document(tool).sign_in(&ctx, Permit::for_a_test(), dir);
assert_eq!(document(tool).program(&ctx), None, "{tool}");
match os {
crate::host::Os::MacOs | crate::host::Os::Linux => {
let command = command.expect("left to the search path");
assert_eq!(command.get_program(), tool.program(), "{tool}");
assert_eq!(
env_of(&command, "PATH"),
Some(nowhere.as_os_str()),
"{tool}"
);
}
crate::host::Os::Windows => assert_eq!(
command.map(drop).map_err(|e| e.kind()),
Err(std::io::ErrorKind::NotFound),
"{tool}"
),
}
}
}
}
fn npm_shim(at: &std::path::Path, target: &str, interpreted: bool) {
let run = if interpreted {
format!(
"\r\nIF EXIST \"%dp0%\\node.exe\" (\r\n SET \"_prog=%dp0%\\node.exe\"\r\n) ELSE (\r\n \
SET \"_prog=node\"\r\n)\r\n\r\nendLocal & goto #_undefined_# 2>NUL || title \
%COMSPEC% & set PATHEXT=%PATHEXT:;.JS;=;% & \"%_prog%\" \"%dp0%\\{target}\" %*\r\n"
)
} else {
format!("\"%dp0%\\{target}\" %*\r\n")
};
std::fs::write(
at,
format!(
"@ECHO off\r\nGOTO start\r\n:find_dp0\r\nSET dp0=%~dp0\r\nEXIT /b\r\n:start\r\n\
SETLOCAL\r\nCALL :find_dp0\r\n{run}"
),
)
.expect("a shim");
crate::host::fs::testing::make_runnable(at).expect("runnable");
}
fn a_program_at(at: &std::path::Path) -> std::path::PathBuf {
std::fs::create_dir_all(at.parent().expect("its folder")).expect("a folder");
std::fs::write(at, "").expect("a program");
crate::host::fs::testing::make_runnable(at).expect("runnable");
at.to_path_buf()
}
fn under(root: &std::path::Path, parts: &[&str]) -> std::path::PathBuf {
parts
.iter()
.fold(root.to_path_buf(), |at, part| at.join(part))
}
#[test]
fn behind_npms_shim_a_sign_in_starts_the_program_the_shim_would() {
let prefix = Prefix::new("npm-shim");
let npm = prefix.0.join("npm");
let claude = a_program_at(&under(
&npm,
&[
"node_modules",
"@anthropic-ai",
"claude-code",
"bin",
"claude.exe",
],
));
npm_shim(
&npm.join("claude.cmd"),
r"node_modules\@anthropic-ai\claude-code\bin\claude.exe",
false,
);
let package = under(&npm, &["node_modules", "@openai", "codex"]);
a_program_at(&package.join("bin").join("codex.js"));
let platform = under(&package, &["node_modules", "@openai", "codex-win32-x64"]);
let codex = a_program_at(&under(
&platform,
&["vendor", "x86_64-pc-windows-msvc", "bin", "codex.exe"],
));
std::fs::write(platform.join("package.json"), "{}").expect("its package.json");
npm_shim(
&npm.join("codex.cmd"),
r"node_modules\@openai\codex\bin\codex.js",
true,
);
let search = joined(&[&prefix.nowhere()]);
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_claude_program(npm.join("claude.cmd"))
.with_codex_program(npm.join("codex.cmd"))
.with_search_path(search.to_string_lossy().into_owned());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
let on_path = joined(&[&npm, &prefix.nowhere()]);
for (tool, native) in [(ProviderId::Claude, &claude), (ProviderId::Codex, &codex)] {
let command = document(tool)
.sign_in(&ctx, Permit::for_a_test(), dir)
.expect("found");
assert_eq!(command.get_program(), native.as_os_str(), "{tool}");
assert_eq!(
env_of(&command, "PATH"),
Some(on_path.as_os_str()),
"{tool}"
);
}
let command = codex_sign_in(&ctx, dir);
let root = crate::host::without_verbatim_prefix(
std::fs::canonicalize(&package).expect("the package's folder"),
);
assert_eq!(
env_of(&command, "CODEX_MANAGED_PACKAGE_ROOT"),
Some(root.as_os_str())
);
assert_eq!(
env_of(&command, "CODEX_MANAGED_BY_NPM"),
Some(std::ffi::OsStr::new("1"))
);
for other in [
"CODEX_MANAGED_BY_BUN",
"CODEX_MANAGED_BY_PNPM",
"CODEX_MANAGED_BY_VITE_PLUS",
] {
assert!(
command
.get_envs()
.any(|(key, value)| key == other && value.is_none()),
"{other} removed"
);
}
}
#[test]
fn behind_any_other_cmd_a_sign_in_starts_the_cmd() {
let prefix = Prefix::new("npm-hoisted");
let npm = prefix.0.join("npm");
let package = under(&npm, &["node_modules", "@openai", "codex"]);
a_program_at(&package.join("bin").join("codex.js"));
npm_shim(
&npm.join("codex.cmd"),
r"node_modules\@openai\codex\bin\codex.js",
true,
);
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_codex_program(npm.join("codex.cmd"))
.with_search_path(prefix.nowhere().to_string_lossy().into_owned());
let dir = std::path::Path::new("/tmp/pitboard-signin-scratch");
assert_eq!(
codex_sign_in(&ctx, dir).get_program(),
npm.join("codex.cmd").as_os_str(),
"no codex.exe for it to start"
);
let hoisted = under(&npm, &["node_modules", "@openai", "codex-win32-arm64"]);
let codex = a_program_at(&under(
&hoisted,
&["vendor", "aarch64-pc-windows-msvc", "bin", "codex.exe"],
));
std::fs::write(hoisted.join("package.json"), "{}").expect("its package.json");
assert_eq!(codex_sign_in(&ctx, dir).get_program(), codex.as_os_str());
let own = a_program_at(&npm.join("other").join("codex.cmd"));
std::fs::write(&own, "@echo off\r\n\"%~dp0codex.exe\" %*\r\n").expect("a script");
a_program_at(&npm.join("other").join("codex.exe"));
let ctx = ctx.with_codex_program(own.clone());
assert_eq!(codex_sign_in(&ctx, dir).get_program(), own.as_os_str());
}
fn codex_sign_in(ctx: &Context, dir: &std::path::Path) -> std::process::Command {
document(ProviderId::Codex)
.sign_in(ctx, Permit::for_a_test(), dir)
.expect("found")
}
#[test]
fn the_address_to_open_is_the_one_the_person_goes_to() {
let mut said = String::from(
"Starting local login server on http://localhost:1455.\n\
If your browser did not open, navigate to this URL to authenticate:\n\n\
https://auth.openai.com/oauth/authorize?response_type=code&state=x\u{1b}[0m\n",
);
assert_eq!(
sign_in_view(ProviderId::Codex, &said, false).url.as_deref(),
Some("https://auth.openai.com/oauth/authorize?response_type=code&state=x")
);
said.push_str("Paste code here if prompted > ");
assert!(!sign_in_view(ProviderId::Codex, &said, false).wants_code);
let said = "Paste code here if prompted > ";
assert!(sign_in_view(ProviderId::Claude, said, false).wants_code);
}
#[test]
fn a_code_is_asked_for_only_where_the_tool_takes_one() {
let said = "Paste code here if prompted > ";
for (tool, takes) in [(ProviderId::Claude, true), (ProviderId::Codex, false)] {
assert_eq!(sign_in_view(tool, said, false).wants_code, takes, "{tool}");
assert!(
!sign_in_view(tool, said, true).wants_code,
"{tool}: asked once"
);
}
}
#[test]
fn a_sign_in_that_has_said_nothing_offers_nothing() {
for &tool in ProviderId::ALL {
assert_eq!(
sign_in_view(tool, "", false),
SignInView::default(),
"{tool}"
);
}
}
#[test]
fn an_address_ends_where_an_address_printed_bare_cannot_go_on() {
let ends = [
'\t', '\n', '\u{b}', '\u{c}', '\r', ' ', '"', '\'', '<', '>', '\u{1b}', '\u{85}',
'\u{a0}', '\u{1680}', '\u{2000}', '\u{200a}', '\u{2028}', '\u{2029}', '\u{202f}',
'\u{205f}', '\u{3000}',
];
for end in ends {
let said = format!("x https://a.b/c{end}d e");
assert_eq!(
https_address(&said).as_deref(),
Some("https://a.b/c"),
"{:?}",
end
);
}
for kept in ['\u{1c}', '\u{7f}', '\u{180e}', '\u{200b}', '\u{feff}'] {
let said = format!("x https://a.b/c{kept}d e");
assert_eq!(
https_address(&said),
Some(format!("https://a.b/c{kept}d")),
"{:?}",
kept
);
}
}
#[test]
fn only_an_https_address_with_something_in_it_is_one_to_open() {
assert_eq!(
https_address("https:// then https://x.y").as_deref(),
Some("https://x.y")
);
assert_eq!(https_address("http://localhost:1455/auth/callback"), None);
assert_eq!(https_address("HTTPS://x.y"), None);
}
#[test]
fn a_hyperlinks_address_is_where_it_goes() {
let cases = [
(
"\u{1b}]8;;https://a.b/c\u{7}sign in\u{1b}]8;;\u{7}",
"https://a.b/c",
),
(
"\u{1b}]8;id=1;https://a.b/c\u{1b}\\https://x.y\u{1b}]8;;\u{1b}\\",
"https://a.b/c",
),
(
"\u{1b}]8;;http://localhost:1455\u{7}\u{1b}[94mhttps://x.y\u{1b}[39m\u{1b}]8;;\u{7}",
"https://x.y",
),
(
"\u{1b}]8;;https://a.b/c d\u{7}https://x.y\u{1b}]8;;\u{7}",
"https://x.y",
),
];
for (said, address) in cases {
assert_eq!(
https_address(&format!("visit: {said}\n")).as_deref(),
Some(address),
"{said:?}"
);
}
}
#[test]
fn an_address_a_terminal_does_not_show_is_none_to_open() {
assert_eq!(https_address("\u{1b}]0;https://a.b/c\u{7}ready"), None);
}
#[test]
fn a_hyperlink_read_part_way_through_offers_all_of_its_address_or_none() {
let said = "visit: \u{1b}]8;;https://a.b/c?d=e\u{7}https://a.b/c?d=e\u{1b}]8;;\u{7}\n";
let whole = said.find('\u{7}').expect("the end of where it goes") + 1;
for (at, _) in said.char_indices().chain([(said.len(), ' ')]) {
let offered = https_address(&said[..at]);
if at < whole {
assert_eq!(offered, None, "{:?}", &said[..at]);
} else {
assert_eq!(offered.as_deref(), Some("https://a.b/c?d=e"), "{at}");
}
}
}
#[test]
#[allow(
clippy::disallowed_methods,
reason = "this process's own PATH is what the context is meant to read"
)]
fn the_search_path_is_this_processs_own_path_unless_given() {
let ctx = Context::new(std::path::PathBuf::from("/nowhere"));
assert_eq!(
ctx.search_path(),
std::env::var_os("PATH").unwrap_or_default()
);
assert_eq!(
ctx.with_search_path("/opt/tools/bin".into()).search_path(),
"/opt/tools/bin"
);
}
}