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\
exec {real} \"$@\"\n",
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_claude),
proxy_result = proxy_result,
)
}
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
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
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_skip=config ;;
--config=*) if sx_explicit_config "${{a#--config=}}"; then sx_plain=yes; fi ;;
-c?*) if sx_explicit_config "${{a#-c}}"; then sx_plain=yes; fi ;;
-p|--profile|--remote|--remote-auth-token-env|--local-provider) sx_plain=yes; sx_skip=value ;;
-p?*|--profile=*|--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"
case "$a" in login|logout|completion|mcp|mcp-server|debug|features|apply|help) 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}
if [ "$sx_use_proxy" = yes ]; then
set -- -c openai_base_url="http://127.0.0.1:$port/v1" "$@"
fi
fi
exec {real} "$@"
"#,
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_codex),
proxy_result = proxy_result,
)
}
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_claude() -> 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("claude");
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("set -- -c model_provider="));
assert!(
s.contains("if [ \"$sx_use_proxy\" = yes ]"),
"the overrides are conditional: {s}"
);
}
#[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 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::*;
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"));
}
}