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}