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}