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
183impl Cli {
184    /// Returns the log level.
185    ///
186    /// # Examples
187    ///
188    /// ```rust
189    /// use clap::Parser;
190    /// use sublime_cli_tools::cli::{Cli, LogLevel};
191    ///
192    /// let cli = Cli::parse_from(["workspace", "--log-level", "debug", "version"]);
193    /// assert_eq!(cli.log_level(), LogLevel::Debug);
194    /// ```
195    #[must_use]
196    pub const fn log_level(&self) -> LogLevel {
197        self.log_level
198    }
199
200    /// Returns the output format.
201    ///
202    /// # Examples
203    ///
204    /// ```rust
205    /// use clap::Parser;
206    /// use sublime_cli_tools::cli::Cli;
207    /// use sublime_cli_tools::output::OutputFormat;
208    ///
209    /// let cli = Cli::parse_from(["workspace", "--format", "json", "version"]);
210    /// assert_eq!(cli.output_format(), OutputFormat::Json);
211    /// ```
212    #[must_use]
213    pub const fn output_format(&self) -> OutputFormat {
214        self.format.0
215    }
216
217    /// Returns whether color output is disabled.
218    ///
219    /// Also checks the NO_COLOR environment variable.
220    ///
221    /// # Examples
222    ///
223    /// ```rust
224    /// use clap::Parser;
225    /// use sublime_cli_tools::cli::Cli;
226    ///
227    /// let cli = Cli::parse_from(["workspace", "--no-color", "version"]);
228    /// assert!(cli.is_color_disabled());
229    /// ```
230    #[must_use]
231    pub fn is_color_disabled(&self) -> bool {
232        self.no_color || std::env::var("NO_COLOR").is_ok()
233    }
234
235    /// Returns the root directory.
236    ///
237    /// # Examples
238    ///
239    /// ```rust
240    /// use clap::Parser;
241    /// use sublime_cli_tools::cli::Cli;
242    /// use std::path::PathBuf;
243    ///
244    /// let cli = Cli::parse_from(["workspace", "--root", "/tmp", "version"]);
245    /// assert_eq!(cli.root(), Some(&PathBuf::from("/tmp")));
246    /// ```
247    #[must_use]
248    pub const fn root(&self) -> Option<&PathBuf> {
249        self.root.as_ref()
250    }
251
252    /// Returns the config file path.
253    ///
254    /// # Examples
255    ///
256    /// ```rust
257    /// use clap::Parser;
258    /// use sublime_cli_tools::cli::Cli;
259    /// use std::path::PathBuf;
260    ///
261    /// let cli = Cli::parse_from(["workspace", "--config", "custom.toml", "version"]);
262    /// assert_eq!(cli.config_path(), Some(&PathBuf::from("custom.toml")));
263    /// ```
264    #[must_use]
265    pub const fn config_path(&self) -> Option<&PathBuf> {
266        self.config.as_ref()
267    }
268}