1use std::process::Command;
5
6use crate::utils::{run_command_timeout, NPM_INSTALL_TIMEOUT_SECS};
7
8const OUTDATED_TIMEOUT_SECS: u64 = 30;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Backend {
13 Bun,
14 Npm,
15}
16
17pub fn bun_available() -> bool {
19 Command::new("bun")
20 .arg("--version")
21 .stdout(std::process::Stdio::null())
22 .stderr(std::process::Stdio::null())
23 .status()
24 .map(|s| s.success())
25 .unwrap_or(false)
26}
27
28pub fn resolve_backend(explicit: Option<Backend>) -> Backend {
30 match explicit {
31 Some(Backend::Bun) => {
32 if bun_available() {
33 Backend::Bun
34 } else {
35 Backend::Npm
36 }
37 }
38 Some(Backend::Npm) => Backend::Npm,
39 None => {
40 if bun_available() {
41 Backend::Bun
42 } else {
43 Backend::Npm
44 }
45 }
46 }
47}
48
49pub fn backend_install(
51 specs: &[&str],
52 backend: Backend,
53 lockfile_only: bool,
54 no_scripts: bool,
55) -> Result<(), String> {
56 if specs.is_empty() {
57 return Ok(());
58 }
59 match backend {
60 Backend::Bun => {
61 let mut args = vec!["add"];
62 if lockfile_only {
63 args.push("--lockfile-only");
64 }
65 if no_scripts {
66 args.push("--ignore-scripts");
67 }
68 for s in specs {
69 args.push(s);
70 }
71 let out = run_command_timeout("bun", &args, NPM_INSTALL_TIMEOUT_SECS)
72 .map_err(|e| format!("bun add: {}", e))?;
73 if !out.status.success() {
74 let stderr = String::from_utf8_lossy(&out.stderr);
75 return Err(format!("bun add failed: {}", stderr));
76 }
77 Ok(())
78 }
79 Backend::Npm => {
80 let mut args = vec!["install"];
81 if lockfile_only {
82 args.push("--package-lock-only");
83 }
84 if no_scripts {
85 args.push("--ignore-scripts");
86 }
87 for s in specs {
88 args.push(s);
89 }
90 let out = run_command_timeout("npm", &args, NPM_INSTALL_TIMEOUT_SECS)
91 .map_err(|e| format!("npm install: {}", e))?;
92 if !out.status.success() {
93 let stderr = String::from_utf8_lossy(&out.stderr);
94 return Err(format!("npm install failed: {}", stderr));
95 }
96 Ok(())
97 }
98 }
99}
100
101pub fn backend_install_from_package_json(
103 backend: Backend,
104 lockfile_only: bool,
105 no_scripts: bool,
106) -> Result<(), String> {
107 match backend {
108 Backend::Bun => {
109 let mut args = vec!["install"];
110 if lockfile_only {
111 args.push("--lockfile-only");
112 }
113 if no_scripts {
114 args.push("--ignore-scripts");
115 }
116 let out = run_command_timeout("bun", &args, NPM_INSTALL_TIMEOUT_SECS)
117 .map_err(|e| format!("bun install: {}", e))?;
118 if !out.status.success() {
119 let stderr = String::from_utf8_lossy(&out.stderr);
120 return Err(format!("bun install failed: {}", stderr));
121 }
122 Ok(())
123 }
124 Backend::Npm => {
125 let mut args = vec!["install"];
126 if lockfile_only {
127 args.push("--package-lock-only");
128 }
129 if no_scripts {
130 args.push("--ignore-scripts");
131 }
132 let out = run_command_timeout("npm", &args, NPM_INSTALL_TIMEOUT_SECS)
133 .map_err(|e| format!("npm install: {}", e))?;
134 if !out.status.success() {
135 let stderr = String::from_utf8_lossy(&out.stderr);
136 return Err(format!("npm install failed: {}", stderr));
137 }
138 Ok(())
139 }
140 }
141}
142
143pub fn backend_outdated_json(backend: Backend) -> Option<serde_json::Value> {
146 match backend {
147 Backend::Npm => {
148 let out = run_command_timeout("npm", &["outdated", "--json"], OUTDATED_TIMEOUT_SECS).ok()?;
149 let s = String::from_utf8_lossy(&out.stdout);
150 serde_json::from_str(&s).ok()
151 }
152 Backend::Bun => {
153 let out = run_command_timeout("npm", &["outdated", "--json"], OUTDATED_TIMEOUT_SECS).ok()?;
156 let s = String::from_utf8_lossy(&out.stdout);
157 serde_json::from_str(&s).ok()
158 }
159 }
160}
161
162pub fn backend_fix_packages(packages: &[String], backend: Backend, quiet: bool) -> Result<(), String> {
164 if packages.is_empty() {
165 return Ok(());
166 }
167 let specs: Vec<String> = packages.iter().map(|p| format!("{}@latest", p)).collect();
168 let refs: Vec<&str> = specs.iter().map(String::as_str).collect();
169 if !quiet {
170 println!("Updating {} package(s) via {}...", packages.len(), backend_name(backend));
171 }
172 backend_install(&refs, backend, false, true)
173}
174
175fn backend_name(b: Backend) -> &'static str {
176 match b {
177 Backend::Bun => "bun",
178 Backend::Npm => "npm",
179 }
180}
181
182const AUDIT_TIMEOUT_SECS: u64 = 60;
183
184pub fn backend_audit(backend: Backend) -> Result<Vec<u8>, String> {
187 match backend {
188 Backend::Bun => {
189 let out = run_command_timeout("bun", &["audit", "--json"], AUDIT_TIMEOUT_SECS)
190 .map_err(|e| format!("bun audit: {}", e))?;
191 Ok(out.stdout)
192 }
193 Backend::Npm => {
194 let out = run_command_timeout("npm", &["audit", "--json"], AUDIT_TIMEOUT_SECS)
195 .map_err(|e| format!("npm audit: {}", e))?;
196 Ok(out.stdout)
197 }
198 }
199}
200
201pub fn backend_audit_fix(backend: Backend) -> Result<(), String> {
203 match backend {
204 Backend::Bun => {
205 let out = run_command_timeout("bun", &["audit", "fix"], AUDIT_TIMEOUT_SECS)
206 .map_err(|e| format!("bun audit fix: {}", e))?;
207 if !out.status.success() {
208 let stderr = String::from_utf8_lossy(&out.stderr);
209 return Err(format!("bun audit fix failed: {}", stderr));
210 }
211 Ok(())
212 }
213 Backend::Npm => {
214 let out = run_command_timeout("npm", &["audit", "fix"], AUDIT_TIMEOUT_SECS)
215 .map_err(|e| format!("npm audit fix: {}", e))?;
216 if !out.status.success() {
217 let stderr = String::from_utf8_lossy(&out.stderr);
218 return Err(format!("npm audit fix failed: {}", stderr));
219 }
220 Ok(())
221 }
222 }
223}
224
225pub fn backend_install_tarballs(
227 paths: &[std::path::PathBuf],
228 backend: Backend,
229 no_scripts: bool,
230) -> Result<(), String> {
231 if paths.is_empty() {
232 return Ok(());
233 }
234 let path_strs: Vec<String> = paths
235 .iter()
236 .map(|p| p.to_string_lossy().into_owned())
237 .collect();
238 let refs: Vec<&str> = path_strs.iter().map(String::as_str).collect();
239 match backend {
240 Backend::Bun => {
241 let mut args = vec!["add"];
242 if no_scripts {
243 args.push("--ignore-scripts");
244 }
245 args.extend(refs);
246 let out = run_command_timeout("bun", &args, NPM_INSTALL_TIMEOUT_SECS)
247 .map_err(|e| format!("bun add (cache): {}", e))?;
248 if !out.status.success() {
249 let stderr = String::from_utf8_lossy(&out.stderr);
250 return Err(format!("bun add failed: {}", stderr));
251 }
252 Ok(())
253 }
254 Backend::Npm => {
255 let mut args = vec!["install"];
256 if no_scripts {
257 args.push("--ignore-scripts");
258 }
259 args.extend(refs);
260 let out = run_command_timeout("npm", &args, NPM_INSTALL_TIMEOUT_SECS)
261 .map_err(|e| format!("npm install (cache): {}", e))?;
262 if !out.status.success() {
263 let stderr = String::from_utf8_lossy(&out.stderr);
264 return Err(format!("npm install failed: {}", stderr));
265 }
266 Ok(())
267 }
268 }
269}