Skip to main content

callisto_cli/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4
5/// Changesets-style version and release manager for Rust workspaces.
6#[derive(Parser)]
7#[command(name = "callisto", version)]
8pub struct Cli {
9    #[command(flatten)]
10    pub global: GlobalArgs,
11    #[command(subcommand)]
12    pub command: Command,
13}
14
15/// Flags shared by every subcommand.
16#[derive(Args, Clone, Debug)]
17pub struct GlobalArgs {
18    /// Output format for command results.
19    #[arg(long, global = true, value_enum, default_value = "text")]
20    pub format: OutputFormat,
21
22    /// Workspace directory to operate in (defaults to the current directory).
23    #[arg(long, global = true, default_value = ".")]
24    pub cwd: PathBuf,
25
26    #[arg(
27        long,
28        global = true,
29        help = "Preview manifest and file changes without writing to disk"
30    )]
31    pub dry_run: bool,
32}
33
34/// Output rendering mode: human-readable text or machine-readable JSON.
35#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
36pub enum OutputFormat {
37    Text,
38    Json,
39}
40
41/// A callisto subcommand.
42#[derive(Subcommand, Clone, Debug)]
43pub enum Command {
44    /// Create a new changeset describing pending package changes.
45    Add(AddArgs),
46    /// Show the workspace's pending changesets and diagnostics.
47    Status(StatusArgs),
48    /// Print the workspace's native-build target matrix as JSON or a table.
49    Matrix(MatrixArgs),
50    /// Consume pending changesets and bump package versions accordingly.
51    Version(VersionArgs),
52    /// Manage prerelease mode for the workspace.
53    #[command(subcommand)]
54    Pre(PreArgs),
55    /// Check that changesets and the dependency graph are well-formed.
56    Validate(ValidateArgs),
57    /// Apply a temporary, non-persistent version bump for a snapshot release.
58    Snapshot(SnapshotArgs),
59    /// Scaffold Callisto configuration in the current workspace.
60    Init(InitArgs),
61    /// Compute which packages are ready to publish and print the publish plan.
62    PlanPublish(PlanPublishArgs),
63    /// Publish ready packages to their ecosystem registries via native CLI tools.
64    Publish(PublishArgs),
65    /// Generate a pull request body summarizing pending release changes.
66    ComposePrBody(ComposePrBodyArgs),
67    /// Create git tags for packages in a publish plan.
68    Tag(TagArgs),
69    /// Generate shell completion scripts.
70    Completions(CompletionsArgs),
71    /// Print the JSON schema for a report type.
72    Schema(SchemaArgs),
73}
74
75/// Arguments for the `schema` command.
76#[derive(Args, Clone, Debug, Default)]
77pub struct SchemaArgs {
78    /// Report type to print the schema for (status, version, snapshot, validate, tag, init, plan-publish, changeset, pre, matrix); defaults to status.
79    #[arg(long = "type", value_name = "TYPE")]
80    pub target_type: Option<String>,
81}
82
83/// Arguments for the `add` command.
84#[derive(Args, Clone, Debug)]
85pub struct AddArgs {
86    /// Package and severity to include, as `name:severity` (patch, minor, or major); repeatable. Omit to enter the interactive wizard.
87    #[arg(long = "package", value_name = "NAME:SEVERITY")]
88    pub packages: Vec<String>,
89    /// Human-readable summary of the change to record in the changeset.
90    #[arg(long)]
91    pub summary: Option<String>,
92}
93
94/// Arguments for the `status` command.
95#[derive(Args, Clone, Debug)]
96pub struct StatusArgs {
97    /// Enable strict mode: promote warning-level diagnostics to errors, causing a non-zero exit.
98    #[arg(long)]
99    pub strict: bool,
100    /// Treat dependency-graph warnings as errors.
101    #[arg(long)]
102    pub strict_graph: bool,
103    /// Exit with a distinct status code indicating whether any changesets are pending.
104    #[arg(long)]
105    pub check: bool,
106}
107
108/// Arguments for the `matrix` command.
109#[derive(Args, Clone, Debug, Default)]
110pub struct MatrixArgs {
111    /// Restrict output to one registered package's name (PackageId::name()).
112    #[arg(long)]
113    pub package: Option<String>,
114}
115
116/// Arguments for the `version` command.
117#[derive(Args, Clone, Debug)]
118pub struct VersionArgs {
119    /// Regenerate lockfiles after applying the version bumps.
120    #[arg(long)]
121    pub refresh_lockfiles: bool,
122    /// Treat warning-level diagnostics as errors.
123    #[arg(long)]
124    pub strict: bool,
125    /// Treat dependency-graph warnings as errors.
126    #[arg(long)]
127    pub strict_graph: bool,
128    /// Allow versioning to proceed even if no changesets are pending.
129    #[arg(long)]
130    pub allow_empty_changesets: bool,
131}
132
133/// Subcommands for managing prerelease mode.
134#[derive(Subcommand, Clone, Debug)]
135pub enum PreArgs {
136    /// Enter prerelease mode, tagging subsequent version bumps with the given prerelease tag.
137    Enter { tag: String },
138    /// Exit prerelease mode, returning to normal versioning.
139    Exit,
140}
141
142/// Arguments for the `validate` command.
143#[derive(Args, Clone, Debug)]
144pub struct ValidateArgs {
145    /// Validate only changesets staged in git.
146    #[arg(long)]
147    pub staged: bool,
148    /// Validate only changesets added since the given git ref.
149    #[arg(long, value_name = "REF", conflicts_with = "staged")]
150    pub since: Option<String>,
151    /// Treat warning-level diagnostics as errors.
152    #[arg(long)]
153    pub strict: bool,
154    /// Treat dependency-graph warnings as errors.
155    #[arg(long)]
156    pub strict_graph: bool,
157}
158
159/// Arguments for the `snapshot` command.
160#[derive(Args, Clone, Debug)]
161pub struct SnapshotArgs {
162    /// Tag to append to the snapshot version (e.g. a commit SHA or branch name).
163    #[arg(long)]
164    pub tag: String,
165    /// Abort if the workspace graph contains crosscheck failures or other
166    /// error-severity diagnostics.
167    #[arg(long)]
168    pub strict: bool,
169}
170
171/// Arguments for the `init` command.
172#[derive(Args, Clone, Debug)]
173pub struct InitArgs {
174    /// Skip the interactive confirmation prompt.
175    #[arg(long)]
176    pub yes: bool,
177}
178
179/// Arguments for the `plan-publish` command.
180#[derive(Args, Clone, Debug, Default)]
181pub struct PlanPublishArgs {
182    /// Plan only the named package(s). Repeatable: `--package foo --package bar`.
183    #[arg(long = "package", value_name = "NAME")]
184    pub only: Vec<String>,
185}
186
187/// Arguments for the `publish` command.
188#[derive(Args, Clone, Debug, Default)]
189pub struct PublishArgs {
190    /// Publish only the named package(s). Repeatable: `--package foo --package bar`.
191    /// When omitted, all packages in the plan are published.
192    #[arg(long = "package", value_name = "NAME")]
193    pub only: Vec<String>,
194}
195
196/// Arguments for the `compose-pr-body` command.
197#[derive(Args, Clone, Debug)]
198pub struct ComposePrBodyArgs {
199    /// Existing PR body text to merge with, or `-` to read it from stdin.
200    #[arg(long, value_name = "TEXT|-")]
201    pub existing_body: Option<String>,
202    /// Label to attach to the PR body; repeatable.
203    #[arg(long = "label")]
204    pub labels: Vec<String>,
205    /// Branch name to reference in the generated PR body.
206    #[arg(long)]
207    pub branch: Option<String>,
208}
209
210/// Arguments for the `tag` command.
211#[derive(Args, Clone, Debug)]
212pub struct TagArgs {
213    /// Path to a publish plan JSON file, inline JSON, or `-` to read it from stdin.
214    #[arg(long, value_name = "FILE|-")]
215    pub plan: String,
216    /// Also move a floating major-version tag (e.g. `v1`) to point at the new tag.
217    #[arg(long)]
218    pub floating_major: bool,
219    /// Abort if the workspace graph contains crosscheck failures or other
220    /// error-severity diagnostics.
221    #[arg(long)]
222    pub strict: bool,
223}
224
225/// Arguments for the `completions` command.
226#[derive(Args, Clone, Debug)]
227pub struct CompletionsArgs {
228    /// Shell to generate a completion script for.
229    #[arg(value_enum)]
230    pub shell: clap_complete::Shell,
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use clap::CommandFactory;
237
238    /// QW-5: --strict flag on status subcommand must have a meaningful help string.
239    #[test]
240    fn strict_flag_help_text_is_meaningful() {
241        let mut cmd = Cli::command();
242        cmd.build();
243
244        // Find the "status" subcommand.
245        let status_sub = cmd
246            .get_subcommands()
247            .find(|s| s.get_name() == "status")
248            .expect("status subcommand must exist");
249
250        // Find the --strict argument.
251        let strict_arg = status_sub
252            .get_arguments()
253            .find(|a| a.get_long() == Some("strict"))
254            .expect("--strict argument must exist on status subcommand");
255
256        let help = strict_arg
257            .get_help()
258            .map(|h| h.to_string())
259            .unwrap_or_default()
260            .to_lowercase();
261
262        // Must contain "strict" and describe what it does.
263        assert!(
264            help.contains("strict"),
265            "--strict help text must contain the word 'strict'; got: {help:?}"
266        );
267        assert!(
268            help.contains("warning") || help.contains("error"),
269            "--strict help text must mention 'warning' or 'error'; got: {help:?}"
270        );
271        // Must be longer than a placeholder.
272        assert!(
273            help.len() > 20,
274            "--strict help text is too short to be meaningful: {help:?}"
275        );
276    }
277
278    /// AC-006/AC-007 (parse slice): `callisto matrix --package foo` parses
279    /// into Command::Matrix with the package field populated; MatrixArgs
280    /// declares no --format of its own (the global flag is used instead).
281    #[test]
282    fn test_cli_parse_matrix_command_with_package() {
283        use clap::Parser;
284        let cli = Cli::parse_from(["callisto", "matrix", "--package", "foo"]);
285        if let Command::Matrix(args) = cli.command {
286            assert_eq!(args.package, Some("foo".to_string()));
287        } else {
288            panic!("Expected Matrix command");
289        }
290    }
291
292    /// AC-003b: bare `callisto matrix` (no --package) parses with package: None.
293    #[test]
294    fn test_cli_parse_matrix_command_bare() {
295        use clap::Parser;
296        let cli = Cli::parse_from(["callisto", "matrix"]);
297        if let Command::Matrix(args) = cli.command {
298            assert_eq!(args.package, None);
299        } else {
300            panic!("Expected Matrix command");
301        }
302    }
303}