use std::path::{Path, PathBuf};
use crate::core::user_path::{self, PathSink};
use crate::hooks::bindings::ENV_FILE_DELIVERIES;
pub const SHIMS_DIR: &str = "shims";
pub const FIELD_SHIM: &str = "cli_shim";
pub const FIELD_USER_PATH: &str = "user_path";
pub fn shims_dir(ol_dir: &Path) -> PathBuf {
ol_dir.join(SHIMS_DIR)
}
pub fn shim_commands() -> impl Iterator<Item = &'static str> {
ENV_FILE_DELIVERIES
.iter()
.flat_map(|d| d.commands.iter().map(|c| c.name))
}
pub fn shim_path(dir: &Path, cmd: &str) -> PathBuf {
dir.join(format!("{cmd}.exe"))
}
pub fn wanted(owns_machine: bool) -> bool {
cfg!(windows) && owns_machine
}
fn same_binary(a: &Path, b: &Path) -> bool {
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(a), Ok(b)) => a.len() == b.len() && a.modified().ok() == b.modified().ok(),
_ => false,
}
}
pub fn stale_shims(ol_dir: &Path, exe: &Path) -> Vec<&'static str> {
let dir = shims_dir(ol_dir);
shim_commands()
.filter(|cmd| !same_binary(&shim_path(&dir, cmd), exe))
.collect()
}
fn place(exe: &Path, target: &Path) -> std::io::Result<()> {
let staged = target.with_extension("exe.new");
let _ = std::fs::remove_file(&staged);
if std::fs::hard_link(exe, &staged).is_err() {
std::fs::copy(exe, &staged)?;
if let Ok(modified) = std::fs::metadata(exe).and_then(|m| m.modified()) {
std::fs::File::options()
.write(true)
.open(&staged)
.and_then(|f| f.set_modified(modified))?;
}
}
if target.exists() {
let placed = crate::core::update::place_launcher(&staged, target)?;
if let Some(bak) = placed.bak {
let _ = std::fs::remove_file(bak);
}
Ok(())
} else {
std::fs::rename(&staged, target)
}
}
pub fn ensure_shims(ol_dir: &Path, exe: &Path) -> Result<Vec<&'static str>, String> {
let dir = shims_dir(ol_dir);
let stale = stale_shims(ol_dir, exe);
if stale.is_empty() {
return Ok(stale);
}
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
for cmd in &stale {
let target = shim_path(&dir, cmd);
place(exe, &target).map_err(|e| format!("{}: {e}", target.display()))?;
}
Ok(stale)
}
pub fn remove_shims(ol_dir: &Path) -> Result<bool, String> {
let dir = shims_dir(ol_dir);
match std::fs::remove_dir_all(&dir) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(format!("{}: {e}", dir.display())),
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Placed {
pub shims: Vec<&'static str>,
pub user_path: bool,
}
pub fn ensure_all(ol_dir: &Path, exe: &Path, sink: &dyn PathSink) -> Result<Placed, String> {
let shims = ensure_shims(ol_dir, exe)?;
let user_path = user_path::ensure_first(sink, &shims_dir(ol_dir))?;
Ok(Placed { shims, user_path })
}
pub fn remove_all(ol_dir: &Path, sink: &dyn PathSink) -> Result<bool, String> {
let path = user_path::remove(sink, &shims_dir(ol_dir))?;
let shims = remove_shims(ol_dir)?;
Ok(path || shims)
}
pub fn drift(ol_dir: &Path, exe: &Path, sink: &dyn PathSink) -> Vec<&'static str> {
let mut lost = Vec::new();
if !stale_shims(ol_dir, exe).is_empty() {
lost.push(FIELD_SHIM);
}
let first = sink
.read()
.ok()
.flatten()
.is_some_and(|p| user_path::is_first(&p, &shims_dir(ol_dir).display().to_string()));
if !first {
lost.push(FIELD_USER_PATH);
}
lost
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
pub(crate) struct RecordingPath {
pub value: Mutex<Option<String>>,
}
impl PathSink for RecordingPath {
fn read(&self) -> Result<Option<String>, String> {
Ok(self.value.lock().unwrap().clone())
}
fn write(&self, value: &str) -> Result<(), String> {
*self.value.lock().unwrap() = Some(value.to_string());
Ok(())
}
}
fn fake_exe(dir: &Path, body: &[u8]) -> PathBuf {
let exe = dir.join("openlatch.exe");
std::fs::write(&exe, body).unwrap();
exe
}
#[test]
fn a_shim_is_placed_for_every_env_file_command() {
let cmds: Vec<_> = shim_commands().collect();
for cmd in ["cursor-agent", "agent", "opencode", "cline"] {
assert!(cmds.contains(&cmd), "{cmd} has no shim");
}
}
#[test]
fn ensure_places_links_once_and_relinks_after_an_update() {
let tmp = tempfile::tempdir().unwrap();
let (ol, bin) = (tmp.path().join("ol"), tmp.path().join("bin"));
std::fs::create_dir_all(&bin).unwrap();
let exe = fake_exe(&bin, b"v1");
assert_eq!(
ensure_shims(&ol, &exe).unwrap().len(),
shim_commands().count()
);
assert!(ensure_shims(&ol, &exe).unwrap().is_empty(), "idempotent");
let shim = shim_path(&shims_dir(&ol), "opencode");
assert_eq!(std::fs::read(&shim).unwrap(), b"v1");
std::fs::remove_file(&exe).unwrap();
let exe = fake_exe(&bin, b"v2-longer");
assert_eq!(stale_shims(&ol, &exe).len(), shim_commands().count());
ensure_shims(&ol, &exe).unwrap();
assert_eq!(std::fs::read(&shim).unwrap(), b"v2-longer");
assert!(!shim.with_extension("bak").exists());
}
#[test]
fn ensure_all_places_shims_then_puts_the_folder_first() {
let tmp = tempfile::tempdir().unwrap();
let ol = tmp.path().join("ol");
let exe = fake_exe(tmp.path(), b"x");
let sink = RecordingPath::default();
*sink.value.lock().unwrap() = Some("C:\\tools".into());
let placed = ensure_all(&ol, &exe, &sink).unwrap();
assert!(placed.user_path);
assert!(drift(&ol, &exe, &sink).is_empty());
assert_eq!(ensure_all(&ol, &exe, &sink).unwrap(), Placed::default());
*sink.value.lock().unwrap() = Some(format!("C:\\new;{}", shims_dir(&ol).display()));
std::fs::remove_file(shim_path(&shims_dir(&ol), "cline")).unwrap();
assert_eq!(drift(&ol, &exe, &sink), vec![FIELD_SHIM, FIELD_USER_PATH]);
let placed = ensure_all(&ol, &exe, &sink).unwrap();
assert_eq!(placed.shims, vec!["cline"]);
assert!(placed.user_path);
assert!(drift(&ol, &exe, &sink).is_empty());
}
#[test]
fn remove_all_takes_out_the_entry_and_the_folder() {
let tmp = tempfile::tempdir().unwrap();
let ol = tmp.path().join("ol");
let exe = fake_exe(tmp.path(), b"x");
let sink = RecordingPath::default();
*sink.value.lock().unwrap() = Some("C:\\tools".into());
ensure_all(&ol, &exe, &sink).unwrap();
assert!(remove_all(&ol, &sink).unwrap());
assert_eq!(sink.read().unwrap().as_deref(), Some("C:\\tools"));
assert!(!shims_dir(&ol).exists());
assert!(!remove_all(&ol, &sink).unwrap(), "idempotent");
}
#[test]
fn the_gate_needs_windows_and_the_machine_install() {
assert!(!wanted(false));
assert_eq!(wanted(true), cfg!(windows));
}
}