Skip to main content

socket_patch_core/utils/
global_packages.rs

1use std::path::PathBuf;
2use std::process::Command;
3
4// ---------------------------------------------------------------------------
5// Individual package manager global prefix helpers
6// ---------------------------------------------------------------------------
7
8/// Get the npm global `node_modules` path using `npm root -g`.
9pub fn get_npm_global_prefix() -> Result<String, String> {
10    let output = Command::new("npm")
11        .args(["root", "-g"])
12        .stdin(std::process::Stdio::null())
13        .stdout(std::process::Stdio::piped())
14        .stderr(std::process::Stdio::piped())
15        .output()
16        .map_err(|e| format!("Failed to run `npm root -g`: {e}"))?;
17
18    if !output.status.success() {
19        return Err(
20            "Failed to determine npm global prefix. Ensure npm is installed and in PATH."
21                .to_string(),
22        );
23    }
24
25    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
26    if path.is_empty() {
27        return Err("npm root -g returned empty output".to_string());
28    }
29
30    Ok(path)
31}
32
33/// Get the yarn global `node_modules` path via `yarn global dir`.
34pub fn get_yarn_global_prefix() -> Option<String> {
35    let output = Command::new("yarn")
36        .args(["global", "dir"])
37        .stdin(std::process::Stdio::null())
38        .stdout(std::process::Stdio::piped())
39        .stderr(std::process::Stdio::piped())
40        .output()
41        .ok()?;
42
43    if !output.status.success() {
44        return None;
45    }
46
47    let dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
48    if dir.is_empty() {
49        return None;
50    }
51
52    Some(
53        PathBuf::from(dir)
54            .join("node_modules")
55            .to_string_lossy()
56            .to_string(),
57    )
58}
59
60/// Get the pnpm global `node_modules` path via `pnpm root -g`.
61pub fn get_pnpm_global_prefix() -> Option<String> {
62    let output = Command::new("pnpm")
63        .args(["root", "-g"])
64        .stdin(std::process::Stdio::null())
65        .stdout(std::process::Stdio::piped())
66        .stderr(std::process::Stdio::piped())
67        .output()
68        .ok()?;
69
70    if !output.status.success() {
71        return None;
72    }
73
74    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
75    if path.is_empty() {
76        return None;
77    }
78
79    Some(path)
80}
81
82/// Get the bun global `node_modules` path via `bun pm bin -g`.
83pub fn get_bun_global_prefix() -> Option<String> {
84    let output = Command::new("bun")
85        .args(["pm", "bin", "-g"])
86        .stdin(std::process::Stdio::null())
87        .stdout(std::process::Stdio::piped())
88        .stderr(std::process::Stdio::piped())
89        .output()
90        .ok()?;
91
92    if !output.status.success() {
93        return None;
94    }
95
96    let bin_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
97    if bin_path.is_empty() {
98        return None;
99    }
100
101    let bun_root = PathBuf::from(&bin_path);
102    let parent = bun_root.parent()?;
103
104    Some(
105        parent
106            .join("install")
107            .join("global")
108            .join("node_modules")
109            .to_string_lossy()
110            .to_string(),
111    )
112}
113
114// ---------------------------------------------------------------------------
115// Aggregation helpers
116// ---------------------------------------------------------------------------
117
118/// Get the global `node_modules` path, with support for a custom override.
119///
120/// If `custom` is `Some`, that value is returned directly. Otherwise, falls
121/// back to `get_npm_global_prefix()`.
122pub fn get_global_prefix(custom: Option<&str>) -> Result<String, String> {
123    if let Some(custom_path) = custom {
124        return Ok(custom_path.to_string());
125    }
126    get_npm_global_prefix()
127}
128
129/// Get all global `node_modules` paths for package lookup.
130///
131/// Returns paths from all detected package managers (npm, pnpm, yarn, bun).
132/// If `custom` is provided, only that path is returned.
133pub fn get_global_node_modules_paths(custom: Option<&str>) -> Vec<String> {
134    if let Some(custom_path) = custom {
135        return vec![custom_path.to_string()];
136    }
137
138    let mut paths = Vec::new();
139
140    if let Ok(npm_path) = get_npm_global_prefix() {
141        paths.push(npm_path);
142    }
143
144    if let Some(pnpm_path) = get_pnpm_global_prefix() {
145        paths.push(pnpm_path);
146    }
147
148    if let Some(yarn_path) = get_yarn_global_prefix() {
149        paths.push(yarn_path);
150    }
151
152    if let Some(bun_path) = get_bun_global_prefix() {
153        paths.push(bun_path);
154    }
155
156    paths
157}
158
159/// Check if a path is within a global `node_modules` directory.
160pub fn is_global_path(pkg_path: &str) -> bool {
161    let paths = get_global_node_modules_paths(None);
162    let normalized = PathBuf::from(pkg_path);
163    let normalized_str = normalized.to_string_lossy();
164
165    paths.iter().any(|global_path| {
166        let gp = PathBuf::from(global_path);
167        normalized_str.starts_with(&*gp.to_string_lossy())
168    })
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_get_global_prefix_custom() {
177        let result = get_global_prefix(Some("/custom/node_modules"));
178        assert_eq!(result.unwrap(), "/custom/node_modules");
179    }
180
181    #[test]
182    fn test_get_global_node_modules_paths_custom() {
183        let paths = get_global_node_modules_paths(Some("/my/custom/path"));
184        assert_eq!(paths, vec!["/my/custom/path".to_string()]);
185    }
186}