1use std::io::{self, Write};
7use std::path::Path;
8use std::process::{Command, ExitStatus};
9
10use anyhow::{Context, Result};
11
12use crate::package::{Runner, Script};
13
14pub const EXIT_CODE_INTERRUPTED: i32 = 130;
17
18#[derive(Debug)]
20pub struct ExecutionResult {
21 pub status: ExitStatus,
23 pub command: String,
25}
26
27impl ExecutionResult {
28 pub fn success(&self) -> bool {
30 self.status.success()
31 }
32
33 pub fn code(&self) -> Option<i32> {
35 self.status.code()
36 }
37}
38
39pub fn run_script(
57 runner: Runner,
58 script: &Script,
59 args: Option<&str>,
60 dry_run: bool,
61) -> Result<i32> {
62 let args_vec: Vec<String> = args
63 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
64 .unwrap_or_default();
65
66 let project_dir = std::env::current_dir().context("Failed to get current directory")?;
67
68 execute_script(runner, script.name(), &args_vec, &project_dir, dry_run)
69 .map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
70}
71
72pub fn run_script_in_dir(
91 runner: Runner,
92 script: &Script,
93 args: Option<&str>,
94 project_dir: &Path,
95 dry_run: bool,
96) -> Result<i32> {
97 let args_vec: Vec<String> = args
98 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
99 .unwrap_or_default();
100
101 execute_script(runner, script.name(), &args_vec, project_dir, dry_run)
102 .map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
103}
104
105pub fn run_scripts(
126 runner: Runner,
127 scripts: &[(&Script, Option<String>)],
128 dry_run: bool,
129) -> Result<Vec<i32>> {
130 let total = scripts.len();
131 let mut results = Vec::with_capacity(total);
132 let project_dir = std::env::current_dir().context("Failed to get current directory")?;
133
134 for (i, (script, args)) in scripts.iter().enumerate() {
135 println!(
137 "\n\x1b[1;36mRunning {}/{}: {}...\x1b[0m",
138 i + 1,
139 total,
140 script.name()
141 );
142 io::stdout().flush().ok();
143
144 let args_vec: Vec<String> = args
145 .as_ref()
146 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
147 .unwrap_or_default();
148
149 let result = execute_script(runner, script.name(), &args_vec, &project_dir, dry_run)?;
150 let exit_code = result.code().unwrap_or(EXIT_CODE_INTERRUPTED);
151 results.push(exit_code);
152
153 if exit_code != 0 {
155 println!(
156 "\n\x1b[1;31mScript '{}' failed with exit code {}\x1b[0m",
157 script.name(),
158 exit_code
159 );
160 break;
161 }
162 }
163
164 Ok(results)
165}
166
167pub fn run_scripts_in_dir(
189 runner: Runner,
190 scripts: &[(&Script, Option<String>)],
191 project_dir: &Path,
192 dry_run: bool,
193) -> Result<Vec<i32>> {
194 let total = scripts.len();
195 let mut results = Vec::with_capacity(total);
196
197 for (i, (script, args)) in scripts.iter().enumerate() {
198 println!(
200 "\n\x1b[1;36mRunning {}/{}: {}...\x1b[0m",
201 i + 1,
202 total,
203 script.name()
204 );
205 io::stdout().flush().ok();
206
207 let args_vec: Vec<String> = args
208 .as_ref()
209 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
210 .unwrap_or_default();
211
212 let result = execute_script(runner, script.name(), &args_vec, project_dir, dry_run)?;
213 let exit_code = result.code().unwrap_or(EXIT_CODE_INTERRUPTED);
214 results.push(exit_code);
215
216 if exit_code != 0 {
218 println!(
219 "\n\x1b[1;31mScript '{}' failed with exit code {}\x1b[0m",
220 script.name(),
221 exit_code
222 );
223 break;
224 }
225 }
226
227 Ok(results)
228}
229
230pub fn execute_script(
246 runner: Runner,
247 script: &str,
248 args: &[String],
249 project_dir: &Path,
250 dry_run: bool,
251) -> Result<ExecutionResult> {
252 let cmd_parts = runner.run_command_with_args(script, args);
253 let command_str = cmd_parts.join(" ");
254
255 if dry_run {
256 println!("Would run: {command_str}");
257 return Ok(ExecutionResult {
258 status: std::process::ExitStatus::default(),
259 command: command_str,
260 });
261 }
262
263 let mut command = Command::new(&cmd_parts[0]);
264 command.args(&cmd_parts[1..]);
265 command.current_dir(project_dir);
266
267 command.stdin(std::process::Stdio::inherit());
269 command.stdout(std::process::Stdio::inherit());
270 command.stderr(std::process::Stdio::inherit());
271
272 let status = command
273 .status()
274 .with_context(|| format!("Failed to execute: {command_str}"))?;
275
276 Ok(ExecutionResult {
277 status,
278 command: command_str,
279 })
280}
281
282pub fn format_dry_run_command(runner: Runner, script: &str, args: Option<&str>) -> String {
284 let args_vec: Vec<String> = args
285 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
286 .unwrap_or_default();
287
288 let cmd = runner.run_command_with_args(script, &args_vec);
289 format!("Would run: {}", cmd.join(" "))
290}
291
292pub fn execute_workspace_script(
309 runner: Runner,
310 workspace: &str,
311 script: &str,
312 args: &[String],
313 project_dir: &Path,
314 dry_run: bool,
315) -> Result<ExecutionResult> {
316 let cmd_parts = runner.workspace_command_with_args(workspace, script, args);
317 let command_str = cmd_parts.join(" ");
318
319 if dry_run {
320 println!("Would run: {command_str}");
321 return Ok(ExecutionResult {
322 status: std::process::ExitStatus::default(),
323 command: command_str,
324 });
325 }
326
327 let mut command = Command::new(&cmd_parts[0]);
328 command.args(&cmd_parts[1..]);
329 command.current_dir(project_dir);
330
331 command.stdin(std::process::Stdio::inherit());
333 command.stdout(std::process::Stdio::inherit());
334 command.stderr(std::process::Stdio::inherit());
335
336 let status = command
337 .status()
338 .with_context(|| format!("Failed to execute: {command_str}"))?;
339
340 Ok(ExecutionResult {
341 status,
342 command: command_str,
343 })
344}
345
346pub fn run_workspace_script(
361 runner: Runner,
362 workspace: &str,
363 script: &Script,
364 args: Option<&str>,
365 project_dir: &Path,
366 dry_run: bool,
367) -> Result<i32> {
368 let args_vec: Vec<String> = args
369 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
370 .unwrap_or_default();
371
372 execute_workspace_script(
373 runner,
374 workspace,
375 script.name(),
376 &args_vec,
377 project_dir,
378 dry_run,
379 )
380 .map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
381}
382
383pub fn format_workspace_dry_run_command(
385 runner: Runner,
386 workspace: &str,
387 script: &str,
388 args: Option<&str>,
389) -> String {
390 let args_vec: Vec<String> = args
391 .map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
392 .unwrap_or_default();
393
394 let cmd = runner.workspace_command_with_args(workspace, script, &args_vec);
395 format!("Would run: {}", cmd.join(" "))
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_dry_run() {
404 let result = execute_script(Runner::Npm, "test", &[], Path::new("."), true).unwrap();
405
406 assert_eq!(result.command, "npm run test");
407 }
408
409 #[test]
410 fn test_dry_run_with_args() {
411 let args = vec!["--watch".to_string(), "--coverage".to_string()];
412 let result = execute_script(Runner::Npm, "test", &args, Path::new("."), true).unwrap();
413
414 assert_eq!(result.command, "npm run test -- --watch --coverage");
415 }
416
417 #[test]
418 fn test_format_dry_run_command() {
419 assert_eq!(
420 format_dry_run_command(Runner::Npm, "dev", None),
421 "Would run: npm run dev"
422 );
423
424 assert_eq!(
425 format_dry_run_command(Runner::Npm, "dev", Some("--host")),
426 "Would run: npm run dev -- --host"
427 );
428
429 assert_eq!(
430 format_dry_run_command(Runner::Yarn, "dev", Some("--host")),
431 "Would run: yarn dev --host"
432 );
433
434 assert_eq!(
435 format_dry_run_command(Runner::Pnpm, "dev", Some("--host")),
436 "Would run: pnpm dev -- --host"
437 );
438
439 assert_eq!(
440 format_dry_run_command(Runner::Bun, "dev", Some("--host")),
441 "Would run: bun run dev --host"
442 );
443 }
444
445 #[test]
446 fn test_run_script_dry_run() {
447 let script = Script::new("dev", "vite");
448 let result = run_script(Runner::Npm, &script, None, true).unwrap();
449 assert_eq!(result, 0);
450 }
451
452 #[test]
453 fn test_run_script_with_args_dry_run() {
454 let script = Script::new("dev", "vite");
455 let result = run_script(Runner::Npm, &script, Some("--host"), true).unwrap();
456 assert_eq!(result, 0);
457 }
458
459 #[test]
460 fn test_run_scripts_dry_run() {
461 let script1 = Script::new("build", "vite build");
462 let script2 = Script::new("test", "vitest");
463 let scripts: Vec<(&Script, Option<String>)> =
464 vec![(&script1, None), (&script2, Some("--coverage".to_string()))];
465
466 let results = run_scripts(Runner::Npm, &scripts, true).unwrap();
467 assert_eq!(results, vec![0, 0]);
468 }
469}