1use crate::commands::{init::init_script_file, script::run_script, Commands, script::Scripts, show::show_scripts, completions::generate_completions, validate::{validate_scripts, print_validation_results}};
5use crate::error::CargoScriptError;
6use std::fs;
7use clap::{Parser, CommandFactory, ArgAction};
8use colored::*;
9
10#[derive(Parser, Debug)]
12#[command(
13 name = "cargo-script",
14 about = "A powerful CLI tool for managing project scripts in Rust",
15 long_about = "Think npm scripts, make, or just — but built specifically for the Rust ecosystem with modern CLI best practices.",
16 after_help = "EXAMPLES:\n cargo script build Run the 'build' script\n cargo script run test Explicitly run the 'test' script\n cargo script test --env RUST_LOG=debug Run with environment variable\n cargo script test --dry-run Preview what would run\n cargo script test --no-metrics Run without performance metrics\n cargo script --interactive Interactive script selection\n cargo script show List all available scripts\n cargo script show --filter test Filter scripts by name/description\n cargo script init Initialize Scripts.toml\n cargo script validate Validate Scripts.toml\n\nFor more information, visit: https://github.com/rsaz/cargo-script",
17 version,
18 subcommand_required = false,
19 arg_required_else_help = false,
20)]
21pub struct Cli {
22 #[command(subcommand)]
23 command: Option<Commands>,
24 #[arg(value_name = "SCRIPT_NAME")]
26 script_name: Option<String>,
27 #[arg(long, default_value = "Scripts.toml", global = true)]
29 scripts_path: String,
30 #[arg(short, long, global = true)]
32 quiet: bool,
33 #[arg(short = 'v', long, global = true)]
35 verbose: bool,
36 #[arg(short, long, value_name = "KEY=VALUE", action = ArgAction::Append, global = true)]
38 env: Vec<String>,
39 #[arg(long, global = true)]
41 dry_run: bool,
42 #[arg(long, global = true)]
44 no_metrics: bool,
45 #[arg(short, long, global = true)]
47 interactive: bool,
48}
49
50pub fn run_with_error_handling() {
52 if let Err(e) = run() {
53 eprintln!("{}", e);
54 std::process::exit(1);
55 }
56}
57
58pub fn run() -> Result<(), CargoScriptError> {
67 let args: Vec<String> = std::env::args().collect();
71 let cli = if args.len() > 1 && args[1] == "script" {
72 let mut cargo_args = vec![args[0].clone()]; cargo_args.extend(args.into_iter().skip(2)); if cargo_args.len() == 1 {
79 return handle_show_command("Scripts.toml", false, false, None);
81 }
82
83 if cargo_args.len() == 2 && (cargo_args[1] == "--help" || cargo_args[1] == "-h") {
85 let mut app = Cli::command();
86 app.print_help().unwrap();
87 std::process::exit(0);
88 }
89
90 Cli::try_parse_from(cargo_args).unwrap_or_else(|e| {
91 e.exit()
93 })
94 } else {
95 Cli::parse()
97 };
98
99 let command = match (&cli.command, &cli.script_name) {
104 (Some(_cmd), Some(_)) => {
105 eprintln!("{}", "Error: Cannot specify both a subcommand and a script name".red().bold());
107 eprintln!("{}", "Use either 'cargo script <script_name>' or 'cargo script <subcommand>'".white());
108 std::process::exit(1);
109 }
110 (Some(cmd), None) => cmd.clone(),
111 (None, Some(script_name)) => {
112 Commands::Run {
114 script: Some(script_name.clone()),
115 env: cli.env.clone(),
116 dry_run: cli.dry_run,
117 quiet: false, verbose: false, no_metrics: cli.no_metrics,
120 interactive: cli.interactive,
121 }
122 }
123 (None, None) => {
124 if cli.interactive {
127 Commands::Run {
128 script: None,
129 env: cli.env.clone(),
130 dry_run: cli.dry_run,
131 quiet: false,
132 verbose: false,
133 no_metrics: cli.no_metrics,
134 interactive: true,
135 }
136 } else {
137 Commands::Show {
138 quiet: cli.quiet,
139 verbose: cli.verbose,
140 filter: None,
141 }
142 }
143 }
144 };
145
146 let should_show_banner = !cli.quiet
154 && !matches!(&command, Commands::Completions { .. })
155 && !matches!(&command, Commands::Run { dry_run: true, .. })
156 && (cli.verbose
157 || !std::path::Path::new(&cli.scripts_path).exists()
158 || matches!(&command, Commands::Init | Commands::Show { .. } | Commands::Validate { .. }));
159
160 if should_show_banner {
161 let init_msg = format!("A CLI tool to run custom scripts in Rust, defined in [ Scripts.toml ] {}", emoji::objects::computer::FLOPPY_DISK.glyph);
162 print_framed_message(&init_msg);
163 }
164
165 let scripts_path = &cli.scripts_path;
166
167 match &command {
168 Commands::Run { script, env, dry_run, quiet, verbose, no_metrics, interactive } => {
169 let final_quiet = cli.quiet || *quiet;
173 let final_verbose = cli.verbose || *verbose;
174 let final_dry_run = cli.dry_run || *dry_run;
175 let final_no_metrics = cli.no_metrics || *no_metrics;
176 let final_env = if !env.is_empty() { env.clone() } else { cli.env.clone() };
177 let show_metrics = !final_no_metrics; let scripts_content = fs::read_to_string(scripts_path)
180 .map_err(|e| CargoScriptError::ScriptFileNotFound {
181 path: scripts_path.clone(),
182 source: e,
183 })?;
184
185 let scripts: Scripts = toml::from_str(&scripts_content)
186 .map_err(|e| {
187 let message = e.message().to_string();
188 let line = e.span().map(|s| s.start);
189 CargoScriptError::InvalidToml {
190 path: scripts_path.clone(),
191 message,
192 line,
193 }
194 })?;
195
196 let script_name = if *interactive || script.is_none() {
198 crate::commands::script::interactive_select_script(&scripts, final_quiet)?
199 } else {
200 script.clone().ok_or_else(|| CargoScriptError::ScriptNotFound {
201 script_name: "".to_string(),
202 available_scripts: scripts.scripts.keys().cloned().collect(),
203 })?
204 };
205
206 run_script(&scripts, &script_name, final_env, final_dry_run, final_quiet, final_verbose, show_metrics)?;
207 }
208 Commands::Init => {
209 init_script_file(Some(scripts_path));
210 }
211 Commands::Show { quiet, verbose: _verbose, filter } => {
212 let final_quiet = cli.quiet || *quiet;
214 handle_show_command(scripts_path, final_quiet, cli.verbose, filter.as_deref())?;
215 }
216 Commands::Completions { shell } => {
217 let mut app = Cli::command();
218 generate_completions(shell.clone(), &mut app);
219 }
220 Commands::Validate { quiet, verbose: _verbose } => {
221 let final_quiet = cli.quiet || *quiet;
223
224 let scripts_content = fs::read_to_string(scripts_path)
225 .map_err(|e| CargoScriptError::ScriptFileNotFound {
226 path: scripts_path.clone(),
227 source: e,
228 })?;
229
230 let scripts: Scripts = toml::from_str(&scripts_content)
231 .map_err(|e| {
232 let message = e.message().to_string();
233 let line = e.span().map(|s| s.start);
234 CargoScriptError::InvalidToml {
235 path: scripts_path.clone(),
236 message,
237 line,
238 }
239 })?;
240
241 let validation_result = validate_scripts(&scripts);
242 if !final_quiet {
243 print_validation_results(&validation_result);
244 }
245
246 if !validation_result.is_valid() {
247 std::process::exit(1);
248 }
249 }
250 }
251
252 Ok(())
253}
254
255fn handle_show_command(scripts_path: &str, quiet: bool, _verbose: bool, filter: Option<&str>) -> Result<(), CargoScriptError> {
257 let scripts_content = fs::read_to_string(scripts_path)
258 .map_err(|e| CargoScriptError::ScriptFileNotFound {
259 path: scripts_path.to_string(),
260 source: e,
261 })?;
262
263 let scripts: Scripts = toml::from_str(&scripts_content)
264 .map_err(|e| {
265 let message = e.message().to_string();
266 let line = e.span().map(|s| s.start);
267 CargoScriptError::InvalidToml {
268 path: scripts_path.to_string(),
269 message,
270 line,
271 }
272 })?;
273
274 if !quiet {
275 show_scripts(&scripts, filter);
276 }
277
278 Ok(())
279}
280
281fn print_framed_message(message: &str) {
291 let framed_message = format!("| {} |", message);
292 let frame = "-".repeat(framed_message.len()-2);
293 println!("\n{}\n{}\n{}\n", frame.yellow(), framed_message.yellow(), frame.yellow());
294}