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}