npm_run_scripts/
cli.rs

1//! CLI argument definitions for nrs.
2//!
3//! Uses clap with derive macros for argument parsing.
4//!
5//! # Example
6//!
7//! ```no_run
8//! use npm_run_scripts::cli::Cli;
9//!
10//! let cli = Cli::parse_args();
11//! println!("Project dir: {:?}", cli.project_dir());
12//! ```
13
14use std::path::PathBuf;
15
16use clap::{CommandFactory, Parser, ValueEnum};
17use clap_complete::{generate, Shell};
18
19use crate::config::SortMode;
20use crate::package::Runner;
21
22/// Fast interactive TUI for running npm scripts.
23#[derive(Parser, Debug)]
24#[command(name = "nrs")]
25#[command(author, version, about, long_about = None)]
26#[command(arg_required_else_help = false)]
27pub struct Cli {
28    /// Path to project directory (default: current directory)
29    #[arg(value_name = "PATH")]
30    pub path: Option<PathBuf>,
31
32    /// Rerun last executed script (no TUI)
33    #[arg(short = 'L', long = "last")]
34    pub last: bool,
35
36    /// List scripts non-interactively (no TUI)
37    #[arg(short, long)]
38    pub list: bool,
39
40    /// Exclude scripts matching pattern (can be repeated)
41    #[arg(short, long, value_name = "PATTERN")]
42    pub exclude: Vec<String>,
43
44    /// Initial sort mode
45    #[arg(short, long, value_name = "MODE", value_enum)]
46    pub sort: Option<CliSortMode>,
47
48    /// Override package manager
49    #[arg(short, long, value_name = "RUNNER", value_enum)]
50    pub runner: Option<CliRunner>,
51
52    /// Arguments to pass to the selected script
53    #[arg(short, long, value_name = "ARGS", allow_hyphen_values = true)]
54    pub args: Option<String>,
55
56    /// Run script directly without TUI
57    #[arg(short = 'n', long = "script", value_name = "NAME")]
58    pub script: Option<String>,
59
60    /// Show command without executing
61    #[arg(short, long)]
62    pub dry_run: bool,
63
64    /// Path to config file
65    #[arg(short, long, value_name = "PATH")]
66    pub config: Option<PathBuf>,
67
68    /// Ignore config files
69    #[arg(long)]
70    pub no_config: bool,
71
72    /// Enable debug output
73    #[arg(long)]
74    pub debug: bool,
75
76    /// Generate shell completions
77    #[arg(long, value_name = "SHELL", value_enum)]
78    pub completions: Option<CliShell>,
79}
80
81/// Shell type for completion generation.
82#[derive(Debug, Clone, Copy, ValueEnum)]
83pub enum CliShell {
84    /// Bash shell
85    Bash,
86    /// Zsh shell
87    Zsh,
88    /// Fish shell
89    Fish,
90    /// PowerShell
91    Powershell,
92    /// Elvish shell
93    Elvish,
94}
95
96/// Sort mode for CLI parsing.
97#[derive(Debug, Clone, Copy, ValueEnum)]
98pub enum CliSortMode {
99    /// Sort by most recently used.
100    Recent,
101    /// Sort alphabetically.
102    Alpha,
103    /// Group by category/prefix.
104    Category,
105}
106
107impl From<CliSortMode> for SortMode {
108    fn from(mode: CliSortMode) -> Self {
109        match mode {
110            CliSortMode::Recent => SortMode::Recent,
111            CliSortMode::Alpha => SortMode::Alpha,
112            CliSortMode::Category => SortMode::Category,
113        }
114    }
115}
116
117/// Package manager for CLI parsing.
118#[derive(Debug, Clone, Copy, ValueEnum)]
119pub enum CliRunner {
120    Npm,
121    Yarn,
122    Pnpm,
123    Bun,
124}
125
126impl From<CliRunner> for Runner {
127    fn from(runner: CliRunner) -> Self {
128        match runner {
129            CliRunner::Npm => Runner::Npm,
130            CliRunner::Yarn => Runner::Yarn,
131            CliRunner::Pnpm => Runner::Pnpm,
132            CliRunner::Bun => Runner::Bun,
133        }
134    }
135}
136
137impl Cli {
138    /// Parse command line arguments.
139    pub fn parse_args() -> Self {
140        Cli::parse()
141    }
142
143    /// Get the project directory.
144    ///
145    /// Returns the provided path or the current directory.
146    pub fn project_dir(&self) -> PathBuf {
147        self.path
148            .clone()
149            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
150    }
151
152    /// Check if TUI should be shown.
153    pub fn should_show_tui(&self) -> bool {
154        !self.list && !self.last && self.script.is_none()
155    }
156
157    /// Get the sort mode.
158    pub fn sort_mode(&self) -> Option<SortMode> {
159        self.sort.map(Into::into)
160    }
161
162    /// Get the runner override.
163    pub fn runner_override(&self) -> Option<Runner> {
164        self.runner.map(Into::into)
165    }
166
167    /// Generate shell completions and write to stdout.
168    pub fn generate_completions(shell: CliShell) {
169        let mut cmd = Cli::command();
170        let shell = match shell {
171            CliShell::Bash => Shell::Bash,
172            CliShell::Zsh => Shell::Zsh,
173            CliShell::Fish => Shell::Fish,
174            CliShell::Powershell => Shell::PowerShell,
175            CliShell::Elvish => Shell::Elvish,
176        };
177        generate(shell, &mut cmd, "nrs", &mut std::io::stdout());
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_default_project_dir() {
187        let cli = Cli {
188            path: None,
189            last: false,
190            list: false,
191            exclude: vec![],
192            sort: None,
193            runner: None,
194            args: None,
195            script: None,
196            dry_run: false,
197            config: None,
198            no_config: false,
199            debug: false,
200            completions: None,
201        };
202
203        // Should return current directory
204        assert!(cli.project_dir().is_absolute() || cli.project_dir() == PathBuf::from("."));
205    }
206
207    #[test]
208    fn test_should_show_tui() {
209        let mut cli = Cli {
210            path: None,
211            last: false,
212            list: false,
213            exclude: vec![],
214            sort: None,
215            runner: None,
216            args: None,
217            script: None,
218            dry_run: false,
219            config: None,
220            no_config: false,
221            debug: false,
222            completions: None,
223        };
224
225        assert!(cli.should_show_tui());
226
227        cli.list = true;
228        assert!(!cli.should_show_tui());
229
230        cli.list = false;
231        cli.last = true;
232        assert!(!cli.should_show_tui());
233
234        cli.last = false;
235        cli.script = Some("dev".to_string());
236        assert!(!cli.should_show_tui());
237    }
238}