use crate::paths::Paths;
use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
pub(crate) const PROXY_PASSTHROUGH_EXIT_STATUS: i32 = 3;
pub fn shim_path(paths: &Paths) -> PathBuf {
shim_path_for(paths, "claude-code")
}
pub fn shim_bin_dir(paths: &Paths) -> PathBuf {
paths.store_dir().join("bin")
}
pub fn shim_path_for(paths: &Paths, tool: &str) -> PathBuf {
let bin = match tool {
"codex" => "codex",
"gemini" => "gemini",
"antigravity" => "agy",
_ => "claude",
};
paths.store_dir().join("bin").join(bin)
}
fn sh_quote(p: &Path) -> String {
let s = p.to_string_lossy();
format!("'{}'", s.replace('\'', "'\\''"))
}
fn proxy_result_script(tool: &str) -> String {
format!(
r#"sx_proxy_status=$?
sx_use_proxy=no
sx_proxy_bad=no
if [ "$sx_proxy_status" -eq {passthrough} ] && [ -z "$port" ]; then
:
elif [ "$sx_proxy_status" -ne 0 ]; then
sx_proxy_bad=yes
else
case "$port" in
''|*[!0-9]*) sx_proxy_bad=yes ;;
*)
sx_port=$port
while [ "${{sx_port#0}}" != "$sx_port" ]; do sx_port=${{sx_port#0}}; done
case "$sx_port" in
''|??????*) sx_proxy_bad=yes ;;
*)
if [ "$sx_port" -gt 65535 ]; then
sx_proxy_bad=yes
else
port=$sx_port
sx_use_proxy=yes
fi
;;
esac
;;
esac
fi
if [ "$sx_proxy_bad" = yes ]; then
printf '%s\n' 'swapdex: managed proxy startup failed; run `swapdex proxy --ensure --tool {tool}` for details.' >&2
exit 1
fi"#,
passthrough = PROXY_PASSTHROUGH_EXIT_STATUS,
tool = tool,
)
}
pub fn shim_script(pointer: &Path, real_claude: &Path, swapdex: &Path) -> String {
let proxy_result = proxy_result_script("claude-code").replace('\n', "\n\t");
format!(
"#!/bin/sh\n\
# swapdex claude shim - launch claude in the default account's slot.\n\
# Managed by swapdex; re-created by `swapdex shim`.\n\
# Signing in must reach Anthropic directly. The OAuth exchange is between\n\
# the browser and the real API, and a proxy in the middle both breaks the\n\
# code exchange and answers with whichever account it already has - so a\n\
# fresh slot looks signed in as somebody else, or the prompt takes no\n\
# input at all.\n\
# Only documented top-level authentication commands bypass managed\n\
# routing. Prompt text and option values can contain words like login.\n\
sx_plain=no\n\
sx_options=yes\n\
sx_skip=\n\
sx_command=\n\
sx_auth_command=\n\
for a in \"$@\"; do\n\
\tif [ -n \"$sx_skip\" ]; then\n\
\t\tsx_skip=\n\
\t\tcontinue\n\
\tfi\n\
\tif [ \"$sx_command\" = auth ] && [ -z \"$sx_auth_command\" ]; then\n\
\t\tsx_auth_command=other\n\
\t\tcase \"$a\" in login|logout|status|-h|--help) sx_auth_command=\"$a\" ;; esac\n\
\t\tcontinue\n\
\tfi\n\
\tif [ -n \"$sx_command\" ] && [ \"$sx_command\" != ambiguous ]; then continue; fi\n\
\tif [ \"$sx_options\" = no ]; then\n\
\t\tsx_command=prompt\n\
\t\tcontinue\n\
\tfi\n\
\tcase \"$a\" in\n\
\t\t--) sx_options=no; sx_command=prompt ;;\n\
\t\t-h|--help|-v|--version) sx_plain=yes ;;\n\
\t\t-p|--print|--print=*|-p?*) sx_command=prompt ;;\n\
\t\t-m|--model|--permission-mode|--settings) sx_skip=value ;;\n\
\t\t--setting-sources|--plugin-dir|--plugin-url|--cwd) sx_skip=value ;;\n\
\t\t--debug-file) sx_skip=value ;;\n\
\t\t-m?*|--model=*|--permission-mode=*|--settings=*) ;;\n\
\t\t--setting-sources=*|--plugin-dir=*|--plugin-url=*|--cwd=*) ;;\n\
\t\t--debug-file=*) ;;\n\
\t\t# Optional and variadic values are indistinguishable from a later\n\
\t\t# command token, so these forms cannot authorize direct auth.\n\
\t\t-d|--debug|--debug=*|-d?*|--mcp-config|--mcp-config=*) sx_command=ambiguous ;;\n\
\t\t--verbose) ;;\n\
\t\t-*) sx_command=unknown ;;\n\
\t\t*) if [ \"$sx_command\" != ambiguous ]; then sx_command=\"$a\"; fi ;;\n\
\tesac\n\
done\n\
case \"$sx_command:$sx_auth_command\" in\n\
\tauth:login|auth:logout|auth:status|auth:-h|auth:--help|setup-token:) sx_plain=yes ;;\n\
esac\n\
# Match the documented opt-in value 1 for alternate providers. Empty,\n\
# 0, and false remain managed by swapdex.\n\
if [ -n \"$ANTHROPIC_BASE_URL\" ] ||\n\
\t[ \"$CLAUDE_CODE_USE_BEDROCK\" = 1 ] ||\n\
\t[ \"$CLAUDE_CODE_USE_MANTLE\" = 1 ] ||\n\
\t[ \"$CLAUDE_CODE_USE_VERTEX\" = 1 ] ||\n\
\t[ \"$CLAUDE_CODE_USE_FOUNDRY\" = 1 ] ||\n\
\t[ \"$CLAUDE_CODE_USE_ANTHROPIC_AWS\" = 1 ]; then\n\
\tsx_plain=yes\n\
fi\n\
# Ask swapdex for a live proxy (it starts one if needed and prints one\n\
# validated port). Any uncertain managed state stops before Claude.\n\
if [ \"$sx_plain\" = no ]; then\n\
\tport=$({sx} proxy --ensure --tool claude-code 2>/dev/null)\n\
\t{proxy_result}\n\
\tif [ \"$sx_use_proxy\" = yes ]; then\n\
\t\tANTHROPIC_BASE_URL=\"http://127.0.0.1:$port\"\n\
\t\texport ANTHROPIC_BASE_URL\n\
\tfi\n\
fi\n\
if [ -z \"$CLAUDE_CONFIG_DIR\" ]; then\n\
\tdir=$(cat {ptr} 2>/dev/null)\n\
\tif [ -n \"$dir\" ]; then\n\
\t\tCLAUDE_CONFIG_DIR=\"$dir\"\n\
\t\texport CLAUDE_CONFIG_DIR\n\
\tfi\n\
fi\n\
if [ -n \"$CLAUDE_CONFIG_DIR\" ]; then\n\
\texec {sx} claude-launch --native {real} -- \"$@\"\n\
fi\n\
exec {real} \"$@\"\n",
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_claude),
proxy_result = proxy_result,
)
}
pub(crate) fn claude_authentication_command(args: &[String]) -> bool {
let mut args = args.iter().map(String::as_str);
while let Some(arg) = args.next() {
match arg {
"auth" => {
return matches!(
args.next(),
Some("login" | "logout" | "status" | "-h" | "--help")
)
}
"setup-token" => return true,
"--verbose" | "-h" | "--help" | "-v" | "--version" => {}
"-m" | "--model" | "--permission-mode" | "--settings" | "--setting-sources"
| "--plugin-dir" | "--plugin-url" | "--cwd" | "--debug-file" => {
if args.next().is_none() {
return false;
}
}
value
if (value.starts_with("-m") && value.len() > 2)
|| [
"--model=",
"--permission-mode=",
"--settings=",
"--setting-sources=",
"--plugin-dir=",
"--plugin-url=",
"--cwd=",
"--debug-file=",
]
.iter()
.any(|prefix| value.starts_with(prefix)) => {}
_ => return false,
}
}
false
}
pub fn codex_shim_script(pointer: &Path, real_codex: &Path, swapdex: &Path) -> String {
let proxy_result = proxy_result_script("codex");
format!(
r#"#!/bin/sh
# swapdex codex shim - launch codex in the default account's slot.
# Managed by swapdex; re-created by `swapdex shim`.
if [ -z "$CODEX_HOME" ]; then
dir=$(cat {ptr} 2>/dev/null)
if [ -n "$dir" ]; then
CODEX_HOME="$dir"
export CODEX_HOME
fi
fi
# Parse the command separately from option values and prompt text. In
# particular, `exec login` is a model prompt, not an OAuth operation.
sx_plain=no
sx_skip=
sx_command=
sx_options=yes
sx_arg_index=0
sx_last_config=0
port=
sx_explicit_config() {{
sx_key=$(printf '%s' "${{1%%=*}}" | tr -d '[:space:]')
case "$sx_key" in model_provider|openai_base_url|model_providers.*) return 0 ;; esac
return 1
}}
for a in "$@"; do
sx_arg_index=$((sx_arg_index + 1))
if [ "$sx_skip" = config ]; then
if sx_explicit_config "$a"; then sx_plain=yes; fi
sx_skip=
continue
fi
if [ "$sx_skip" = value ]; then
sx_skip=
continue
fi
if [ "$sx_skip" = images ]; then
case "$a" in -*) sx_skip= ;; *) continue ;; esac
fi
[ "$sx_options" = yes ] || continue
case "$a" in
--) sx_options=no ;;
-c|--config) sx_last_config=$sx_arg_index; sx_skip=config ;;
--config=*) sx_last_config=$sx_arg_index; if sx_explicit_config "${{a#--config=}}"; then sx_plain=yes; fi ;;
-c?*) sx_last_config=$sx_arg_index; if sx_explicit_config "${{a#-c}}"; then sx_plain=yes; fi ;;
-p|--profile) sx_skip=value ;;
-p?*|--profile=*) ;;
--remote|--remote-auth-token-env|--local-provider) sx_plain=yes; sx_skip=value ;;
--remote=*|--remote-auth-token-env=*|--local-provider=*|--oss) sx_plain=yes ;;
-i|--image) sx_skip=images ;;
-C|--cd|-m|--model|-s|--sandbox|-a|--ask-for-approval|--add-dir|--enable|--disable) sx_skip=value ;;
-h|--help|-V|--version) sx_plain=yes ;;
-*) ;;
*)
if [ -z "$sx_command" ]; then
sx_command="$a"
# Commands that reach no model: auth, local thread-store edits,
# sandboxed local commands and self-maintenance. Routing them
# cost a session repair and a proxy check on every call.
case "$a" in login|logout|completion|mcp|mcp-server|debug|features|apply|help|queue|archive|unarchive|delete|migrate-rollouts|sandbox|plugin|update) sx_plain=yes ;; esac
fi
;;
esac
done
if [ "$sx_plain" = no ]; then
if ! {sx} repair-codex-sessions --quiet; then
printf '%s\n' 'swapdex: session repair was incomplete; run swapdex repair-codex-sessions for details.' >&2
fi
port=$({sx} proxy --ensure --tool codex 2>/dev/null)
{proxy_result}
# Codex repaints its usage only from reads at `chatgpt_base_url`, and takes
# that URL only over HTTPS. The proxy answers it on a TLS port with a
# certificate Codex is told to trust. Skipped when the user already names a
# CA for Codex: replacing theirs would break whatever it was there for.
sx_usage=
if [ "$sx_use_proxy" = yes ] && [ -z "${{CODEX_CA_CERTIFICATE:-}}" ] && [ -z "${{SSL_CERT_FILE:-}}" ] && [ -r {ca} ]; then
sx_usage=$({sx} proxy --usage-port --tool codex 2>/dev/null)
case "$sx_usage" in ''|*[!0-9]*) sx_usage= ;; esac
fi
if [ -n "$sx_usage" ]; then
CODEX_CA_CERTIFICATE={ca}
export CODEX_CA_CERTIFICATE
fi
if [ "$sx_use_proxy" = yes ]; then
if [ "$sx_last_config" -eq 0 ]; then
if [ -n "$sx_usage" ]; then
set -- -c openai_base_url="http://127.0.0.1:$port/v1" -c chatgpt_base_url="https://127.0.0.1:$sx_usage/backend-api/" "$@"
else
set -- -c openai_base_url="http://127.0.0.1:$port/v1" "$@"
fi
else
# Codex can discard root -c flags when a subcommand has its own.
# Rebuild the argument list so the managed URL has the same scope
# as the caller's last real -c, without moving prompt text or --.
sx_arg_index=0
for sx_arg in "$@"; do
if [ "$sx_arg_index" -eq 0 ]; then set --; fi
sx_arg_index=$((sx_arg_index + 1))
if [ "$sx_arg_index" -eq "$sx_last_config" ]; then
set -- "$@" -c openai_base_url="http://127.0.0.1:$port/v1"
if [ -n "$sx_usage" ]; then
set -- "$@" -c chatgpt_base_url="https://127.0.0.1:$sx_usage/backend-api/"
fi
fi
set -- "$@" "$sx_arg"
done
fi
fi
fi
exec {real} "$@"
"#,
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_codex),
proxy_result = proxy_result,
ca = sh_quote(
&pointer
.parent()
.unwrap_or(Path::new("."))
.join("codex-tls")
.join("ca.pem")
),
)
}
pub fn proxy_marker(paths: &Paths) -> PathBuf {
proxy_marker_for(paths, "claude-code")
}
pub fn proxy_marker_for(paths: &Paths, tool: &str) -> PathBuf {
match tool {
"codex" => paths.store_dir().join("proxy-codex"),
"gemini" => paths.store_dir().join("proxy-gemini"),
"antigravity" => paths.store_dir().join("proxy-antigravity"),
_ => paths.store_dir().join("proxy"),
}
}
const SHIM_MARKER: &str = "swapdex claude shim";
const SHIM_MARKER_CODEX: &str = "swapdex codex shim";
fn is_our_shim(path: &Path) -> bool {
let mut buf = [0u8; 256];
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
use std::io::Read;
let n = f.read(&mut buf).unwrap_or(0);
let head = String::from_utf8_lossy(&buf[..n]);
head.contains(SHIM_MARKER) || head.contains(SHIM_MARKER_CODEX)
}
pub(crate) fn resolved_on_path(binary: &str) -> Option<(PathBuf, bool)> {
let path = std::env::var_os("PATH")?;
let cwd = std::env::current_dir().ok();
for dir in std::env::split_paths(&path) {
let dir = if dir.is_absolute() {
dir
} else if let Some(cwd) = &cwd {
cwd.join(dir)
} else {
continue;
};
let cand = dir.join(binary);
if is_executable_file(&cand) {
let ours = is_our_shim(&cand);
return Some((cand, ours));
}
}
None
}
fn find_real_claude(shim_dir: &Path) -> Option<PathBuf> {
find_real(shim_dir, "claude")
}
fn find_real(shim_dir: &Path, bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
let cwd = std::env::current_dir().ok();
for dir in std::env::split_paths(&path) {
let dir = if dir.is_absolute() {
dir
} else if let Some(cwd) = &cwd {
cwd.join(dir)
} else {
continue;
};
if dir == shim_dir {
continue;
}
let cand = dir.join(bin);
if is_executable_file(&cand) && !is_our_shim(&cand) {
return Some(cand);
}
}
None
}
fn is_executable_file(path: &Path) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
#[derive(Debug, PartialEq)]
pub enum ShimReach {
Active,
ConfiguredElsewhere,
Missing,
}
pub fn shim_reach(active: bool, profile_text: Option<&str>, shim_dir: &Path) -> ShimReach {
shim_reach_at_home(active, profile_text, shim_dir, dirs::home_dir().as_deref())
}
pub fn shim_reach_for(
paths: &Paths,
active: bool,
profile_text: Option<&str>,
shim_dir: &Path,
) -> ShimReach {
shim_reach_at_home(active, profile_text, shim_dir, Some(paths.home()))
}
fn shim_reach_at_home(
active: bool,
profile_text: Option<&str>,
shim_dir: &Path,
home: Option<&Path>,
) -> ShimReach {
if active {
return ShimReach::Active;
}
match profile_text {
Some(t) if profile_already_adds_at_home(t, shim_dir, home) => {
ShimReach::ConfiguredElsewhere
}
_ => ShimReach::Missing,
}
}
pub fn shell_profile_text() -> Option<(PathBuf, String)> {
let paths = Paths::resolve().ok()?;
shell_profile_text_for(&paths)
}
pub fn shell_profile_text_for(paths: &Paths) -> Option<(PathBuf, String)> {
let p = shell_profile_at(paths.home())?;
let t = std::fs::read_to_string(&p).ok()?;
Some((p, t))
}
fn profile_already_adds_at_home(profile_text: &str, shim_dir: &Path, home: Option<&Path>) -> bool {
let full = shim_dir.to_string_lossy().to_string();
let home = home.map(|h| h.to_string_lossy().to_string());
let alts: Vec<String> = home
.iter()
.filter_map(|h| full.strip_prefix(h.as_str()))
.flat_map(|rest| [format!("$HOME{rest}"), format!("~{rest}")])
.collect();
profile_text.lines().any(|l| {
let l = l.trim();
if !l.contains("PATH") || l.starts_with('#') {
return false;
}
l.contains(&full) || alts.iter().any(|a| l.contains(a))
})
}
fn path_line(shim_dir: &Path) -> String {
format!("export PATH=\"{}:$PATH\"", shim_dir.display())
}
const PROFILE_MARKER: &str = "# added by swapdex (claude shim)";
fn shell_profile_at(home: &Path) -> Option<PathBuf> {
let shell = std::env::var("SHELL").unwrap_or_default();
let name = shell.rsplit('/').next().unwrap_or("");
match name {
"zsh" => Some(home.join(".zshrc")),
"bash" => {
let bp = home.join(".bash_profile");
if bp.exists() {
Some(bp)
} else {
Some(home.join(".bashrc"))
}
}
_ => None,
}
}
fn already_on_path(shim_dir: &Path) -> bool {
std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).any(|d| d == shim_dir))
.unwrap_or(false)
}
pub enum PathSetup {
AlreadyThere,
Added(PathBuf),
Manual,
}
fn read_profile_for_edit(profile: &Path) -> Result<String> {
match std::fs::read_to_string(profile) {
Ok(s) => Ok(s),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
Err(e) => bail!(
"cannot read {} ({e}) - refusing to rewrite it, because rewriting \
what we could not read would replace it",
profile.display()
),
}
}
pub fn ensure_on_path(shim_dir: &Path) -> Result<PathSetup> {
let paths = Paths::resolve().context("resolve paths for shell profile")?;
ensure_on_path_for(&paths, shim_dir)
}
pub fn ensure_on_path_for(paths: &Paths, shim_dir: &Path) -> Result<PathSetup> {
if already_on_path(shim_dir) {
return Ok(PathSetup::AlreadyThere);
}
let Some(profile) = shell_profile_at(paths.home()) else {
return Ok(PathSetup::Manual);
};
let existing = read_profile_for_edit(&profile)?;
let line = path_line(shim_dir);
if existing.contains(PROFILE_MARKER)
|| profile_already_adds_at_home(&existing, shim_dir, Some(paths.home()))
{
return Ok(PathSetup::Added(profile));
}
let mut out = existing;
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&format!("\n{PROFILE_MARKER}\n{line}\n"));
std::fs::write(&profile, out).with_context(|| format!("edit {}", profile.display()))?;
Ok(PathSetup::Added(profile))
}
#[derive(Debug, PartialEq)]
pub enum PinVerdict {
Pin(u16),
RefuseNoService,
}
pub fn pin_verdict(service_installed: bool, port: u16) -> PinVerdict {
if service_installed {
PinVerdict::Pin(port)
} else {
PinVerdict::RefuseNoService
}
}
pub fn pin_is_ours(pinned: Option<u16>, mine: u16) -> bool {
pinned == Some(mine)
}
pub fn withdraw_pin(paths: &Paths, mine: u16) -> bool {
let file = paths.claude_dir().join("settings.json");
let Ok(text) = std::fs::read_to_string(&file) else {
return false;
};
let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&text) else {
return false;
};
if !pin_is_ours(pinned_port(&v), mine) {
return false;
}
let emptied = match v.get_mut("env").and_then(|e| e.as_object_mut()) {
Some(env) => {
env.remove("ANTHROPIC_BASE_URL");
env.is_empty()
}
None => false,
};
if emptied {
if let Some(o) = v.as_object_mut() {
o.remove("env");
}
}
let Ok(body) = serde_json::to_vec_pretty(&v) else {
return false;
};
crate::atomic::write_secret(&file, &body).is_ok()
}
pub fn pinned_port(settings: &serde_json::Value) -> Option<u16> {
let url = settings.get("env")?.get("ANTHROPIC_BASE_URL")?.as_str()?;
let rest = url.strip_prefix("http://")?;
let (host, port) = rest.trim_end_matches('/').rsplit_once(':')?;
if host != "127.0.0.1" && host != "localhost" {
return None;
}
port.parse().ok()
}
pub fn with_base_url(settings: &serde_json::Value, port: u16) -> serde_json::Value {
let mut out = settings.clone();
if !out.is_object() {
out = serde_json::json!({});
}
let obj = out.as_object_mut().expect("object");
let env = obj.entry("env").or_insert_with(|| serde_json::json!({}));
if !env.is_object() {
*env = serde_json::json!({});
}
env.as_object_mut().expect("env object").insert(
"ANTHROPIC_BASE_URL".to_string(),
serde_json::Value::String(format!("http://127.0.0.1:{port}")),
);
out
}
pub fn pin_base_url(paths: &Paths, port: u16, service_installed: bool) -> Result<Option<PathBuf>> {
if pin_verdict(service_installed, port) == PinVerdict::RefuseNoService {
return Ok(None);
}
let file = paths.claude_dir().join("settings.json");
let current: serde_json::Value = std::fs::read(&file)
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_else(|| serde_json::json!({}));
let next = with_base_url(¤t, port);
if next == current {
return Ok(Some(file));
}
if file.exists() {
let _ = std::fs::copy(&file, file.with_extension("json.swapdex-bak"));
}
if let Some(d) = file.parent() {
std::fs::create_dir_all(d).ok();
}
let bytes = serde_json::to_vec_pretty(&next).context("serialize settings.json")?;
crate::atomic::write_secret(&file, &bytes).context("write settings.json")?;
Ok(Some(file))
}
#[derive(Debug, PartialEq)]
pub enum PathVerdict {
Wins,
Shadowed(String),
Absent,
}
pub fn path_verdict(shim_dir: &std::path::Path, entries: &[&str]) -> PathVerdict {
path_verdict_for(shim_dir, entries, "claude")
}
pub fn path_verdict_for(shim_dir: &std::path::Path, entries: &[&str], binary: &str) -> PathVerdict {
path_verdict_with(shim_dir, entries, &|d| {
is_executable_file(&std::path::Path::new(d).join(binary))
})
}
pub fn path_verdict_with(
shim_dir: &std::path::Path,
entries: &[&str],
holds_tool: &dyn Fn(&str) -> bool,
) -> PathVerdict {
let shim = shim_dir.to_string_lossy();
if !entries.iter().any(|e| *e == shim) {
return PathVerdict::Absent;
}
for e in entries {
if *e == shim {
return PathVerdict::Wins;
}
if holds_tool(e) {
return PathVerdict::Shadowed((*e).to_string());
}
}
PathVerdict::Absent
}
pub fn install(paths: &Paths) -> Result<(PathBuf, PathBuf)> {
install_claude(paths)?.context("could not find the real `claude` on PATH - install it first")
}
pub fn install_claude(paths: &Paths) -> Result<Option<(PathBuf, PathBuf)>> {
let shim = shim_path(paths);
let shim_dir = shim
.parent()
.map(|p| p.to_path_buf())
.context("shim path has no parent")?;
let Some(real) = find_real_claude(&shim_dir) else {
return Ok(None);
};
let pointer = paths.store_dir().join("active-claude");
let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("swapdex"));
std::fs::create_dir_all(&shim_dir).context("create shim dir")?;
std::fs::write(&shim, shim_script(&pointer, &real, &me)).context("write shim")?;
make_executable(&shim)?;
Ok(Some((shim, shim_dir)))
}
pub fn install_codex(paths: &Paths) -> Result<Option<PathBuf>> {
let shim = shim_path_for(paths, "codex");
let shim_dir = shim
.parent()
.map(|p| p.to_path_buf())
.context("shim path has no parent")?;
let Some(real) = find_real(&shim_dir, "codex") else {
return Ok(None);
};
let pointer = paths.store_dir().join("active-codex");
let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("swapdex"));
std::fs::create_dir_all(&shim_dir).context("create shim dir")?;
std::fs::write(&shim, codex_shim_script(&pointer, &real, &me)).context("write codex shim")?;
make_executable(&shim)?;
Ok(Some(shim))
}
fn make_executable(p: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o755))
.context("chmod shim")?;
}
Ok(())
}
#[cfg(test)]
mod reach_tests {
use super::*;
#[test]
fn a_shell_that_never_read_the_profile_is_not_a_broken_setup() {
let dir = Path::new("/Users/x/Library/Application Support/swapdex/bin");
let zshrc = "export PATH=\"/Users/x/Library/Application Support/swapdex/bin:$PATH\"\n";
assert_eq!(
shim_reach(false, Some(zshrc), dir),
ShimReach::ConfiguredElsewhere
);
}
#[test]
fn nothing_putting_it_on_path_is_still_a_real_finding() {
let dir = Path::new("/Users/x/Library/Application Support/swapdex/bin");
assert_eq!(
shim_reach(false, Some("export EDITOR=vim\n"), dir),
ShimReach::Missing
);
assert_eq!(shim_reach(false, None, dir), ShimReach::Missing);
}
#[test]
fn a_profile_that_set_up_some_other_store_excuses_nothing() {
let mine = Path::new("/tmp/store-a/bin");
let theirs = "# added by swapdex\nexport PATH=\"/tmp/store-b/bin:$PATH\"\n";
assert_eq!(shim_reach(false, Some(theirs), mine), ShimReach::Missing);
}
#[test]
fn a_shim_that_works_here_needs_no_explaining() {
let dir = Path::new("/tmp/bin");
assert_eq!(shim_reach(true, None, dir), ShimReach::Active);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn an_existing_path_line_is_recognised_however_it_is_spelled() {
let root = tempfile::tempdir().unwrap();
let home = root.path();
let shim_dir = home.join("Library/Application Support/swapdex/bin");
let full = shim_dir.display().to_string();
for spelling in [
format!("export PATH=\"{full}:$PATH\""),
"export PATH=\"$HOME/Library/Application Support/swapdex/bin:$PATH\"".to_string(),
"export PATH=\"~/Library/Application Support/swapdex/bin:$PATH\"".to_string(),
] {
assert!(
profile_already_adds_at_home(
&format!("# something\n{spelling}\n"),
&shim_dir,
Some(home),
),
"not recognised: {spelling}"
);
}
assert!(!profile_already_adds_at_home(
"export PATH=\"/usr/local/bin:$PATH\"\n",
&shim_dir,
Some(home),
));
assert!(!profile_already_adds_at_home(
&format!("# export PATH=\"{full}:$PATH\"\n"),
&shim_dir,
Some(home),
));
assert!(!profile_already_adds_at_home(
&format!("echo {full}\n"),
&shim_dir,
Some(home),
));
}
#[test]
fn the_shim_does_not_proxy_a_sign_in() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("sx_plain=no"),
"it decides whether this is a sign-in: {s}"
);
for command in ["auth:login", "auth:logout", "auth:status", "setup-token:"] {
assert!(s.contains(command), "recognised: {command}");
}
let guard = s.find("if [ \"$sx_plain\" = no ]").expect("the guard");
let export = s
.find("ANTHROPIC_BASE_URL=\"http://")
.expect("the proxy export");
assert!(
guard < export,
"the proxy address is only set when not signing in"
);
}
#[test]
fn the_codex_shim_routes_through_a_running_proxy() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("proxy --ensure --tool codex"),
"asks swapdex for a live codex proxy: {s}"
);
assert!(
s.contains("openai_base_url=\"http://127.0.0.1:$port/v1\""),
"routes the built-in provider without changing session identity: {s}"
);
assert!(
!s.contains("chatgpt_base_url=\"http:"),
"the shim points chatgpt_base_url at a plain-HTTP proxy: {s}"
);
assert!(!s.contains("set -- -c model_provider="));
assert!(
s.contains("if [ \"$sx_use_proxy\" = yes ]"),
"the overrides are conditional: {s}"
);
}
#[cfg(unix)]
fn run_codex_shim_with_usage(
with_ca_file: bool,
env: &[(&str, &str)],
input: &[&str],
) -> (Vec<String>, String) {
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
let root = tempfile::tempdir().unwrap();
let pointer = root.path().join("active-codex");
let real = root.path().join("real codex");
let swapdex = root.path().join("fake swapdex");
let shim = root.path().join("codex shim");
std::fs::write(&pointer, root.path().join("codex-home").to_str().unwrap()).unwrap();
if with_ca_file {
std::fs::create_dir_all(root.path().join("codex-tls")).unwrap();
std::fs::write(root.path().join("codex-tls/ca.pem"), "ca").unwrap();
}
std::fs::write(
&real,
"#!/bin/sh\nprintf 'CA=%s\\0' \"${CODEX_CA_CERTIFICATE:-}\"\nprintf '%s\\0' \"$@\"\n",
)
.unwrap();
std::fs::write(
&swapdex,
"#!/bin/sh\ncase \"$*\" in *--usage-port*) printf 18788 ;; proxy*) printf 8788 ;; esac\n",
)
.unwrap();
std::fs::write(&shim, codex_shim_script(&pointer, &real, &swapdex)).unwrap();
for path in [&real, &swapdex] {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut cmd = Command::new("sh");
cmd.arg(&shim)
.args(input)
.env_remove("CODEX_CA_CERTIFICATE")
.env_remove("SSL_CERT_FILE")
.env("CODEX_HOME", root.path().join("codex-home"))
.env("SWAPDEX_ROOT", root.path());
for (k, v) in env {
cmd.env(k, v);
}
let output = cmd.output().unwrap();
assert!(output.status.success(), "{input:?}: {:?}", output.stderr);
let mut parts: Vec<String> = output
.stdout
.split(|b| *b == 0)
.map(|b| String::from_utf8(b.to_vec()).unwrap())
.collect();
assert_eq!(parts.pop().as_deref(), Some(""), "missing final NUL");
let ca = parts.remove(0);
let ca = ca.strip_prefix("CA=").unwrap().to_string();
let expected_ca = root
.path()
.join("codex-tls/ca.pem")
.to_string_lossy()
.into_owned();
(parts, if ca == expected_ca { "OURS".into() } else { ca })
}
#[test]
fn the_codex_shim_routes_usage_reads_over_https_with_its_ca() {
let usage = "chatgpt_base_url=https://127.0.0.1:18788/backend-api/";
let model = "openai_base_url=http://127.0.0.1:8788/v1";
let (args, ca) = run_codex_shim_with_usage(true, &[], &["exec", "prompt"]);
assert_eq!(args, ["-c", model, "-c", usage, "exec", "prompt"]);
assert_eq!(ca, "OURS", "Codex was not told to trust the usage listener");
let (args, ca) = run_codex_shim_with_usage(true, &[], &["exec", "-c", "model=x", "prompt"]);
assert_eq!(
args,
["exec", "-c", model, "-c", usage, "-c", "model=x", "prompt"],
"the scoped branch lost the usage route"
);
assert_eq!(ca, "OURS");
}
#[test]
fn the_codex_shim_leaves_usage_alone_without_a_ca_or_over_the_users_own() {
let model = "openai_base_url=http://127.0.0.1:8788/v1";
let (args, ca) = run_codex_shim_with_usage(false, &[], &["exec", "prompt"]);
assert_eq!(args, ["-c", model, "exec", "prompt"], "routed without a CA");
assert_eq!(ca, "", "exported a CA that does not exist");
for var in ["CODEX_CA_CERTIFICATE", "SSL_CERT_FILE"] {
let (args, ca) =
run_codex_shim_with_usage(true, &[(var, "/etc/corp-ca.pem")], &["exec", "prompt"]);
assert_eq!(
args,
["-c", model, "exec", "prompt"],
"routed usage over the user's own {var}"
);
assert_ne!(ca, "OURS", "replaced the user's {var}");
}
}
#[test]
fn codex_proxy_override_shares_the_last_real_config_scope() {
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
let root = tempfile::tempdir().unwrap();
let pointer = root.path().join("active-codex");
let real = root.path().join("real codex");
let swapdex = root.path().join("fake swapdex");
let shim = root.path().join("codex shim");
std::fs::write(&pointer, root.path().join("codex-home").to_str().unwrap()).unwrap();
std::fs::write(&real, "#!/bin/sh\nprintf '%s\\0' \"$@\"\n").unwrap();
std::fs::write(
&swapdex,
"#!/bin/sh\ncase \"$1\" in proxy) printf 8788 ;; esac\n",
)
.unwrap();
std::fs::write(&shim, codex_shim_script(&pointer, &real, &swapdex)).unwrap();
for path in [&real, &swapdex] {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let base = "openai_base_url=http://127.0.0.1:8788/v1";
for (input, expected) in [
(vec!["exec", "prompt"], vec!["-c", base, "exec", "prompt"]),
(
vec!["exec", "-c", "model_reasoning_effort=low", "prompt"],
vec![
"exec",
"-c",
base,
"-c",
"model_reasoning_effort=low",
"prompt",
],
),
(
vec!["exec", "--config", "model_reasoning_effort=low", "prompt"],
vec![
"exec",
"-c",
base,
"--config",
"model_reasoning_effort=low",
"prompt",
],
),
(
vec!["-c", "model_reasoning_effort=low", "exec", "prompt"],
vec![
"-c",
base,
"-c",
"model_reasoning_effort=low",
"exec",
"prompt",
],
),
(
vec!["-c", "model=first", "exec", "-c", "model=second", "prompt"],
vec![
"-c",
"model=first",
"exec",
"-c",
base,
"-c",
"model=second",
"prompt",
],
),
(
vec![
"exec",
"resume",
"--last",
"-c",
"model_reasoning_effort=low",
"prompt",
],
vec![
"exec",
"resume",
"--last",
"-c",
base,
"-c",
"model_reasoning_effort=low",
"prompt",
],
),
(
vec!["exec", "-c", "model_reasoning_effort = \"low\"", "prompt"],
vec![
"exec",
"-c",
base,
"-c",
"model_reasoning_effort = \"low\"",
"prompt",
],
),
(
vec!["exec", "-cmodel_reasoning_effort=low", "prompt"],
vec!["exec", "-c", base, "-cmodel_reasoning_effort=low", "prompt"],
),
(
vec!["exec", "--config=model_reasoning_effort=low", "prompt"],
vec![
"exec",
"-c",
base,
"--config=model_reasoning_effort=low",
"prompt",
],
),
(
vec!["exec", "-C", "-c", "-c", "model=second", "--", "-c"],
vec![
"exec",
"-C",
"-c",
"-c",
base,
"-c",
"model=second",
"--",
"-c",
],
),
(
vec!["exec", "--", "-c", "prompt"],
vec!["-c", base, "exec", "--", "-c", "prompt"],
),
(
vec!["exec", "-c", "model_provider=fixture", "prompt"],
vec!["exec", "-c", "model_provider=fixture", "prompt"],
),
] {
let output = Command::new("sh")
.arg(&shim)
.args(&input)
.env("CODEX_HOME", root.path().join("codex-home"))
.env("SWAPDEX_ROOT", root.path())
.output()
.unwrap();
assert!(output.status.success(), "{input:?}: {:?}", output.stderr);
let mut parts: Vec<_> = output.stdout.split(|byte| *byte == 0).collect();
assert_eq!(parts.pop(), Some(&b""[..]), "missing final NUL");
let actual: Vec<_> = parts
.into_iter()
.map(|bytes| String::from_utf8(bytes.to_vec()).unwrap())
.collect();
assert_eq!(actual, expected, "{input:?}");
}
}
#[test]
fn the_codex_shim_repairs_the_resolved_home_before_routing() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
let home = s.find("CODEX_HOME=").unwrap();
let repair = s.find("repair-codex-sessions --quiet").unwrap();
let proxy = s.find("proxy --ensure").unwrap();
assert!(home < repair && repair < proxy);
}
#[test]
fn the_codex_shim_points_codex_home_at_the_default_slot() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
assert!(s.starts_with("#!/bin/sh"));
assert!(
s.contains("/store/active-codex"),
"reads codex's own pointer"
);
assert!(s.contains("/usr/bin/codex"), "execs the real codex");
assert!(s.contains("CODEX_HOME="), "sets the slot env");
assert!(
s.contains("if [ -z \"$CODEX_HOME\" ]"),
"an explicit CODEX_HOME is a decision already made"
);
assert!(s.contains("exec "), "replaces the process");
assert!(!s.contains("CLAUDE_CONFIG_DIR"));
assert!(!s.contains("ANTHROPIC_BASE_URL"));
}
#[test]
fn script_references_pointer_real_claude_and_config_dir() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(s.starts_with("#!/bin/sh"));
assert!(s.contains("/store/active-claude"), "reads the pointer");
assert!(s.contains("/usr/bin/claude"), "execs the real claude");
assert!(s.contains("CLAUDE_CONFIG_DIR="), "sets the slot env");
assert!(s.contains("exec "), "replaces the process");
}
#[test]
fn an_explicit_config_dir_wins_over_the_default_pointer() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("if [ -z \"$CLAUDE_CONFIG_DIR\" ]"),
"the pointer only fills in when nothing chose a dir: {s}"
);
assert!(s.contains("/store/active-claude"), "{s}");
assert!(s.contains("CLAUDE_CONFIG_DIR="), "{s}");
}
#[test]
fn script_gets_its_proxy_from_swapdex_and_checks_the_result() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("'/bin/swapdex' proxy --ensure"),
"asks swapdex by absolute path, so the user starts nothing: {s}"
);
assert!(
s.contains("ANTHROPIC_BASE_URL"),
"points claude at the proxy"
);
assert!(
s.contains("http://127.0.0.1:$port"),
"loopback only, port from swapdex"
);
assert!(
s.contains("2>/dev/null")
&& s.contains("sx_proxy_status=$?")
&& s.contains("if [ \"$sx_use_proxy\" = yes ]"),
"the proxy status and validated port decide whether Claude is routed: {s}"
);
}
#[test]
fn script_quotes_paths_with_spaces() {
let s = shim_script(
Path::new("/a b/active-claude"),
Path::new("/c d/claude"),
Path::new("/e f/swapdex"),
);
assert!(s.contains("'/a b/active-claude'"), "pointer is quoted");
assert!(s.contains("'/e f/swapdex'"), "swapdex path is quoted");
assert!(s.contains("'/c d/claude'"), "real claude is quoted");
}
#[test]
fn recognizes_our_own_shim_by_marker() {
let dir = tempfile::tempdir().unwrap();
let shim = dir.path().join("claude");
std::fs::write(
&shim,
shim_script(Path::new("/p"), Path::new("/real"), Path::new("/sx")),
)
.unwrap();
assert!(is_our_shim(&shim), "our shim is recognized by its marker");
let real = dir.path().join("real-claude");
std::fs::write(&real, "#!/bin/sh\nexec node /opt/claude \"$@\"\n").unwrap();
assert!(!is_our_shim(&real), "a real claude is not flagged");
}
}
pub fn real_path_in(text: &str) -> Option<PathBuf> {
let line = text
.lines()
.rev()
.find(|l| l.trim_start().starts_with("exec "))?;
let token = line
.trim_start()
.strip_prefix("exec ")?
.strip_suffix(" \"$@\"")?
.trim();
let inner = token.strip_prefix('\'')?.strip_suffix('\'')?;
let path = inner.replace("'\\''", "'");
(!path.is_empty()).then(|| PathBuf::from(path))
}
pub fn stale_shim(paths: &Paths, tool: &str) -> Option<(PathBuf, String)> {
let shim = shim_path_for(paths, tool);
let text = std::fs::read_to_string(&shim).ok()?;
let called = swapdex_path_in(&text)?;
let me = std::env::current_exe().ok()?;
let same = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
if same(&called) != same(&me) {
return None;
}
let real = real_path_in(&text)?;
let expected = match tool {
"claude-code" => shim_script(&paths.store_dir().join("active-claude"), &real, &called),
"codex" => codex_shim_script(&paths.store_dir().join("active-codex"), &real, &called),
_ => return None,
};
(expected != text).then_some((shim, expected))
}
pub fn refresh_if_stale(paths: &Paths, tool: &str) -> Result<bool> {
let Some((shim, expected)) = stale_shim(paths, tool) else {
return Ok(false);
};
let tmp = shim.with_extension(format!("swapdex-{}", std::process::id()));
std::fs::write(&tmp, expected).context("write refreshed shim")?;
make_executable(&tmp)?;
std::fs::rename(&tmp, &shim).context("replace shim")?;
Ok(true)
}
pub fn swapdex_path_in(text: &str) -> Option<PathBuf> {
let at = text.find(" proxy --ensure")?;
let start = text[..at].rfind("$(")? + 2;
let token = text[start..at].trim();
let inner = token.strip_prefix('\'')?.strip_suffix('\'')?;
let path = inner.replace("'\\''", "'");
(!path.is_empty()).then(|| PathBuf::from(path))
}
#[cfg(test)]
mod embedded_path_tests {
use super::*;
#[test]
fn the_wrapped_binary_is_read_back_exactly() {
for real in ["/usr/bin/codex", "/opt/it's here/codex", "/a b/claude"] {
let real = Path::new(real);
let codex =
codex_shim_script(Path::new("/s/active-codex"), real, Path::new("/x/swapdex"));
assert_eq!(
real_path_in(&codex).as_deref(),
Some(real),
"codex: {real:?}"
);
let claude = shim_script(Path::new("/s/active-claude"), real, Path::new("/x/swapdex"));
assert_eq!(
real_path_in(&claude).as_deref(),
Some(real),
"claude: {real:?}"
);
}
}
use std::path::Path;
#[test]
fn the_path_comes_back_out_of_a_shim_we_wrote() {
for shim in [
shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/opt/homebrew/bin/swapdex"),
),
codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/opt/homebrew/bin/swapdex"),
),
] {
assert_eq!(
swapdex_path_in(&shim).as_deref(),
Some(Path::new("/opt/homebrew/bin/swapdex"))
);
}
}
#[test]
fn a_quoted_path_survives_the_round_trip() {
let odd = Path::new("/Users/o'brien/.local/bin/swapdex");
let shim = codex_shim_script(Path::new("/p"), Path::new("/usr/bin/codex"), odd);
assert_eq!(swapdex_path_in(&shim).as_deref(), Some(odd));
}
#[test]
fn something_that_is_not_our_shim_yields_nothing() {
assert_eq!(
swapdex_path_in("#!/bin/sh\nexec /usr/bin/claude \"$@\"\n"),
None
);
}
}
pub fn swapdex_copies_on(path_var: &str) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
for dir in path_var.split(':').filter(|d| !d.is_empty()) {
let cand = Path::new(dir).join("swapdex");
if !cand.is_file() {
continue;
}
let real = std::fs::canonicalize(&cand).unwrap_or(cand);
if !out.contains(&real) {
out.push(real);
}
}
out
}
#[cfg(test)]
mod copies_tests {
use super::*;
#[test]
fn two_entries_for_one_file_are_one_install() {
let root = tempfile::tempdir().unwrap();
let real = root.path().join("cellar");
std::fs::create_dir_all(&real).unwrap();
std::fs::write(real.join("swapdex"), b"#!/bin/sh\n").unwrap();
let linked = root.path().join("bin");
std::fs::create_dir_all(&linked).unwrap();
std::os::unix::fs::symlink(real.join("swapdex"), linked.join("swapdex")).unwrap();
let path = format!("{}:{}", linked.display(), real.display());
assert_eq!(
swapdex_copies_on(&path).len(),
1,
"a symlink is not a second install"
);
}
#[test]
fn two_real_files_are_two_installs_in_path_order() {
let root = tempfile::tempdir().unwrap();
let (a, b) = (root.path().join("npm"), root.path().join("brew"));
for d in [&a, &b] {
std::fs::create_dir_all(d).unwrap();
std::fs::write(d.join("swapdex"), b"#!/bin/sh\n").unwrap();
}
let found = swapdex_copies_on(&format!("{}:{}", a.display(), b.display()));
assert_eq!(found.len(), 2, "both are real, and one shadows the other");
let a_real = std::fs::canonicalize(&a).unwrap();
assert!(
found[0].starts_with(&a_real),
"the one that wins comes first"
);
}
#[test]
fn nothing_installed_is_not_a_problem() {
assert!(swapdex_copies_on("/nonexistent-a:/nonexistent-b").is_empty());
}
}
#[cfg(test)]
fn shim_marker_line() -> String {
format!("#!/bin/sh\n# {SHIM_MARKER_CODEX}\n")
}
pub fn real_tool(paths: &Paths, tool: &str) -> Option<PathBuf> {
find_real(&shim_bin_dir(paths), crate::commands::tool_binary(tool))
}
#[cfg(test)]
mod real_tool_tests {
use super::*;
#[test]
fn the_shim_dir_is_stepped_over() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
let dir = shim_bin_dir(&paths);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("codex"), shim_marker_line()).unwrap();
if let Some(found) = real_tool(&paths, "codex") {
assert!(
!found.starts_with(&dir),
"resolved {} inside the shim dir",
found.display()
);
}
}
}
#[cfg(test)]
mod shadow_tests {
use super::*;
#[test]
fn a_shim_that_loses_the_path_is_reported_as_shadowed() {
let shim = std::path::Path::new("/home/u/.local/share/swapdex/bin");
assert_eq!(
path_verdict(shim, &["/home/u/.local/share/swapdex/bin", "/usr/bin"]),
PathVerdict::Wins
);
assert_eq!(
path_verdict_with(
shim,
&["/home/u/.local/bin", "/home/u/.local/share/swapdex/bin"],
&|d| d == "/home/u/.local/bin"
),
PathVerdict::Shadowed("/home/u/.local/bin".into())
);
assert_eq!(
path_verdict(shim, &["/usr/bin", "/bin"]),
PathVerdict::Absent
);
}
#[test]
fn an_earlier_directory_without_the_tool_does_not_shadow() {
let shim = std::path::Path::new("/shim/bin");
assert_eq!(
path_verdict_with(shim, &["/empty", "/shim/bin"], &|d| d != "/empty"),
PathVerdict::Wins
);
}
}
#[cfg(test)]
mod pin_base_url_tests {
use super::*;
#[test]
fn a_base_url_is_pinned_only_when_a_service_keeps_the_proxy_alive() {
assert_eq!(pin_verdict(true, 8787), PinVerdict::Pin(8787));
assert_eq!(pin_verdict(false, 8787), PinVerdict::RefuseNoService);
}
#[test]
fn pinning_preserves_every_other_setting() {
let before = serde_json::json!({
"model": "opus",
"env": {"FOO": "1"},
"permissions": {"allow": ["Bash"]}
});
let after = with_base_url(&before, 8787);
assert_eq!(after["model"], "opus");
assert_eq!(after["permissions"]["allow"][0], "Bash");
assert_eq!(after["env"]["FOO"], "1");
assert_eq!(after["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8787");
}
#[test]
fn pinning_works_on_settings_that_have_no_env_yet() {
let after = with_base_url(&serde_json::json!({"model": "opus"}), 9001);
assert_eq!(after["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:9001");
assert_eq!(after["model"], "opus");
}
}
#[cfg(test)]
mod pinned_port_tests {
use super::*;
#[test]
fn the_pinned_port_can_be_read_back_out_of_settings() {
let pinned = serde_json::json!({
"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787"},
"model": "opus"
});
assert_eq!(pinned_port(&pinned), Some(8787));
let other_port = serde_json::json!({
"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:9001"}
});
assert_eq!(pinned_port(&other_port), Some(9001));
assert_eq!(pinned_port(&serde_json::json!({"model": "opus"})), None);
assert_eq!(
pinned_port(
&serde_json::json!({"env": {"ANTHROPIC_BASE_URL": "https://api.anthropic.com"}})
),
None
);
}
}
#[cfg(test)]
mod unpin_on_exit_tests {
use super::*;
#[test]
fn only_this_proxy_pin_is_withdrawn() {
assert!(pin_is_ours(Some(8787), 8787), "our own port");
assert!(!pin_is_ours(Some(9001), 8787), "another proxy port");
assert!(
!pin_is_ours(None, 8787),
"nothing pinned - nothing to withdraw"
);
}
}
#[cfg(test)]
mod withdraw_pin_file_tests {
use super::*;
#[test]
fn it_removes_only_our_pin_and_keeps_the_rest() {
let td = tempfile::tempdir().unwrap();
let paths = crate::paths::Paths::rooted(td.path());
std::fs::create_dir_all(paths.claude_dir()).unwrap();
let f = paths.claude_dir().join("settings.json");
let write = |v: serde_json::Value| std::fs::write(&f, v.to_string()).unwrap();
let read = || -> serde_json::Value {
serde_json::from_str(&std::fs::read_to_string(&f).unwrap()).unwrap()
};
write(serde_json::json!({
"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787", "KEEP": "1"},
"model": "opus", "hooks": {}
}));
assert!(withdraw_pin(&paths, 8787));
let v = read();
assert!(v["env"]["ANTHROPIC_BASE_URL"].is_null(), "the pin is gone");
assert_eq!(v["env"]["KEEP"], "1", "other env survives");
assert_eq!(v["model"], "opus", "other settings survive");
write(serde_json::json!({"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:9001"}}));
assert!(!withdraw_pin(&paths, 8787), "not ours to withdraw");
assert_eq!(read()["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:9001");
}
}
#[cfg(test)]
mod profile_read_tests {
use super::*;
#[test]
fn a_missing_profile_reads_as_empty() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("no-such-rc");
assert_eq!(read_profile_for_edit(&p).unwrap(), "");
}
#[test]
fn an_unreadable_profile_is_an_error_not_an_empty_one() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("rc-is-a-dir");
std::fs::create_dir(&p).unwrap();
let err = read_profile_for_edit(&p).unwrap_err().to_string();
assert!(err.contains("refusing"), "unhelpful: {err}");
}
}
#[cfg(test)]
mod per_tool_path_tests {
use super::*;
#[test]
fn every_tool_gets_its_own_marker_and_shim() {
let d = tempfile::tempdir().unwrap();
let paths = crate::paths::Paths::rooted(d.path());
let tools = ["claude-code", "codex", "gemini", "antigravity"];
let markers: Vec<_> = tools.iter().map(|t| proxy_marker_for(&paths, t)).collect();
let shims: Vec<_> = tools.iter().map(|t| shim_path_for(&paths, t)).collect();
let serving: Vec<_> = tools
.iter()
.map(|t| crate::proxy::serving_file_for(&paths, t))
.collect();
for i in 0..tools.len() {
for j in (i + 1)..tools.len() {
assert_ne!(
markers[i], markers[j],
"{} and {} share a proxy marker",
tools[i], tools[j]
);
assert_ne!(
shims[i], shims[j],
"{} and {} share a shim path",
tools[i], tools[j]
);
assert_ne!(
serving[i], serving[j],
"{} and {} share a serving file",
tools[i], tools[j]
);
}
}
}
#[test]
fn the_established_names_do_not_move() {
let d = tempfile::tempdir().unwrap();
let paths = crate::paths::Paths::rooted(d.path());
assert!(proxy_marker_for(&paths, "claude-code").ends_with("proxy"));
assert!(proxy_marker_for(&paths, "codex").ends_with("proxy-codex"));
assert!(shim_path_for(&paths, "claude-code").ends_with("claude"));
assert!(shim_path_for(&paths, "codex").ends_with("codex"));
}
}