use std::net::{Ipv4Addr, TcpListener};
use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::ui::{self, Tone};
const BEGIN: &str = "# >>> llmtrim >>>";
const END: &str = "# <<< llmtrim <<<";
const NO_PROXY: &str = "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,fd00::/8,*.local";
pub const DEFAULT_PORT: u16 = 43117;
fn first_free_port(start: u16, span: u16) -> Option<u16> {
(start..=start.saturating_add(span))
.find(|&p| TcpListener::bind((Ipv4Addr::LOCALHOST, p)).is_ok())
}
fn port_holder(port: u16) -> Option<(u32, String)> {
#[cfg(windows)]
{
let out = std::process::Command::new("netstat")
.args(["-ano"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let needle = format!(":{port}");
let pid: u32 = text
.lines()
.filter(|l| l.contains("LISTENING") && l.contains(&needle))
.filter_map(|l| l.split_whitespace().last()?.parse().ok())
.next()?;
let out = std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
.output()
.ok()?;
let line = String::from_utf8_lossy(&out.stdout);
let name = line
.trim()
.trim_start_matches('"')
.split('"')
.next()?
.to_string();
(!name.is_empty()).then_some((pid, name))
}
#[cfg(unix)]
{
if let Ok(out) = std::process::Command::new("ss")
.args(["-ltnp", &format!("sport = :{port}")])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
if let Some(users) = text.split("users:((\"").nth(1) {
let name = users.split('"').next().unwrap_or_default().to_string();
if let Some(pid) = users.split("pid=").nth(1).and_then(|s| {
s.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse()
.ok()
}) {
return Some((pid, name));
}
}
}
let out = std::process::Command::new("lsof")
.args(["-nP", &format!("-iTCP:{port}"), "-sTCP:LISTEN", "-Fpc"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout);
let pid = text.lines().find(|l| l.starts_with('p'))?[1..]
.parse()
.ok()?;
let name = text.lines().find(|l| l.starts_with('c'))?[1..].to_string();
Some((pid, name))
}
}
fn kill_pid(pid: u32) {
#[cfg(windows)]
{
let _ = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output();
}
#[cfg(unix)]
{
let _ = std::process::Command::new("kill")
.args(["-9", &pid.to_string()])
.output();
}
}
#[derive(Debug, PartialEq, Eq)]
enum PortChoice {
Use(u16),
ScanFrom(u16),
}
fn choose_port(explicit: Option<u16>, running: Option<u16>, configured: Option<u16>) -> PortChoice {
if let Some(p) = explicit.or(running).or(configured) {
PortChoice::Use(p)
} else {
PortChoice::ScanFrom(DEFAULT_PORT)
}
}
pub fn resolve_port(explicit: Option<u16>, running: Option<u16>) -> Result<u16> {
match choose_port(explicit, running, configured_port()) {
PortChoice::Use(p) => Ok(p),
PortChoice::ScanFrom(start) => first_free_port(start, 64)
.with_context(|| format!("no free port in {start}..={}", start.saturating_add(64))),
}
}
pub(crate) fn parse_proxy_port(text: &str) -> Option<u16> {
let after = text.split("127.0.0.1:").nth(1)?;
let digits: String = after.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
}
pub fn configured_port() -> Option<u16> {
#[cfg(windows)]
{
user_env_key()
.ok()
.and_then(|env| env.get_value::<String, _>("HTTPS_PROXY").ok())
.and_then(|v| parse_proxy_port(&v))
}
#[cfg(not(windows))]
{
profile_target()
.and_then(|(p, _)| std::fs::read_to_string(p).ok())
.and_then(|t| parse_proxy_port(&t))
}
}
pub fn heal_managed_env() -> Result<Vec<PathBuf>> {
#[cfg(windows)]
{
let env = user_env_key()?;
if !has_proxy_in(&env) || env.get_value::<String, _>("NO_PROXY").is_ok() {
return Ok(vec![]);
}
let proxy: String = env.get_value("HTTPS_PROXY").context("read HTTPS_PROXY")?;
let ca: String = env.get_value("NODE_EXTRA_CA_CERTS").unwrap_or_default();
set_env_in(&env, &proxy, &ca)?; broadcast_env_change();
Ok(vec![PathBuf::from("HKCU\\Environment")])
}
#[cfg(not(windows))]
{
let Ok(home) = std::env::var("HOME") else {
return Ok(vec![]);
};
let Some(port) = configured_port() else {
return Ok(vec![]); };
let proxy = format!("http://127.0.0.1:{port}");
let ca = crate::serve::ca_cert_path()?.to_string_lossy().into_owned();
heal_profiles_in(std::path::Path::new(&home), &proxy, &ca)
}
}
#[cfg(not(windows))]
fn managed_block_needs_heal(s: &str) -> bool {
let (mut in_block, mut saw_begin, mut saw_end, mut bypass_in_block) =
(false, false, false, false);
for line in s.lines() {
match line.trim() {
BEGIN => {
in_block = true;
saw_begin = true;
}
END => {
if in_block {
saw_end = true;
}
in_block = false;
}
l if in_block && l.contains("NO_PROXY") => bypass_in_block = true,
_ => {}
}
}
saw_begin && saw_end && !bypass_in_block
}
#[cfg(not(windows))]
fn heal_profiles_in(base: &std::path::Path, proxy: &str, ca: &str) -> Result<Vec<PathBuf>> {
let block = env_block(proxy, ca, Syntax::Posix);
let mut healed = Vec::new();
for path in candidate_profiles(base) {
let existing = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => continue, };
if !managed_block_needs_heal(&existing) {
continue;
}
let mut base_content = strip_block(&existing);
if !base_content.is_empty() && !base_content.ends_with('\n') {
base_content.push('\n');
}
std::fs::write(&path, format!("{base_content}{block}"))
.with_context(|| format!("failed to write {}", path.display()))?;
healed.push(path);
}
Ok(healed)
}
pub fn run(requested: Option<u16>) -> Result<()> {
let color = ui::color_stdout();
let running = crate::daemon::running();
let running_port = running.as_ref().map(|s| s.port);
let configured = configured_port();
let pinned = requested.or(running_port).or(configured);
let mut port = match choose_port(requested, running_port, configured) {
PortChoice::Use(p) => p,
PortChoice::ScanFrom(start) => first_free_port(start, 64)
.with_context(|| format!("no free port in {start}..={}", start.saturating_add(64)))?,
};
if pinned.is_none() && port != DEFAULT_PORT {
match port_holder(DEFAULT_PORT) {
Some((pid, name)) if name.to_lowercase().contains("llmtrim") => {
kill_pid(pid);
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(100));
if std::net::TcpListener::bind(("127.0.0.1", DEFAULT_PORT)).is_ok() {
port = DEFAULT_PORT;
break;
}
}
if port == DEFAULT_PORT {
println!(
"{}",
ui::note(
color,
&format!(
"Stopped an orphaned llmtrim daemon (pid {pid}) holding port {DEFAULT_PORT}."
)
)
);
} else {
println!(
"{}",
ui::note(
color,
&format!(
"Port {DEFAULT_PORT} held by an old llmtrim (pid {pid}) that wouldn't die — using {port}."
)
)
);
}
}
Some((pid, name)) => {
println!(
"{}",
ui::note(
color,
&format!("Port {DEFAULT_PORT} busy ({name}, pid {pid}) — using {port}.")
)
);
}
None => {
println!(
"{}",
ui::note(color, &format!("Port {DEFAULT_PORT} busy — using {port}."))
);
}
}
}
let mut rows: Vec<(&str, String, String)> = Vec::new();
crate::serve::ensure_ca()?;
let ca = crate::serve::ca_cert_path()?.to_string_lossy().to_string();
let proxy = format!("http://127.0.0.1:{port}");
rows.push((ui::OK, "Local CA".into(), ca.clone()));
#[cfg(windows)]
{
set_user_env(&proxy, &ca)?;
rows.push((
ui::OK,
"Environment".into(),
"HKCU\\Environment — HTTPS_PROXY + CA trust".into(),
));
if let Ok(paths) = remove_profile_block() {
for path in paths {
rows.push((
ui::OK,
"Profile".into(),
format!("legacy env block removed from {}", path.display()),
));
}
}
broadcast_env_change();
}
#[cfg(not(windows))]
let manual_env = {
let paths = write_profile_block(&proxy, &ca)?;
if paths.is_empty() {
rows.push((
ui::NOTE,
"Profile".into(),
"no shell profile found — set the env yourself (below)".into(),
));
true
} else {
let names = paths
.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect::<Vec<_>>()
.join(", ");
rows.push((
ui::OK,
"Profile".into(),
format!("{names} — HTTPS_PROXY + CA trust"),
));
false
}
};
match crate::autostart::configure(true, port) {
Ok(()) => rows.push((ui::OK, "Autostart".into(), "runs at login".into())),
Err(e) => rows.push((ui::WARN, "Autostart".into(), format!("not enabled: {e}"))),
}
let daemon_ok = match &running {
Some(state) if state.port == port => {
rows.push((
ui::OK,
"Interceptor".into(),
format!("already running · pid {} · port {port}", state.pid),
));
true
}
_ => {
let _ = crate::daemon::stop(); match crate::daemon::spawn_detached(port) {
Ok(pid) => {
rows.push((
ui::OK,
"Interceptor".into(),
format!("running · pid {pid} · port {port}"),
));
true
}
Err(e) => {
rows.push((ui::WARN, "Interceptor".into(), format!("not started: {e}")));
false
}
}
}
};
print!(
"{}",
ui::panel(color, "llmtrim setup", &ui::kv_rows(color, &rows))
);
#[cfg(not(windows))]
if manual_env {
println!();
println!("Export these in your shell yourself:");
println!(" export HTTPS_PROXY={proxy}");
println!(" export NO_PROXY={NO_PROXY}");
println!(" export NODE_EXTRA_CA_CERTS={ca}");
}
let check = if cfg!(windows) {
"echo $env:HTTPS_PROXY"
} else {
"echo $HTTPS_PROXY"
};
println!();
if daemon_ok {
println!(
"{}",
ui::paint(color, Tone::Bold, "Done — the interceptor is running.")
);
} else {
println!(
"{}",
ui::warn(
color,
"Setup finished, but the interceptor is not running — see above."
)
);
}
println!(
"Only programs started after this pick up the proxy env; already-running\n\
tools (your editor, Claude Code, open terminals) keep their old environment\n\
until relaunched. To route one through llmtrim:"
);
println!();
let new_shell = if cfg!(windows) {
"open a new terminal (any shell — the env is set for your whole user)"
} else {
"open a new terminal (or re-source your shell profile)"
};
println!(" 1. {new_shell}");
println!(" 2. verify it took: {check} → {proxy}");
println!(" 3. launch your tool from that shell");
println!();
println!(
" {} llmtrim status",
ui::paint(color, Tone::Dim, "watch savings")
);
#[cfg(windows)]
println!(
"{}",
ui::note(
color,
&format!(
"For GUI apps that pin their own trust store, trust the CA system-wide: \
certutil -addstore -user Root \"{ca}\" — or see llmtrim ca."
)
)
);
#[cfg(not(windows))]
println!(
"{}",
ui::note(
color,
"GUI apps that ignore the shell env need the CA trusted system-wide — see llmtrim ca."
)
);
if caveman_installed() {
println!();
println!(
"{}",
ui::warn(
color,
"caveman detected. It shapes model output on every request. llmtrim's auto \
mode already shapes output where it pays (code, long context, plain prose) \
and skips it on tool-calling agent traffic on purpose: tool-call replies are \
already short, so terse shaping there saves no tokens (our bench measured \
quality neutral). So caveman adds nothing llmtrim does not already do where \
it helps. To remove it:"
)
);
#[cfg(not(windows))]
println!(
" bash <(curl -s https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/hooks/uninstall.sh)"
);
#[cfg(windows)]
println!(
" irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/hooks/uninstall.ps1 | iex"
);
println!(" claude plugin disable caveman # if installed as a Claude Code plugin");
println!(" npx skills remove caveman # Cursor, Windsurf, Cline, Copilot, …");
println!(" gemini extensions uninstall caveman # Gemini CLI");
}
Ok(())
}
fn caveman_installed() -> bool {
let claude_dir = std::env::var("CLAUDE_CONFIG_DIR")
.map(PathBuf::from)
.ok()
.or_else(|| {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(|h| PathBuf::from(h).join(".claude"))
});
let Some(dir) = claude_dir else {
return false;
};
caveman_installed_in(&dir)
}
fn caveman_installed_in(dir: &std::path::Path) -> bool {
if dir.join(".caveman-active").is_file()
|| dir.join("hooks").join("caveman-activate.js").is_file()
|| dir.join("hooks").join("caveman-config.js").is_file()
{
return true;
}
fn walk(p: &std::path::Path, depth: u8) -> bool {
if depth == 0 {
return false;
}
let Ok(rd) = std::fs::read_dir(p) else {
return false;
};
for e in rd.flatten() {
if e.file_name().to_string_lossy().contains("caveman") {
return true;
}
let path = e.path();
if path.is_dir() && walk(&path, depth - 1) {
return true;
}
}
false
}
walk(&dir.join("plugins"), 3)
}
pub fn uninstall(purge: bool, keep_binary: bool) -> Result<()> {
let color = ui::color_stdout();
let mut rows: Vec<(&str, String, String)> = Vec::new();
match crate::daemon::stop() {
Ok(Some(pid)) => rows.push((ui::OK, "Interceptor".into(), format!("stopped (pid {pid})"))),
Ok(None) => rows.push((
ui::NOTE,
"Interceptor".into(),
"no daemon was running".into(),
)),
Err(e) => rows.push((
ui::WARN,
"Interceptor".into(),
format!("could not stop: {e}"),
)),
}
match crate::autostart::configure(false, DEFAULT_PORT) {
Ok(()) => rows.push((ui::OK, "Autostart".into(), "disabled".into())),
Err(e) => rows.push((ui::WARN, "Autostart".into(), format!("not changed: {e}"))),
}
#[cfg(windows)]
{
match clear_user_env() {
Ok(true) => rows.push((
ui::OK,
"Environment".into(),
"interceptor env removed from HKCU\\Environment".into(),
)),
Ok(false) => rows.push((
ui::NOTE,
"Environment".into(),
"no interceptor env to remove".into(),
)),
Err(e) => rows.push((ui::WARN, "Environment".into(), format!("not cleaned: {e}"))),
}
if let Ok(paths) = remove_profile_block() {
for path in paths {
rows.push((
ui::OK,
"Profile".into(),
format!("legacy env block removed from {}", path.display()),
));
}
}
broadcast_env_change();
}
#[cfg(not(windows))]
match remove_profile_block() {
Ok(paths) if paths.is_empty() => {
rows.push((ui::NOTE, "Profile".into(), "no env block to remove".into()))
}
Ok(paths) => {
for path in paths {
rows.push((
ui::OK,
"Profile".into(),
format!("env block removed from {}", path.display()),
));
}
}
Err(e) => rows.push((ui::WARN, "Profile".into(), format!("not cleaned: {e}"))),
}
let home = crate::daemon::home_dir()?;
if home.exists() {
match std::fs::remove_dir_all(&home) {
Ok(()) => rows.push((
ui::OK,
"State".into(),
format!("removed {} (CA, key, daemon state)", home.display()),
)),
Err(e) => rows.push((
ui::WARN,
"State".into(),
format!("could not remove {}: {e}", home.display()),
)),
}
} else {
rows.push((
ui::NOTE,
"State".into(),
"no state directory to remove".into(),
));
}
#[cfg(windows)]
match untrust_ca_root() {
Ok(true) => rows.push((
ui::OK,
"Trust".into(),
"removed the CA from your user Root store".into(),
)),
Ok(false) => rows.push((
ui::NOTE,
"Trust".into(),
"CA was not trusted system-wide".into(),
)),
Err(e) => rows.push((ui::WARN, "Trust".into(), format!("not untrusted: {e}"))),
}
#[cfg(target_os = "macos")]
match untrust_ca_keychain() {
Ok(true) => rows.push((
ui::OK,
"Trust".into(),
"removed the CA from your login keychain".into(),
)),
Ok(false) => rows.push((
ui::NOTE,
"Trust".into(),
"CA was not in the login keychain".into(),
)),
Err(e) => rows.push((ui::WARN, "Trust".into(), format!("not untrusted: {e}"))),
}
match crate::tracking::db_path() {
Ok(db) if db.exists() && purge => {
std::fs::remove_file(&db).ok();
rows.push((ui::OK, "Ledger".into(), format!("removed {}", db.display())));
}
Ok(db) if db.exists() => {
rows.push((
ui::NOTE,
"Ledger".into(),
format!("kept {} (use --purge to remove)", db.display()),
));
}
_ => {}
}
let manager_cmd = match crate::update::channel() {
crate::update::Channel::Npm => Some("npm uninstall -g @llmtrim/cli"),
crate::update::Channel::Cargo => Some("cargo uninstall llmtrim"),
crate::update::Channel::Homebrew => Some("brew uninstall llmtrim"),
crate::update::Channel::Binary => None,
};
if keep_binary {
rows.push((ui::NOTE, "Binary".into(), "kept".into()));
} else if let Some(cmd) = manager_cmd {
rows.push((
ui::NOTE,
"Binary".into(),
format!("owned by your package manager — finish with: {cmd}"),
));
} else if let Ok(exe) = std::env::current_exe() {
#[cfg(unix)]
{
std::fs::remove_file(&exe).ok();
rows.push((
ui::OK,
"Binary".into(),
format!("removed {}", exe.display()),
));
}
#[cfg(windows)]
{
match remove_bin_dir_from_path() {
Ok(true) => rows.push((
ui::OK,
"PATH".into(),
"removed the llmtrim bin dir from your user PATH".into(),
)),
Ok(false) => {}
Err(e) => rows.push((ui::WARN, "PATH".into(), format!("not cleaned: {e}"))),
}
if let Some(dir) = installer_dir_of(&exe) {
schedule_dir_removal(&dir);
rows.push((
ui::OK,
"Binary".into(),
format!("scheduled removal of {} after exit", dir.display()),
));
} else {
schedule_file_removal(&exe);
rows.push((
ui::OK,
"Binary".into(),
format!("scheduled removal of {} after exit", exe.display()),
));
}
broadcast_env_change(); }
#[cfg(all(not(unix), not(windows)))]
{
rows.push((
ui::NOTE,
"Binary".into(),
format!("remove manually: {}", exe.display()),
));
}
}
print!(
"{}",
ui::panel(color, "llmtrim uninstall", &ui::kv_rows(color, &rows))
);
println!();
println!(
"{}",
ui::paint(
color,
Tone::Bold,
"Done. Your current shell still has HTTPS_PROXY, HTTP_PROXY, NO_PROXY, and \
NODE_EXTRA_CA_CERTS exported. Open a new shell to clear them, or run: \
unset HTTPS_PROXY HTTP_PROXY NO_PROXY no_proxy NODE_EXTRA_CA_CERTS"
)
);
#[cfg(windows)]
println!(
"{}",
ui::note(
color,
"Apps already running (browsers, Plex/media servers, anything networked) keep the \
old proxy until you restart them — reboot to clear them all at once."
)
);
#[cfg(not(windows))]
println!(
"{}",
ui::note(
color,
"If you trusted the CA system-wide manually, remove it from your OS trust store."
)
);
Ok(())
}
#[cfg(not(windows))]
fn candidate_profiles(base: &std::path::Path) -> Vec<PathBuf> {
[
".zshrc",
".zprofile",
".bashrc",
".bash_profile",
".profile",
]
.iter()
.map(|f| base.join(f))
.collect()
}
#[cfg_attr(windows, allow(dead_code))]
fn remove_profile_block_in(base: &std::path::Path) -> Result<Vec<PathBuf>> {
#[cfg(windows)]
{
let _ = base;
Ok(vec![])
}
#[cfg(not(windows))]
{
let mut cleaned = Vec::new();
for path in candidate_profiles(base) {
let existing = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => continue, };
if !existing.contains(BEGIN) {
continue;
}
std::fs::write(&path, strip_block(&existing))
.with_context(|| format!("failed to write {}", path.display()))?;
cleaned.push(path);
}
Ok(cleaned)
}
}
fn remove_profile_block() -> Result<Vec<PathBuf>> {
#[cfg(windows)]
{
let Some((path, _)) = profile_target() else {
return Ok(vec![]);
};
let existing = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return Ok(vec![]),
};
if !existing.contains(BEGIN) {
return Ok(vec![]);
}
std::fs::write(&path, strip_block(&existing))
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(vec![path])
}
#[cfg(not(windows))]
{
let Ok(home) = std::env::var("HOME") else {
return Ok(vec![]);
};
remove_profile_block_in(std::path::Path::new(&home))
}
}
pub fn profile_has_block() -> bool {
#[cfg(windows)]
{
user_env_has_proxy()
}
#[cfg(not(windows))]
{
let Ok(home) = std::env::var("HOME") else {
return false;
};
profile_has_block_in(std::path::Path::new(&home))
}
}
#[cfg(not(windows))]
fn profile_has_block_in(base: &std::path::Path) -> bool {
candidate_profiles(base).into_iter().any(|path| {
std::fs::read_to_string(&path)
.map(|t| t.contains(BEGIN))
.unwrap_or(false)
})
}
#[cfg(windows)]
const ENV_KEYS: [&str; 4] = [
"HTTPS_PROXY",
"HTTP_PROXY",
"NO_PROXY",
"NODE_EXTRA_CA_CERTS",
];
#[cfg(windows)]
fn user_env_key() -> Result<winreg::RegKey> {
use winreg::RegKey;
use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (env, _) = hkcu
.create_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
.context("failed to open HKCU\\Environment")?;
Ok(env)
}
#[cfg(windows)]
fn set_user_env(proxy: &str, ca: &str) -> Result<()> {
set_env_in(&user_env_key()?, proxy, ca)
}
#[cfg(windows)]
fn clear_user_env() -> Result<bool> {
clear_env_in(&user_env_key()?)
}
#[cfg(windows)]
fn user_env_has_proxy() -> bool {
user_env_key().is_ok_and(|env| has_proxy_in(&env))
}
#[cfg(windows)]
fn broadcast_env_change() {
const PS: &str = "\
$sig = '[DllImport(\"user32.dll\", SetLastError=true, CharSet=CharSet.Auto)]\
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, \
string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);';\
$t = Add-Type -MemberDefinition $sig -Name NativeMethods -Namespace Win32 -PassThru;\
$r = [UIntPtr]::Zero;\
[void]$t::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 0x2, 5000, [ref]$r)";
let _ = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", PS])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
#[cfg(windows)]
fn set_env_in(env: &winreg::RegKey, proxy: &str, ca: &str) -> Result<()> {
env.set_value("HTTPS_PROXY", &proxy)
.context("failed to set HTTPS_PROXY")?;
env.set_value("HTTP_PROXY", &proxy)
.context("failed to set HTTP_PROXY")?;
env.set_value("NO_PROXY", &NO_PROXY)
.context("failed to set NO_PROXY")?;
env.set_value("NODE_EXTRA_CA_CERTS", &ca)
.context("failed to set NODE_EXTRA_CA_CERTS")?;
Ok(())
}
#[cfg(windows)]
fn clear_env_in(env: &winreg::RegKey) -> Result<bool> {
let mut removed = false;
for key in ENV_KEYS {
match env.delete_value(key) {
Ok(()) => removed = true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e).with_context(|| format!("failed to delete {key}")),
}
}
Ok(removed)
}
#[cfg(windows)]
fn has_proxy_in(env: &winreg::RegKey) -> bool {
env.get_value::<String, _>("HTTPS_PROXY")
.is_ok_and(|v| v.contains("127.0.0.1"))
}
#[cfg(any(windows, target_os = "macos"))]
const CA_SUBJECT_CN: &str = "llmtrim local CA";
#[cfg(windows)]
fn untrust_ca_root() -> Result<bool> {
let out = std::process::Command::new("certutil")
.args(["-user", "-delstore", "Root", CA_SUBJECT_CN])
.output()
.context("failed to run certutil -delstore")?;
Ok(out.status.success())
}
#[cfg(target_os = "macos")]
fn untrust_ca_keychain() -> Result<bool> {
let out = std::process::Command::new("security")
.args(["delete-certificate", "-c", CA_SUBJECT_CN])
.output()
.context("failed to run security delete-certificate")?;
Ok(out.status.success())
}
#[cfg(windows)]
fn installer_bin_dir() -> Option<PathBuf> {
std::env::var_os("LOCALAPPDATA").map(|l| PathBuf::from(l).join("llmtrim").join("bin"))
}
#[cfg(windows)]
fn installer_dir_of(exe: &std::path::Path) -> Option<PathBuf> {
let root = PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("llmtrim");
exe.starts_with(&root).then_some(root)
}
#[cfg(windows)]
fn encode_utf16_nul(s: &str) -> Vec<u8> {
s.encode_utf16()
.chain(std::iter::once(0))
.flat_map(u16::to_le_bytes)
.collect()
}
#[cfg(windows)]
fn remove_bin_dir_from_path() -> Result<bool> {
use winreg::enums::RegType;
use winreg::types::FromRegValue;
let Some(bin) = installer_bin_dir() else {
return Ok(false);
};
let env = user_env_key()?;
let Ok(raw) = env.get_raw_value("Path") else {
return Ok(false); };
if raw.vtype != RegType::REG_SZ && raw.vtype != RegType::REG_EXPAND_SZ {
return Ok(false); }
let current = String::from_reg_value(&raw).unwrap_or_default();
let stripped = strip_path_entry(¤t, &bin.to_string_lossy());
if stripped == current {
return Ok(false);
}
let new_raw = winreg::RegValue {
bytes: encode_utf16_nul(&stripped).into(),
vtype: raw.vtype,
};
env.set_raw_value("Path", &new_raw)
.context("failed to update the user PATH")?;
Ok(true)
}
#[cfg(windows)]
fn schedule_dir_removal(dir: &std::path::Path) {
schedule_removal_script(&format!(
"rmdir /s /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit",
dir.display()
));
}
#[cfg(windows)]
fn schedule_file_removal(file: &std::path::Path) {
schedule_removal_script(&format!(
"del /f /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit",
file.display()
));
}
#[cfg(windows)]
fn schedule_removal_script(attempt: &str) {
use std::os::windows::process::CommandExt;
use std::process::Stdio;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let script = format!("for /l %i in (1,1,30) do (ping 127.0.0.1 -n 3 >nul & {attempt})");
let _ = std::process::Command::new("cmd")
.args(["/c", &script])
.creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
#[cfg_attr(not(windows), allow(dead_code))]
fn strip_path_entry(path: &str, dir: &str) -> String {
let norm = |s: &str| s.trim().trim_end_matches(['\\', '/']).to_ascii_lowercase();
let target = norm(dir);
path.split(';')
.filter(|seg| norm(seg) != target)
.collect::<Vec<_>>()
.join(";")
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
enum Syntax {
Posix,
PowerShell,
}
#[cfg(not(windows))]
fn shell_profile_file(shell: &str) -> &'static str {
if shell.ends_with("zsh") {
".zshrc"
} else if shell.ends_with("bash") {
".bashrc"
} else {
".profile"
}
}
fn profile_target() -> Option<(PathBuf, Syntax)> {
#[cfg(not(windows))]
{
let home = std::env::var("HOME").ok()?;
let shell = std::env::var("SHELL").unwrap_or_default();
let file = shell_profile_file(&shell);
Some((PathBuf::from(home).join(file), Syntax::Posix))
}
#[cfg(windows)]
{
powershell_profile().map(|p| (p, Syntax::PowerShell))
}
}
#[cfg(windows)]
fn powershell_profile() -> Option<PathBuf> {
if let Ok(out) = std::process::Command::new("powershell")
.args(["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"])
.output()
{
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
let up = std::env::var("USERPROFILE").ok()?;
Some(
PathBuf::from(up)
.join("Documents")
.join("PowerShell")
.join("profile.ps1"),
)
}
#[allow(dead_code)]
fn env_block(proxy: &str, ca: &str, syntax: Syntax) -> String {
match syntax {
Syntax::Posix => format!(
"{BEGIN}\n\
export HTTPS_PROXY=\"{proxy}\"\n\
export HTTP_PROXY=\"{proxy}\"\n\
export NO_PROXY=\"{NO_PROXY}\"\n\
export no_proxy=\"{NO_PROXY}\"\n\
export NODE_EXTRA_CA_CERTS=\"{ca}\"\n\
{END}\n"
),
Syntax::PowerShell => format!(
"{BEGIN}\n\
$env:HTTPS_PROXY = \"{proxy}\"\n\
$env:HTTP_PROXY = \"{proxy}\"\n\
$env:NO_PROXY = \"{NO_PROXY}\"\n\
$env:NODE_EXTRA_CA_CERTS = \"{ca}\"\n\
{END}\n"
),
}
}
#[cfg(not(windows))]
fn write_profile_block_in(
base: &std::path::Path,
shell: &str,
proxy: &str,
ca: &str,
) -> Result<Vec<PathBuf>> {
let _ = remove_profile_block_in(base);
let mut targets: Vec<PathBuf> = candidate_profiles(base)
.into_iter()
.filter(|p| p.exists())
.collect();
let shell_default = base.join(shell_profile_file(shell));
if !targets.contains(&shell_default) {
targets.push(shell_default); }
let block = env_block(proxy, ca, Syntax::Posix);
let mut written = Vec::with_capacity(targets.len());
for path in targets {
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let mut base_content = strip_block(&existing);
if !base_content.is_empty() && !base_content.ends_with('\n') {
base_content.push('\n');
}
std::fs::write(&path, format!("{base_content}{block}"))
.with_context(|| format!("failed to write {}", path.display()))?;
written.push(path);
}
Ok(written)
}
#[allow(dead_code)]
fn write_profile_block(proxy: &str, ca: &str) -> Result<Vec<PathBuf>> {
#[cfg(not(windows))]
{
let Ok(home) = std::env::var("HOME") else {
return Ok(Vec::new());
};
let shell = std::env::var("SHELL").unwrap_or_default();
write_profile_block_in(std::path::Path::new(&home), &shell, proxy, ca)
}
#[cfg(windows)]
{
let Some((path, syntax)) = profile_target() else {
return Ok(Vec::new());
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent); }
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let mut base_content = strip_block(&existing);
if !base_content.is_empty() && !base_content.ends_with('\n') {
base_content.push('\n');
}
let block = env_block(proxy, ca, syntax);
std::fs::write(&path, format!("{base_content}{block}"))
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(vec![path])
}
}
fn strip_block(s: &str) -> String {
let has_begin = s.lines().any(|l| l.trim() == BEGIN);
let has_end = s.lines().any(|l| l.trim() == END);
if has_begin && !has_end {
return s.to_string();
}
let mut out = String::new();
let mut skip = false;
for line in s.lines() {
match line.trim() {
BEGIN => skip = true,
END => skip = false,
_ if !skip => {
out.push_str(line);
out.push('\n');
}
_ => {}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caveman_detection_covers_flag_hooks_and_plugin() {
let tmp = std::env::temp_dir().join(format!("llmtrim-caveman-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
assert!(!caveman_installed_in(&tmp));
std::fs::create_dir_all(&tmp).expect("create test dir");
assert!(!caveman_installed_in(&tmp));
std::fs::write(tmp.join(".caveman-active"), "1").expect("write flag");
assert!(caveman_installed_in(&tmp));
std::fs::remove_file(tmp.join(".caveman-active")).expect("rm flag");
std::fs::create_dir_all(tmp.join("hooks")).expect("mkdir hooks");
std::fs::write(tmp.join("hooks/caveman-activate.js"), "//").expect("write hook");
assert!(caveman_installed_in(&tmp));
std::fs::remove_file(tmp.join("hooks/caveman-activate.js")).expect("rm hook");
assert!(!caveman_installed_in(&tmp));
std::fs::create_dir_all(tmp.join("plugins/cache/caveman")).expect("mkdir plugin");
assert!(caveman_installed_in(&tmp));
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(not(windows))]
#[test]
fn heal_rewrites_old_block_adds_no_proxy_preserving_port_and_surroundings() {
let tmp = std::env::temp_dir().join(format!("llmtrim-heal-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("mkdir");
let old = format!(
"export FOO=bar\n{BEGIN}\n\
export HTTPS_PROXY=\"http://127.0.0.1:43117\"\n\
export HTTP_PROXY=\"http://127.0.0.1:43117\"\n\
export NODE_EXTRA_CA_CERTS=\"/home/u/ca.pem\"\n{END}\nexport BAZ=qux\n"
);
let rc = tmp.join(".bashrc");
std::fs::write(&rc, &old).expect("write rc");
let healed =
heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem").expect("heal runs");
assert_eq!(healed, vec![rc.clone()], "the old block is healed");
let after = std::fs::read_to_string(&rc).expect("read back");
assert!(after.contains("export NO_PROXY="), "bypass added");
assert!(after.contains("127.0.0.1:43117"), "port preserved");
assert!(after.contains("export FOO=bar") && after.contains("export BAZ=qux"));
assert_eq!(after.matches(BEGIN).count(), 1, "block not duplicated");
let again = heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem")
.expect("second heal");
assert!(again.is_empty(), "already-current block is left alone");
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(not(windows))]
#[test]
fn managed_block_needs_heal_distinguishes_old_current_malformed_and_outside() {
let old = format!("{BEGIN}\nexport HTTPS_PROXY=\"x\"\n{END}\n");
assert!(managed_block_needs_heal(&old), "old block needs heal");
let current = format!("{BEGIN}\nexport NO_PROXY=\"y\"\n{END}\n");
assert!(!managed_block_needs_heal(¤t), "current block skipped");
let malformed = format!("{BEGIN}\nexport HTTPS_PROXY=\"x\"\n"); assert!(
!managed_block_needs_heal(&malformed),
"malformed block skipped"
);
let outside = format!("export NO_PROXY=mine\n{BEGIN}\nexport HTTPS_PROXY=\"x\"\n{END}\n");
assert!(
managed_block_needs_heal(&outside),
"NO_PROXY outside the block does not count as healed"
);
assert!(!managed_block_needs_heal("export FOO=bar\n"), "no block");
}
#[cfg(not(windows))]
#[test]
fn heal_skips_malformed_block_without_corrupting_it() {
let tmp = std::env::temp_dir().join(format!("llmtrim-heal-bad-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("mkdir");
let bad = format!("keep\n{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:43117\"\n");
std::fs::write(tmp.join(".bashrc"), &bad).expect("write");
let healed =
heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem").expect("heal");
assert!(healed.is_empty(), "malformed block is not healed");
assert_eq!(
std::fs::read_to_string(tmp.join(".bashrc")).expect("read"),
bad,
"malformed file left byte-for-byte untouched"
);
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(not(windows))]
#[test]
fn heal_touches_only_the_stale_profile_in_a_multi_shell_setup() {
let tmp = std::env::temp_dir().join(format!("llmtrim-heal-multi-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("mkdir");
let old = format!("{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:43117\"\n{END}\n");
let current = env_block("http://127.0.0.1:43117", "/home/u/ca.pem", Syntax::Posix);
std::fs::write(tmp.join(".bashrc"), &old).expect("write bashrc");
std::fs::write(tmp.join(".zshrc"), ¤t).expect("write zshrc");
let healed =
heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem").expect("heal");
assert_eq!(
healed,
vec![tmp.join(".bashrc")],
"only the stale profile healed"
);
assert!(
std::fs::read_to_string(tmp.join(".bashrc"))
.expect("read bashrc")
.contains("NO_PROXY"),
"stale profile now carries the bypass"
);
assert_eq!(
std::fs::read_to_string(tmp.join(".zshrc")).expect("read zshrc"),
current,
"already-current profile left untouched"
);
let _ = std::fs::remove_dir_all(&tmp);
}
#[cfg(not(windows))]
#[test]
fn heal_ignores_files_without_a_managed_block() {
let tmp = std::env::temp_dir().join(format!("llmtrim-heal-none-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("mkdir");
std::fs::write(tmp.join(".zshrc"), "export FOO=bar\n").expect("write");
let healed =
heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem").expect("heal");
assert!(
healed.is_empty(),
"a file with no block is never created/touched"
);
assert_eq!(
std::fs::read_to_string(tmp.join(".zshrc")).expect("read"),
"export FOO=bar\n",
"content untouched"
);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn choose_port_precedence() {
assert_eq!(
choose_port(Some(9000), Some(8800), Some(8700)),
PortChoice::Use(9000)
);
assert_eq!(
choose_port(None, Some(8800), Some(8700)),
PortChoice::Use(8800)
);
assert_eq!(choose_port(None, None, Some(8700)), PortChoice::Use(8700));
assert_eq!(
choose_port(None, None, None),
PortChoice::ScanFrom(DEFAULT_PORT)
);
}
#[test]
fn resolve_port_prefers_running_over_default() {
assert_eq!(resolve_port(None, Some(9001)).expect("running port"), 9001);
assert_eq!(
resolve_port(Some(7000), Some(9001)).expect("explicit"),
7000
);
}
#[test]
fn parse_proxy_port_reads_the_wired_port() {
assert_eq!(parse_proxy_port("http://127.0.0.1:8787"), Some(8787));
assert_eq!(
parse_proxy_port("export HTTPS_PROXY=\"http://127.0.0.1:9001\"\nexport X=1\n"),
Some(9001)
);
assert_eq!(parse_proxy_port("no proxy here"), None);
assert_eq!(parse_proxy_port("127.0.0.1:"), None); }
#[test]
fn strip_path_entry_removes_only_the_target() {
let path = r"C:\Windows;C:\Users\u\AppData\Local\llmtrim\bin;C:\tools";
let dir = r"C:\Users\u\AppData\Local\llmtrim\bin";
assert_eq!(strip_path_entry(path, dir), r"C:\Windows;C:\tools");
let messy = r"C:\a;c:\users\u\appdata\local\LLMTRIM\BIN\;C:\b";
assert_eq!(strip_path_entry(messy, dir), r"C:\a;C:\b");
assert_eq!(strip_path_entry(r"C:\a;C:\b", dir), r"C:\a;C:\b");
assert_eq!(strip_path_entry(&format!(";{dir}"), dir), "");
}
#[test]
fn strip_block_removes_managed_section_only() {
let input = format!("keep1\n{BEGIN}\nexport X=1\n{END}\nkeep2\n");
let out = strip_block(&input);
assert_eq!(out, "keep1\nkeep2\n");
}
#[test]
fn strip_block_is_noop_without_markers() {
assert_eq!(strip_block("a\nb\n"), "a\nb\n");
}
#[test]
fn strip_block_missing_end_marker_returns_original() {
let input = "before\n# >>> llmtrim >>>\nexport HTTPS_PROXY=http://127.0.0.1:8787\n";
assert_eq!(strip_block(input), input);
}
#[test]
fn strip_block_normal_removes_block() {
let input = "before\n# >>> llmtrim >>>\nexport HTTPS_PROXY=http://127.0.0.1:8787\n# <<< llmtrim <<<\nafter\n";
let result = strip_block(input);
assert!(result.contains("before"));
assert!(result.contains("after"));
assert!(!result.contains("HTTPS_PROXY"));
}
#[test]
fn env_block_posix_uses_export() {
let b = env_block("http://127.0.0.1:8787", "/home/u/ca.pem", Syntax::Posix);
assert!(b.contains("export HTTPS_PROXY=\"http://127.0.0.1:8787\""));
assert!(b.contains("export NODE_EXTRA_CA_CERTS=\"/home/u/ca.pem\""));
assert!(b.contains(&format!("export NO_PROXY=\"{NO_PROXY}\"")));
assert!(b.contains(&format!("export no_proxy=\"{NO_PROXY}\"")));
assert!(b.starts_with(BEGIN) && b.trim_end().ends_with(END));
}
#[test]
fn env_block_powershell_uses_env_assignment() {
let b = env_block(
"http://127.0.0.1:8787",
"C:\\Users\\u\\ca.pem",
Syntax::PowerShell,
);
assert!(b.contains("$env:HTTPS_PROXY = \"http://127.0.0.1:8787\""));
assert!(b.contains("$env:NODE_EXTRA_CA_CERTS = \"C:\\Users\\u\\ca.pem\""));
assert!(b.contains(&format!("$env:NO_PROXY = \"{NO_PROXY}\"")));
assert!(!b.contains("export ")); }
#[test]
fn strip_block_reverses_powershell_block() {
let withblock = format!("keep\n{}", env_block("p", "c", Syntax::PowerShell));
assert_eq!(strip_block(&withblock), "keep\n");
}
#[test]
fn write_then_strip_is_idempotent() {
let original = "export FOO=bar\n";
let proxy = "http://127.0.0.1:8787";
let ca = "/home/user/.llmtrim/ca.crt";
let block = env_block(proxy, ca, Syntax::Posix);
let written = format!("{original}{block}");
let stripped = strip_block(&written);
assert_eq!(
stripped.trim_end(),
original.trim_end(),
"strip after write returns original"
);
}
#[test]
fn double_write_does_not_duplicate_block() {
let proxy = "http://127.0.0.1:8787";
let ca = "/home/user/.llmtrim/ca.crt";
let original = "export FOO=bar\n";
let block = env_block(proxy, ca, Syntax::Posix);
let after_first = format!("{original}{block}");
let after_second = format!("{}{}", strip_block(&after_first), block);
let begin_count = after_second.matches(">>> llmtrim >>>").count();
assert_eq!(
begin_count, 1,
"block must appear exactly once after double write"
);
}
#[cfg(windows)]
#[test]
fn registry_env_set_has_clear_roundtrip() {
use winreg::RegKey;
use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let scratch = format!("Software\\llmtrim-test-{}", std::process::id());
let (env, _) = hkcu
.create_subkey_with_flags(&scratch, KEY_READ | KEY_WRITE)
.expect("create scratch key");
assert!(!has_proxy_in(&env), "fresh key has no proxy");
assert!(
!clear_env_in(&env).expect("clear on empty key"),
"nothing to clear yet"
);
set_env_in(&env, "http://127.0.0.1:18784", "C:\\Users\\u\\ca.pem").expect("set env");
assert!(has_proxy_in(&env), "proxy set");
assert_eq!(
env.get_value::<String, _>("NODE_EXTRA_CA_CERTS")
.expect("read CA value"),
"C:\\Users\\u\\ca.pem"
);
assert!(
clear_env_in(&env).expect("clear set values"),
"values removed"
);
assert!(!has_proxy_in(&env), "proxy gone after clear");
hkcu.delete_subkey_all(&scratch).ok();
}
#[test]
fn first_free_port_rejects_occupied_accepts_free() {
let held = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind ephemeral");
let taken = held.local_addr().expect("local_addr").port();
assert_eq!(
first_free_port(taken, 0),
None,
"occupied port not rejected"
);
let found = first_free_port(taken, 64).expect("a free port exists in the span");
assert_ne!(found, taken, "scan returned the still-occupied port");
}
#[cfg(not(windows))]
struct TempDir(PathBuf);
#[cfg(not(windows))]
impl TempDir {
fn new(suffix: &str) -> Self {
let dir = std::env::temp_dir().join(format!(
"llmtrim-test-{}-{}",
std::process::id(),
suffix
));
std::fs::create_dir_all(&dir).expect("create temp dir");
Self(dir)
}
fn path(&self) -> &std::path::Path {
&self.0
}
}
#[cfg(not(windows))]
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(not(windows))]
fn write_block_to(base: &std::path::Path, name: &str) {
let block = format!("{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:8787\"\n{END}\n");
std::fs::write(base.join(name), block).expect("write test block");
}
#[cfg(not(windows))]
#[test]
fn remove_profile_block_in_cleans_all_files_that_contain_block() {
let dir = TempDir::new("sweep-two");
let base = dir.path();
write_block_to(base, ".bashrc");
write_block_to(base, ".zshrc");
let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
let mut names: Vec<String> = cleaned
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
names.sort();
assert_eq!(names, vec![".bashrc", ".zshrc"]);
for name in [".bashrc", ".zshrc"] {
let content = std::fs::read_to_string(base.join(name)).expect("read back");
assert!(
!content.contains(BEGIN),
"{name} still contains BEGIN marker after sweep"
);
}
}
#[cfg(not(windows))]
#[test]
fn remove_profile_block_in_noop_when_no_blocks() {
let dir = TempDir::new("noop");
let base = dir.path();
std::fs::write(base.join(".bashrc"), "export FOO=bar\n").expect("write .bashrc");
let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
assert!(cleaned.is_empty(), "expected no files cleaned");
let content = std::fs::read_to_string(base.join(".bashrc")).expect("read");
assert_eq!(content, "export FOO=bar\n");
}
#[cfg(not(windows))]
#[test]
fn remove_profile_block_in_skips_absent_files_cleans_rest() {
let dir = TempDir::new("skip-absent");
let base = dir.path();
write_block_to(base, ".zshrc");
let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
assert_eq!(cleaned.len(), 1);
assert_eq!(cleaned[0].file_name().unwrap().to_string_lossy(), ".zshrc");
}
#[cfg(not(windows))]
#[test]
fn profile_has_block_in_detects_block_in_any_candidate() {
let dir = TempDir::new("has-block");
let base = dir.path();
assert!(!profile_has_block_in(base));
write_block_to(base, ".bashrc");
assert!(profile_has_block_in(base));
remove_profile_block_in(base).expect("sweep");
assert!(!profile_has_block_in(base));
write_block_to(base, ".zshrc");
assert!(profile_has_block_in(base));
}
#[cfg(not(windows))]
#[test]
fn write_profile_block_in_roundtrip_preserves_existing_content() {
let dir = TempDir::new("wpb-roundtrip");
let base = dir.path();
let proxy = "http://127.0.0.1:8787";
let ca = "/tmp/ca.crt";
std::fs::write(base.join(".bashrc"), "export FOO=bar\nexport BAZ=qux\n")
.expect("write pre-existing .bashrc");
let paths =
write_profile_block_in(base, "bash", proxy, ca).expect("write_profile_block_in");
assert_eq!(paths, vec![base.join(".bashrc")]);
let content = std::fs::read_to_string(base.join(".bashrc")).expect("read back");
assert!(
content.contains("export FOO=bar"),
"pre-existing content lost"
);
assert!(content.contains(BEGIN), "BEGIN marker missing");
assert!(content.contains(END), "END marker missing");
assert!(
content.contains(&format!("export HTTPS_PROXY=\"{proxy}\"")),
"proxy export missing"
);
assert!(
content.contains(&format!("export NODE_EXTRA_CA_CERTS=\"{ca}\"")),
"CA export missing"
);
}
#[cfg(not(windows))]
#[test]
fn write_profile_block_in_idempotent_second_call_does_not_duplicate_block() {
let dir = TempDir::new("wpb-idempotent");
let base = dir.path();
let proxy = "http://127.0.0.1:8787";
let ca = "/tmp/ca.crt";
std::fs::write(base.join(".bashrc"), "# user config\n").expect("write .bashrc");
write_profile_block_in(base, "bash", proxy, ca).expect("first write");
write_profile_block_in(base, "bash", proxy, ca).expect("second write");
let content = std::fs::read_to_string(base.join(".bashrc")).expect("read back");
let begin_count = content.matches(BEGIN).count();
assert_eq!(
begin_count, 1,
"BEGIN marker appears {begin_count} times after two writes — expected 1"
);
assert!(
content.contains("# user config"),
"pre-existing content lost after double write"
);
}
#[cfg(not(windows))]
#[test]
fn write_profile_block_in_refreshes_all_existing_and_creates_shell_default() {
let dir = TempDir::new("wpb-switch");
let base = dir.path();
let ca = "/tmp/ca.crt";
write_block_to(base, ".bashrc");
let new_proxy = "http://127.0.0.1:9999";
let paths = write_profile_block_in(base, "zsh", new_proxy, ca).expect("write for zsh");
assert!(
paths.contains(&base.join(".bashrc")),
"existing .bashrc written: {paths:?}"
);
assert!(
paths.contains(&base.join(".zshrc")),
"zsh default created: {paths:?}"
);
for name in [".bashrc", ".zshrc"] {
let body = std::fs::read_to_string(base.join(name)).expect("read rc");
assert_eq!(body.matches(BEGIN).count(), 1, "{name}: exactly one block");
assert!(body.contains("9999"), "{name}: refreshed to the new port");
assert!(
!body.contains("8787"),
"{name}: old port must be gone (no stale block)"
);
}
}
#[cfg(not(windows))]
#[test]
fn write_profile_block_in_creates_only_the_shell_default_when_none_exist() {
let dir = TempDir::new("wpb-none");
let base = dir.path();
let paths = write_profile_block_in(base, "/bin/zsh", "http://127.0.0.1:8788", "/tmp/ca")
.expect("write");
assert_eq!(
paths,
vec![base.join(".zshrc")],
"only the zsh default is created"
);
assert!(base.join(".zshrc").exists());
for other in [".zprofile", ".bashrc", ".bash_profile", ".profile"] {
assert!(!base.join(other).exists(), "{other} must not be created");
}
}
#[cfg(not(windows))]
#[test]
fn write_profile_block_in_writes_to_all_existing_rc_files() {
let dir = TempDir::new("wpb-all");
let base = dir.path();
for f in [".zshrc", ".bashrc", ".profile"] {
std::fs::write(base.join(f), "# pre-existing\n").expect("seed rc");
}
let paths = write_profile_block_in(base, "/bin/bash", "http://127.0.0.1:8788", "/tmp/ca")
.expect("write");
for f in [".zshrc", ".bashrc", ".profile"] {
assert!(paths.contains(&base.join(f)), "{f} written: {paths:?}");
let body = std::fs::read_to_string(base.join(f)).expect("read");
assert_eq!(body.matches(BEGIN).count(), 1, "{f}: one block");
assert!(body.contains("# pre-existing"), "{f}: kept user content");
}
assert!(!base.join(".zprofile").exists());
assert!(!base.join(".bash_profile").exists());
}
#[test]
fn strip_block_at_file_start() {
let input = format!("{BEGIN}\nexport X=1\n{END}\nafter\n");
let out = strip_block(&input);
assert_eq!(out, "after\n");
}
#[test]
fn strip_block_at_file_end() {
let input = format!("before\n{BEGIN}\nexport X=1\n{END}\n");
let out = strip_block(&input);
assert_eq!(out, "before\n");
}
#[test]
fn strip_block_in_the_middle() {
let input = format!("top\n{BEGIN}\nexport X=1\n{END}\nbottom\n");
let out = strip_block(&input);
assert_eq!(out, "top\nbottom\n");
}
#[test]
fn strip_block_two_stacked_blocks() {
let input =
format!("before\n{BEGIN}\nexport X=1\n{END}\n{BEGIN}\nexport Y=2\n{END}\nafter\n");
let out = strip_block(&input);
assert_eq!(out, "before\nafter\n");
}
#[test]
fn strip_block_no_trailing_newline() {
let no_block = "just some text";
let out = strip_block(no_block);
assert_eq!(out, "just some text\n");
let with_block = format!("before\n{BEGIN}\nexport X=1\n{END}");
let out2 = strip_block(&with_block);
assert_eq!(out2, "before\n");
}
#[test]
fn env_block_posix_all_lines_are_valid_exports() {
let proxy = "http://127.0.0.1:8787";
let ca = "/home/user/.llmtrim/ca.crt";
let block = env_block(proxy, ca, Syntax::Posix);
let inner: Vec<&str> = block.lines().filter(|l| *l != BEGIN && *l != END).collect();
assert!(!inner.is_empty(), "no inner lines in POSIX block");
for line in &inner {
assert!(
line.starts_with("export ") && line.contains("=\""),
"line is not a valid POSIX export: {line:?}"
);
}
}
#[test]
fn env_block_powershell_all_lines_use_dollar_env() {
let proxy = "http://127.0.0.1:8787";
let ca = "C:\\Users\\u\\ca.pem";
let block = env_block(proxy, ca, Syntax::PowerShell);
let inner: Vec<&str> = block.lines().filter(|l| *l != BEGIN && *l != END).collect();
assert!(!inner.is_empty(), "no inner lines in PowerShell block");
for line in &inner {
assert!(
line.starts_with("$env:") && line.contains(" = \""),
"line is not a valid PowerShell env assignment: {line:?}"
);
assert!(
!line.starts_with("export "),
"POSIX `export` leaked into PowerShell block: {line:?}"
);
}
}
}