sublime_cli_tools/cli/
mod.rs

1//! CLI framework module.
2//!
3//! This module defines the CLI structure, command parsing, and global options.
4//!
5//! # What
6//!
7//! Provides the core CLI framework including:
8//! - Command-line argument definitions using Clap
9//! - Global options (root, log-level, format, no-color, config)
10//! - Command enumeration and routing
11//! - Argument parsing and validation
12//!
13//! # How
14//!
15//! Uses Clap's derive macros to define a structured CLI with global options
16//! that apply to all commands and command-specific arguments. The framework
17//! separates concerns between:
18//! - CLI parsing (this module)
19//! - Command execution (commands module)
20//! - Output formatting (output module)
21//! - Error handling (error module)
22//!
23//! # Why
24//!
25//! Centralizes CLI definition for consistency, maintainability, and automatic
26//! help generation. Global options ensure consistent behavior across all commands.
27//!
28//! # Examples
29//!
30//! ```rust,no_run
31//! use clap::Parser;
32//! use sublime_cli_tools::cli::Cli;
33//!
34//! // Parse CLI arguments
35//! let cli = Cli::parse();
36//!
37//! // Access global options
38//! let format = cli.format;
39//! let log_level = cli.log_level();
40//! ```
41
42mod args;
43pub mod branding;
44pub mod commands;
45pub mod completions;
46mod dispatch;
47
48#[cfg(test)]
49mod tests;
50
51use clap::Parser;
52use std::path::PathBuf;
53
54pub use args::{LogLevel, OutputFormatArg};
55pub use commands::Commands;
56pub use completions::generate_completions;
57pub use dispatch::dispatch_command;
58
59use crate::output::OutputFormat;
60
61/// Workspace Tools - Changeset-based version management.
62///
63/// This CLI provides comprehensive tools for managing Node.js workspaces using
64/// a changeset-based workflow. It supports both single-package and monorepo
65/// projects with independent or unified versioning strategies.
66///
67/// # Global Options
68///
69/// All global options apply to ALL subcommands and control behavior across
70/// the entire application:
71///
72/// - `--root`: Changes working directory before executing commands
73/// - `--log-level`: Controls logging verbosity (stderr only)
74/// - `--format`: Controls output format (stdout only)
75/// - `--no-color`: Disables ANSI colors in output and logs
76/// - `--config`: Override default config file location
77///
78/// # Stream Separation
79///
80/// The CLI maintains strict separation between:
81/// - **stderr**: Logs only (controlled by `--log-level`)
82/// - **stdout**: Command output only (controlled by `--format`)
83///
84/// This ensures JSON output is never contaminated with logs, enabling
85/// reliable piping and parsing in scripts.
86///
87/// # Examples
88///
89/// ```bash
90/// # Initialize a new project
91/// workspace init
92///
93/// # Add a changeset
94/// workspace changeset add
95///
96/// # Preview version bump
97/// workspace bump --dry-run
98///
99/// # JSON output with no logs (clean JSON for automation)
100/// workspace --format json --log-level silent bump --dry-run
101///
102/// # Debug logging with text output
103/// workspace --log-level debug changeset list
104/// ```
105#[derive(Debug, Parser)]
106#[command(name = "workspace")]
107#[command(version = env!("CARGO_PKG_VERSION"))]
108#[command(about = "Workspace Tools - Changeset-based version management")]
109#[command(long_about = None)]
110#[command(author = "Sublime Labs")]
111#[command(help_template = "\
112{before-help}{name} {version}
113{about-with-newline}
114{usage-heading} {usage}
115
116{all-args}{after-help}
117")]
118pub struct Cli {
119    /// Subcommand to execute.
120    #[command(subcommand)]
121    pub command: Commands,
122
123    /// Project root directory.
124    ///
125    /// Changes working directory before executing the command.
126    /// All file operations will be relative to this path.
127    ///
128    /// Default: Current directory
129    #[arg(global = true, short = 'r', long, value_name = "PATH")]
130    pub root: Option<PathBuf>,
131
132    /// Logging level.
133    ///
134    /// Controls verbosity of operation logs written to stderr.
135    /// Does NOT affect command output (stdout).
136    ///
137    /// Levels:
138    /// - silent: No logs at all
139    /// - error: Only critical errors
140    /// - warn: Errors + warnings
141    /// - info: General progress (default)
142    /// - debug: Detailed operations
143    /// - trace: Very verbose debugging
144    ///
145    /// Default: info
146    #[arg(global = true, short = 'l', long, value_name = "LEVEL", default_value = "info")]
147    pub log_level: LogLevel,
148
149    /// Output format.
150    ///
151    /// Controls format of command output written to stdout.
152    /// Does NOT affect logging (stderr).
153    ///
154    /// Formats:
155    /// - human: Human-readable with colors and tables (default)
156    /// - json: Pretty-printed JSON
157    /// - json-compact: Compact JSON (single line)
158    /// - quiet: Minimal output
159    ///
160    /// Default: human
161    #[arg(global = true, short = 'f', long, value_name = "FORMAT", default_value = "human")]
162    pub format: OutputFormatArg,
163
164    /// Disable colored output.
165    ///
166    /// Removes ANSI color codes from both logs (stderr) and output (stdout).
167    /// Also respects the NO_COLOR environment variable.
168    ///
169    /// Useful for CI/CD environments and file redirection.
170    #[arg(global = true, long)]
171    pub no_color: bool,
172
173    /// Path to config file.
174    ///
175    /// Override default config file location.
176    /// Path can be relative or absolute.
177    ///
178    /// Default: Auto-detect (.changesets.{toml,json,yaml,yml})
179    #[arg(global = true, short = 'c', long, value_name = "PATH")]
180    pub config: Option<PathBuf>,
181
182    /// Quiet mode.
183    ///
184    /// Minimizes both logs (stderr) and output (stdout).
185    /// Equivalent to `--log-level silent --format quiet`.
186    ///
187    /// Useful for scripts and automation where only exit code matters.
188    /// Cannot be combined with --log-level, --format, or --verbose.
189    #[arg(global = true, short = 'q', long, conflicts_with_all = ["log_level", "format", "verbose"])]
190    pub quiet: bool,
191
192    /// Verbose output mode.
193    ///
194    /// Increases detail level in command output (stdout) and enables debug logs (stderr).
195    /// Equivalent to `--log-level debug`.
196    ///
197    /// Different from --log-level which only controls operational logs.
198    /// Cannot be combined with --log-level or --quiet.
199    #[arg(global = true, short = 'v', long, conflicts_with_all = ["log_level", "quiet"])]
200    pub verbose: bool,
201}
202
203impl Cli {
204    /// Returns the raw log level from command line.
205    ///
206    /// Note: Prefer `effective_log_level()` which accounts for --quiet and --verbose flags.
207    ///
208    /// # Examples
209    ///
210    /// ```rust
211    /// use clap::Parser;
212    /// use sublime_cli_tools::cli::{Cli, LogLevel};
213    ///
214    /// let cli = Cli::parse_from(["workspace", "--log-level", "debug", "version"]);
215    /// assert_eq!(cli.log_level(), LogLevel::Debug);
216    /// ```
217    #[must_use]
218    pub const fn log_level(&self) -> LogLevel {
219        self.log_level
220    }
221
222    /// Returns the effective log level, accounting for --quiet and --verbose flags.
223    ///
224    /// Priority:
225    /// 1. --quiet: Returns `Silent`
226    /// 2. --verbose: Returns `Debug`
227    /// 3. Otherwise: Returns the value from --log-level
228    ///
229    /// # Examples
230    ///
231    /// ```rust
232    /// use clap::Parser;
233    /// use sublime_cli_tools::cli::{Cli, LogLevel};
234    ///
235    /// // --quiet overrides to silent
236    /// let cli = Cli::parse_from(["workspace", "--quiet", "version"]);
237    /// assert_eq!(cli.effective_log_level(), LogLevel::Silent);
238    ///
239    /// // --verbose sets debug level
240    /// let cli = Cli::parse_from(["workspace", "--verbose", "version"]);
241    /// assert_eq!(cli.effective_log_level(), LogLevel::Debug);
242    ///
243    /// // Default uses --log-level value
244    /// let cli = Cli::parse_from(["workspace", "--log-level", "warn", "version"]);
245    /// assert_eq!(cli.effective_log_level(), LogLevel::Warn);
246    /// ```
247    #[must_use]
248    pub const fn effective_log_level(&self) -> LogLevel {
249        if self.quiet {
250            LogLevel::Silent
251        } else if self.verbose {
252            LogLevel::Debug
253        } else {
254            self.log_level
255        }
256    }
257
258    /// Returns the raw output format from command line.
259    ///
260    /// Note: Prefer `effective_output_format()` which accounts for --quiet flag.
261    ///
262    /// # Examples
263    ///
264    /// ```rust
265    /// use clap::Parser;
266    /// use sublime_cli_tools::cli::Cli;
267    /// use sublime_cli_tools::output::OutputFormat;
268    ///
269    /// let cli = Cli::parse_from(["workspace", "--format", "json", "version"]);
270    /// assert_eq!(cli.output_format(), OutputFormat::Json);
271    /// ```
272    #[must_use]
273    pub const fn output_format(&self) -> OutputFormat {
274        self.format.0
275    }
276
277    /// Returns the effective output format, accounting for --quiet flag.
278    ///
279    /// Priority:
280    /// 1. --quiet: Returns `Quiet`
281    /// 2. Otherwise: Returns the value from --format
282    ///
283    /// # Examples
284    ///
285    /// ```rust
286    /// use clap::Parser;
287    /// use sublime_cli_tools::cli::Cli;
288    /// use sublime_cli_tools::output::OutputFormat;
289    ///
290    /// // --quiet overrides to quiet format
291    /// let cli = Cli::parse_from(["workspace", "--quiet", "version"]);
292    /// assert_eq!(cli.effective_output_format(), OutputFormat::Quiet);
293    ///
294    /// // Default uses --format value
295    /// let cli = Cli::parse_from(["workspace", "--format", "json", "version"]);
296    /// assert_eq!(cli.effective_output_format(), OutputFormat::Json);
297    /// ```
298    #[must_use]
299    pub const fn effective_output_format(&self) -> OutputFormat {
300        if self.quiet { OutputFormat::Quiet } else { self.format.0 }
301    }
302
303    /// Returns whether color output is disabled.
304    ///
305    /// Also checks the NO_COLOR environment variable.
306    ///
307    /// # Examples
308    ///
309    /// ```rust
310    /// use clap::Parser;
311    /// use sublime_cli_tools::cli::Cli;
312    ///
313    /// let cli = Cli::parse_from(["workspace", "--no-color", "version"]);
314    /// assert!(cli.is_color_disabled());
315    /// ```
316    #[must_use]
317    pub fn is_color_disabled(&self) -> bool {
318        self.no_color || std::env::var("NO_COLOR").is_ok()
319    }
320
321    /// Returns the root directory.
322    ///
323    /// # Examples
324    ///
325    /// ```rust
326    /// use clap::Parser;
327    /// use sublime_cli_tools::cli::Cli;
328    /// use std::path::PathBuf;
329    ///
330    /// let cli = Cli::parse_from(["workspace", "--root", "/tmp", "version"]);
331    /// assert_eq!(cli.root(), Some(&PathBuf::from("/tmp")));
332    /// ```
333    #[must_use]
334    pub const fn root(&self) -> Option<&PathBuf> {
335        self.root.as_ref()
336    }
337
338    /// Returns the config file path.
339    ///
340    /// # Examples
341    ///
342    /// ```rust
343    /// use clap::Parser;
344    /// use sublime_cli_tools::cli::Cli;
345    /// use std::path::PathBuf;
346    ///
347    /// let cli = Cli::parse_from(["workspace", "--config", "custom.toml", "version"]);
348    /// assert_eq!(cli.config_path(), Some(&PathBuf::from("custom.toml")));
349    /// ```
350    #[must_use]
351    pub const fn config_path(&self) -> Option<&PathBuf> {
352        self.config.as_ref()
353    }
354}