use std::path::{Path, PathBuf};
pub const CONFIG_DIR_ENV: &str = "OPENLATCH_CURSOR_DIR";
pub(crate) const IDE_DIR_ENV: &str = "OPENLATCH_CURSOR_IDE_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 ide_user_settings() -> Option<PathBuf> {
ide_user_settings_in(
relocated_ide_dir(),
dirs::config_dir(),
crate::supervision::owns_machine_supervision(),
)
}
fn relocated_ide_dir() -> Option<PathBuf> {
std::env::var_os(IDE_DIR_ENV)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
pub(crate) fn ide_user_settings_in(
relocated: Option<PathBuf>,
config_dir: Option<PathBuf>,
owns_machine: bool,
) -> Option<PathBuf> {
if !owns_machine {
return None;
}
let base = relocated.or_else(|| Some(config_dir?.join("Cursor")))?;
Some(base.join("User").join("settings.json"))
}
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(),
Some(crate::core::user_path::effective_path().as_os_str()),
)
}
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))
}
pub(crate) struct IdeLocation {
pub path: PathBuf,
pub package_json: Option<PathBuf>,
}
pub(crate) fn ide_locations(home: &Path) -> Vec<IdeLocation> {
let app = |dir: PathBuf| dir.join("resources").join("app").join("package.json");
let mut locations: Vec<IdeLocation> = Vec::new();
if cfg!(target_os = "macos") {
for bundle in [
PathBuf::from("/Applications").join("Cursor.app"), home.join("Applications").join("Cursor.app"),
] {
let package_json = bundle
.join("Contents")
.join("Resources")
.join("app")
.join("package.json");
locations.push(IdeLocation {
path: bundle,
package_json: Some(package_json),
});
}
}
if cfg!(windows) {
let mut dirs_: Vec<PathBuf> = Vec::new();
if let Some(local) = dirs::data_local_dir() {
dirs_.push(local.join("Programs").join("cursor"));
}
if let Some(program_files) = std::env::var_os("ProgramFiles") {
dirs_.push(PathBuf::from(program_files).join("cursor"));
}
for dir in dirs_ {
locations.push(IdeLocation {
path: dir.join("Cursor.exe"),
package_json: Some(app(dir)),
});
}
}
if cfg!(target_os = "linux") {
for dir in [
PathBuf::from("/usr/share/cursor"), PathBuf::from("/opt/Cursor"), ] {
locations.push(IdeLocation {
package_json: Some(app(dir.clone())),
path: dir,
});
}
for entry in [
PathBuf::from("/usr/share/applications").join("cursor.desktop"), home.join(".local")
.join("share")
.join("applications")
.join("cursor.desktop"),
] {
locations.push(IdeLocation {
path: entry,
package_json: None,
});
}
}
locations
}
#[cfg(feature = "model-relay")]
fn ide_roots(home: &Path) -> Vec<PathBuf> {
ide_locations(home)
.into_iter()
.filter(|l| l.package_json.is_some())
.filter_map(|l| {
if cfg!(windows) {
l.path.parent().map(Path::to_path_buf)
} else {
Some(l.path)
}
})
.collect()
}
#[cfg(feature = "model-relay")]
pub(crate) fn is_agent_host(p: &crate::hooks::cline_app::process::ExeProcess) -> bool {
let plugin_helper = p
.exe
.file_name()
.is_some_and(|n| n == "Cursor Helper (Plugin)");
let line = p.args.join(" ");
plugin_helper
|| (line.contains("--utility-sub-type=node.mojom.NodeService")
&& line.contains("--inspect-port"))
}
#[cfg(feature = "model-relay")]
pub(crate) fn newest_agent_host_start_in(
rows: &[crate::hooks::cline_app::process::ExeProcess],
) -> Option<std::time::SystemTime> {
rows.iter()
.filter(|p| is_agent_host(p))
.filter_map(|p| p.started)
.max()
}
#[cfg(feature = "model-relay")]
pub(crate) fn newest_agent_host_start() -> Result<Option<std::time::SystemTime>, String> {
let home = dirs::home_dir().ok_or("no home directory")?;
let rows = crate::hooks::cline_app::process::processes_under(&ide_roots(&home))?;
Ok(newest_agent_host_start_in(&rows))
}
#[cfg(feature = "model-relay")]
pub(crate) fn agent_host_started_after(t: std::time::SystemTime) -> Option<bool> {
newest_agent_host_start()
.ok()
.map(|newest| newest.is_some_and(|s| s > t))
}
pub(crate) fn ide_on_path(path_var: Option<&std::ffi::OsStr>) -> bool {
cfg!(target_os = "linux") && on_path(path_var, "cursor")
}
fn ide_installed(home: &Path, path_var: Option<&std::ffi::OsStr>) -> bool {
ide_locations(home).iter().any(|l| l.path.exists()) || ide_on_path(path_var)
}
pub(crate) fn cli_install_dirs(home: &Path) -> Vec<PathBuf> {
let mut dirs_ = vec![home.join(".local").join("share").join("cursor-agent")];
if cfg!(windows) {
if let Some(local) = dirs::data_local_dir() {
dirs_.push(local.join("cursor-agent"));
}
}
dirs_
}
pub(crate) fn cli_on_path(path_var: Option<&std::ffi::OsStr>) -> bool {
on_path(path_var, "cursor-agent")
}
fn cli_installed(home: &Path, path_var: Option<&std::ffi::OsStr>) -> bool {
cli_on_path(path_var) || cli_install_dirs(home).iter().any(|path| path.exists())
}
pub fn enterprise_dir() -> Option<PathBuf> {
if cfg!(target_os = "macos") {
return Some(
PathBuf::from("/Library") .join("Application Support")
.join("Cursor"),
);
}
if cfg!(target_os = "linux") {
return Some(PathBuf::from("/etc").join("cursor")); }
if cfg!(windows) {
return std::env::var_os("ProgramData")
.filter(|v| !v.is_empty())
.map(|v| PathBuf::from(v).join("Cursor"));
}
None
}
fn on_path(path_var: Option<&std::ffi::OsStr>, name: &str) -> bool {
crate::hooks::opencode::find_on_path(path_var, name).is_some()
}
#[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 ide_user_settings_honours_the_seam_and_the_machine_gate() {
let seam = PathBuf::from("/sandbox/cursor-user-data");
let config = PathBuf::from("/home/dev/.config");
let settings = |base: &Path| base.join("User").join("settings.json");
assert_eq!(
ide_user_settings_in(None, Some(config.clone()), true),
Some(settings(&config.join("Cursor")))
);
assert_eq!(
ide_user_settings_in(Some(seam.clone()), Some(config.clone()), true),
Some(settings(&seam)),
"the seam replaces the per-OS base"
);
assert_eq!(
ide_user_settings_in(Some(seam), Some(config.clone()), false),
None
);
assert_eq!(ide_user_settings_in(None, Some(config), false), None);
assert_eq!(ide_user_settings_in(None, None, true), None);
let _lock = seam_lock();
let dir = tempfile::tempdir().expect("temp dir");
{
let _env = EnvOverride::apply([(IDE_DIR_ENV, Some(dir.path().as_os_str().to_owned()))]);
assert_eq!(relocated_ide_dir().as_deref(), Some(dir.path()));
}
let _env = EnvOverride::apply([(IDE_DIR_ENV, Some(std::ffi::OsString::new()))]);
assert_eq!(relocated_ide_dir(), None);
}
#[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)));
}
#[cfg(feature = "model-relay")]
mod agent_host {
use super::*;
use crate::hooks::cline_app::process::ExeProcess;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const WIN_EXE: &str = r"C:\Users\olAdmin\AppData\Local\Programs\cursor\Cursor.exe";
fn at(secs: u64) -> Option<SystemTime> {
UNIX_EPOCH.checked_add(Duration::from_secs(secs))
}
fn row(exe: &str, line: &str, secs: u64) -> ExeProcess {
ExeProcess {
exe: PathBuf::from(exe),
args: vec![line.to_string()],
started: at(secs),
}
}
fn windows_rows() -> Vec<ExeProcess> {
let utility = |extra: &str| {
format!(
"\"{WIN_EXE}\" --type=utility --utility-sub-type=node.mojom.NodeService \
--lang=en-US --service-sandbox-type=none {extra}--user-data-dir=\"C:\\Users\\olAdmin\\AppData\\Roaming\\Cursor\""
)
};
vec![
row(WIN_EXE, &format!("\"{WIN_EXE}\" "), 100),
row(WIN_EXE, &format!("\"{WIN_EXE}\" --type=renderer --user-data-dir=x"), 101),
row(WIN_EXE, &utility("--video-capture-use-gpu-memory-buffer "), 102),
row(
WIN_EXE,
&utility("--dns-result-order=ipv4first --experimental-network-inspection --inspect-port=0 "),
103,
),
row(WIN_EXE, &utility(""), 104),
row(
WIN_EXE,
&format!("{WIN_EXE} --dns-result-order=ipv4first --experimental-network-inspection c:\\x\\gitWorker.js"),
110,
),
row(WIN_EXE, &utility(""), 111),
]
}
#[test]
fn windows_extension_host_is_the_inspect_port_utility() {
assert_eq!(newest_agent_host_start_in(&windows_rows()), at(103));
}
#[test]
fn macos_plugin_helper_is_an_extension_host() {
let rows = vec![
row("/Applications/Cursor.app/Contents/MacOS/Cursor", "Cursor", 100),
row(
"/Applications/Cursor.app/Contents/Frameworks/Cursor Helper (Renderer).app/Contents/MacOS/Cursor Helper (Renderer)",
"--type=renderer",
300,
),
row(
"/Applications/Cursor.app/Contents/Frameworks/Cursor Helper (Plugin).app/Contents/MacOS/Cursor Helper (Plugin)",
"--type=utility",
200,
),
row(
"/Applications/Cursor.app/Contents/Frameworks/Cursor Helper.app/Contents/MacOS/Cursor Helper",
"--type=utility --utility-sub-type=node.mojom.NodeService",
400,
),
];
assert_eq!(newest_agent_host_start_in(&rows), at(200));
}
#[test]
fn linux_extension_host_and_the_newest_wins() {
let exe = "/usr/share/cursor/cursor";
let host = |secs| {
row(
exe,
"/usr/share/cursor/cursor --type=utility --utility-sub-type=node.mojom.NodeService --inspect-port=0",
secs,
)
};
let rows = vec![
host(50),
host(70),
row(exe, "/usr/share/cursor/cursor --type=zygote", 90),
];
assert_eq!(newest_agent_host_start_in(&rows), at(70));
}
#[test]
fn no_extension_host_and_an_unreadable_start_answer_none() {
let mut rows = windows_rows();
rows.retain(|p| !is_agent_host(p));
assert_eq!(newest_agent_host_start_in(&rows), None);
let mut unreadable = windows_rows();
for p in &mut unreadable {
p.started = None;
}
assert_eq!(newest_agent_host_start_in(&unreadable), None);
}
#[test]
fn cursor_agent_is_never_an_ide_root() {
let home = Path::new("/home/dev");
let roots = ide_roots(home);
for cli in cli_install_dirs(home) {
assert!(
roots
.iter()
.all(|r| !cli.starts_with(r) && !r.starts_with(&cli)),
"{} overlaps an IDE root",
cli.display()
);
}
}
}
}