use std::path::{Path, PathBuf};
pub const CONFIG_DIR_ENV: &str = "OPENLATCH_CURSOR_DIR";
const DEFAULT_DIR_NAME: &str = ".cursor";
fn relocated_dir() -> Option<PathBuf> {
std::env::var_os(CONFIG_DIR_ENV)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn default_root() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(DEFAULT_DIR_NAME))
}
pub fn root() -> Option<PathBuf> {
relocated_dir().or_else(default_root)
}
pub fn hooks_json_path(root: &Path) -> PathBuf {
root.join("hooks.json")
}
pub fn config_is_machine_global() -> bool {
let (Some(resolved), Some(default)) = (root(), default_root()) else {
return true;
};
let canonical = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
canonical(&resolved) == canonical(&default)
}
pub fn absent_seam(root: &Path) -> (&'static str, PathBuf) {
(CONFIG_DIR_ENV, root.join("absent-cursor"))
}
pub fn detect() -> Option<PathBuf> {
detect_in(
relocated_dir(),
dirs::home_dir().as_deref(),
std::env::var_os("PATH").as_deref(),
)
}
fn detect_in(
relocated: Option<PathBuf>,
home: Option<&Path>,
path_var: Option<&std::ffi::OsStr>,
) -> Option<PathBuf> {
if let Some(root) = relocated {
return root.is_dir().then_some(root);
}
let home = home?;
(ide_installed(home, path_var) || cli_installed(home, path_var))
.then(|| home.join(DEFAULT_DIR_NAME))
}
fn ide_installed(home: &Path, path_var: Option<&std::ffi::OsStr>) -> bool {
let mut candidates: Vec<PathBuf> = Vec::new();
if cfg!(target_os = "macos") {
candidates.push(PathBuf::from("/Applications").join("Cursor.app")); candidates.push(home.join("Applications").join("Cursor.app"));
}
if cfg!(windows) {
if let Some(local) = dirs::data_local_dir() {
candidates.push(local.join("Programs").join("cursor").join("Cursor.exe"));
}
if let Some(program_files) = std::env::var_os("ProgramFiles") {
candidates.push(
PathBuf::from(program_files)
.join("cursor")
.join("Cursor.exe"),
);
}
}
if cfg!(target_os = "linux") {
candidates.push(PathBuf::from("/usr/share/cursor")); candidates.push(PathBuf::from("/opt/Cursor")); candidates.push(PathBuf::from("/usr/share/applications").join("cursor.desktop")); candidates.push(
home.join(".local")
.join("share")
.join("applications")
.join("cursor.desktop"),
);
}
candidates.iter().any(|path| path.exists())
|| (cfg!(target_os = "linux") && on_path(path_var, "cursor"))
}
fn cli_installed(home: &Path, path_var: Option<&std::ffi::OsStr>) -> bool {
if on_path(path_var, "cursor-agent") {
return true;
}
let mut candidates = vec![home.join(".local").join("share").join("cursor-agent")];
if cfg!(windows) {
if let Some(local) = dirs::data_local_dir() {
candidates.push(local.join("cursor-agent"));
}
}
candidates.iter().any(|path| path.exists())
}
fn on_path(path_var: Option<&std::ffi::OsStr>, name: &str) -> bool {
let Some(path_var) = path_var else {
return false;
};
let names: Vec<String> = if cfg!(windows) {
["exe", "cmd", "ps1"]
.iter()
.map(|ext| format!("{name}.{ext}"))
.collect()
} else {
vec![name.to_string()]
};
std::env::split_paths(path_var)
.filter(|dir| dir.is_absolute())
.any(|dir| names.iter().any(|n| dir.join(n).is_file()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::cline::EnvOverride;
fn seam_lock() -> std::sync::MutexGuard<'static, ()> {
crate::hooks::cline::SEAM_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner())
}
#[test]
fn cursor_root_honours_the_seam() {
let _lock = seam_lock();
let dir = tempfile::tempdir().expect("temp dir");
let _env = EnvOverride::apply([(CONFIG_DIR_ENV, Some(dir.path().as_os_str().to_owned()))]);
assert_eq!(root().as_deref(), Some(dir.path()));
assert_eq!(
hooks_json_path(&root().expect("root")),
dir.path().join("hooks.json")
);
assert!(
!config_is_machine_global(),
"a relocated root is not the machine's own"
);
assert_eq!(
detect().as_deref(),
Some(dir.path()),
"under the seam, the root existing counts"
);
}
#[test]
fn cursor_root_empty_seam_is_unset() {
let _lock = seam_lock();
let _env = EnvOverride::apply([(CONFIG_DIR_ENV, Some(std::ffi::OsString::new()))]);
assert_eq!(relocated_dir(), None, "an empty seam reads as unset");
assert_eq!(root(), default_root());
assert!(
config_is_machine_global(),
"an empty seam leaves the machine's own root"
);
}
#[test]
fn an_absent_seam_is_not_a_detection() {
let _lock = seam_lock();
let dir = tempfile::tempdir().expect("temp dir");
let (key, path) = absent_seam(dir.path());
let _env = EnvOverride::apply([(key, Some(path.clone().into_os_string()))]);
assert!(
!path.exists(),
"the premise: the absent seam names a path it never creates"
);
assert_eq!(detect(), None);
}
#[test]
fn leftover_dot_cursor_alone_is_not_a_detection() {
let home = tempfile::tempdir().expect("temp home");
std::fs::create_dir_all(home.path().join(".cursor")).expect("leftover root");
let empty = tempfile::tempdir().expect("empty PATH dir");
let path_var = empty.path().as_os_str();
let expected = home.path().join(".cursor");
if ide_installed(home.path(), Some(path_var)) {
assert_eq!(
detect_in(None, Some(home.path()), Some(path_var)),
Some(expected)
);
return;
}
assert!(!cli_installed(home.path(), Some(path_var)));
assert_eq!(
detect_in(None, Some(home.path()), Some(path_var)),
None,
"a leftover ~/.cursor alone is not an install"
);
std::fs::create_dir_all(
home.path()
.join(".local")
.join("share")
.join("cursor-agent"),
)
.expect("cli dir");
assert_eq!(
detect_in(None, Some(home.path()), Some(path_var)),
Some(expected)
);
}
#[test]
fn the_generic_agent_name_is_not_the_cli() {
let home = tempfile::tempdir().expect("temp home");
let bin = tempfile::tempdir().expect("PATH dir");
let path_var = bin.path().as_os_str();
std::fs::write(bin.path().join("agent"), "").expect("a generic `agent`");
assert!(
!cli_installed(home.path(), Some(path_var)),
"`agent` alone must never read as Cursor's CLI"
);
let name = if cfg!(windows) {
"cursor-agent.exe"
} else {
"cursor-agent"
};
std::fs::write(bin.path().join(name), "").expect("cursor-agent");
assert!(cli_installed(home.path(), Some(path_var)));
}
}