1use clap::{Parser, Subcommand};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[command(name = "anodizer", version, about = "Release Rust projects with ease")]
7pub struct Cli {
8 #[arg(
9 long,
10 short = 'f',
11 global = true,
12 help = "Path to config file (overrides auto-detection)"
13 )]
14 pub config: Option<PathBuf>,
15 #[arg(long, global = true, help = "Enable verbose output")]
16 pub verbose: bool,
17 #[arg(long, global = true, help = "Enable debug output")]
18 pub debug: bool,
19 #[arg(long, short = 'q', global = true, help = "Suppress non-error output")]
20 pub quiet: bool,
21 #[arg(
22 long,
23 global = true,
24 help = "Strict mode: configured features that silently skip become hard errors"
25 )]
26 pub strict: bool,
27 #[command(subcommand)]
28 pub command: Commands,
29}
30
31#[derive(Subcommand)]
32pub enum Commands {
33 Release {
35 #[arg(long = "crate", visible_alias = "id", action = clap::ArgAction::Append, help = "Release a specific crate (repeatable; --id is accepted as a GoReleaser-compat alias)")]
36 crate_names: Vec<String>,
37 #[arg(long, help = "Release all crates with unreleased changes")]
38 all: bool,
39 #[arg(long, help = "Force release even without unreleased changes")]
40 force: bool,
41 #[arg(long, help = "Build without publishing (snapshot mode)")]
42 snapshot: bool,
43 #[arg(long, help = "Create a nightly release with date-based version")]
44 nightly: bool,
45 #[arg(long, help = "Run full pipeline without side effects")]
46 dry_run: bool,
47 #[arg(long, help = "Remove dist directory before starting")]
48 clean: bool,
49 #[arg(
50 long,
51 value_delimiter = ',',
52 help = "Skip stages (comma-separated, e.g. docker,announce)"
53 )]
54 skip: Vec<String>,
55 #[arg(
56 long,
57 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
58 )]
59 token: Option<String>,
60 #[arg(
61 long,
62 default_value = "60m",
63 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
64 )]
65 timeout: String,
66 #[arg(
67 long,
68 short = 'p',
69 help = "Maximum number of parallel build jobs (default: number of CPUs)"
70 )]
71 parallelism: Option<usize>,
72 #[arg(long, help = "Automatically set --snapshot if the git repo is dirty")]
73 auto_snapshot: bool,
74 #[arg(long, help = "Build only for the host target triple")]
75 single_target: bool,
76 #[arg(
77 long,
78 help = "Path to a custom release notes file (overrides changelog)"
79 )]
80 release_notes: Option<PathBuf>,
81 #[arg(
82 long,
83 conflicts_with = "crate_names",
84 help = "Release a specific workspace in a monorepo config"
85 )]
86 workspace: Option<String>,
87 #[arg(long, help = "Set the release as a draft")]
88 draft: bool,
89 #[arg(long, help = "Path to a file containing custom release header text")]
90 release_header: Option<PathBuf>,
91 #[arg(
92 long,
93 help = "Path to a template file for release header (rendered with template variables)"
94 )]
95 release_header_tmpl: Option<PathBuf>,
96 #[arg(long, help = "Path to a file containing custom release footer text")]
97 release_footer: Option<PathBuf>,
98 #[arg(
99 long,
100 help = "Path to a template file for release footer (rendered with template variables)"
101 )]
102 release_footer_tmpl: Option<PathBuf>,
103 #[arg(
104 long,
105 help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
106 )]
107 release_notes_tmpl: Option<PathBuf>,
108 #[arg(long, help = "Abort immediately on first error during publishing")]
109 fail_fast: bool,
110 #[arg(
111 long,
112 conflicts_with = "merge",
113 help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
114 )]
115 split: bool,
116 #[arg(
117 long,
118 conflicts_with = "split",
119 help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
120 )]
121 merge: bool,
122 #[arg(
123 long,
124 help = "Run local build + archive + sign + checksum + sbom stages but skip release / publish / announce (GoReleaser Pro parity). Artifacts stay in dist/ for inspection."
125 )]
126 prepare: bool,
127 },
128 Build {
130 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
131 crate_names: Vec<String>,
132 #[arg(
133 long,
134 default_value = "60m",
135 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
136 )]
137 timeout: String,
138 #[arg(
139 long,
140 short = 'p',
141 help = "Maximum number of parallel build jobs (default: number of CPUs)"
142 )]
143 parallelism: Option<usize>,
144 #[arg(long, help = "Build only for the host target triple")]
145 single_target: bool,
146 #[arg(
147 long,
148 conflicts_with = "crate_names",
149 help = "Build a specific workspace in a monorepo config"
150 )]
151 workspace: Option<String>,
152 #[arg(
153 long,
154 short = 'o',
155 help = "Copy the built binary to this path (requires --single-target and single crate)"
156 )]
157 output: Option<PathBuf>,
158 #[arg(
159 long,
160 value_delimiter = ',',
161 help = "Skip stages (comma-separated: pre-hooks, post-hooks, validate, before)"
162 )]
163 skip: Vec<String>,
164 },
165 Check {
167 #[arg(long, help = "Validate a specific workspace in a monorepo config")]
168 workspace: Option<String>,
169 },
170 Init,
172 Changelog {
174 #[arg(long = "crate", help = "Generate changelog for a specific crate")]
175 crate_name: Option<String>,
176 },
177 Completion {
179 #[arg(value_enum, help = "Shell to generate completions for")]
180 shell: Shell,
181 },
182 Healthcheck,
184 Man,
186 Jsonschema,
188 ResolveTag {
190 #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
191 tag: String,
192 #[arg(long, help = "Output as JSON")]
193 json: bool,
194 },
195 Targets {
201 #[arg(long, help = "Output as JSON (include-form matrix)")]
202 json: bool,
203 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
204 crate_names: Vec<String>,
205 },
206 Tag {
208 #[arg(long, help = "Show what tag would be created without pushing")]
209 dry_run: bool,
210 #[arg(long, help = "Override bump logic with a specific tag value")]
211 custom_tag: Option<String>,
212 #[arg(long, help = "Override default bump type (patch/minor/major)")]
213 default_bump: Option<String>,
214 #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
215 crate_name: Option<String>,
216 },
217 Continue {
219 #[arg(
220 long,
221 help = "Merge artifacts from split build jobs and run post-build stages"
222 )]
223 merge: bool,
224 #[arg(long, help = "Custom dist directory (overrides config)")]
225 dist: Option<PathBuf>,
226 #[arg(long, help = "Run full pipeline without side effects")]
227 dry_run: bool,
228 #[arg(
229 long,
230 value_delimiter = ',',
231 help = "Skip stages (comma-separated, e.g. docker,announce)"
232 )]
233 skip: Vec<String>,
234 #[arg(
235 long,
236 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
237 )]
238 token: Option<String>,
239 },
240 Publish {
242 #[arg(long, help = "Run full pipeline without side effects")]
243 dry_run: bool,
244 #[arg(
245 long,
246 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
247 )]
248 token: Option<String>,
249 #[arg(long, help = "Custom dist directory (overrides config)")]
250 dist: Option<PathBuf>,
251 },
252 Bump {
258 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
259 level_or_version: Option<String>,
260 #[arg(
261 long,
262 short = 'p',
263 visible_alias = "crate",
264 action = clap::ArgAction::Append,
265 help = "Bump a specific crate (repeatable)"
266 )]
267 package: Vec<String>,
268 #[arg(
269 long,
270 alias = "all",
271 conflicts_with = "package",
272 help = "Bump every workspace member (excluding publish=false)"
273 )]
274 workspace: bool,
275 #[arg(
276 long,
277 action = clap::ArgAction::Append,
278 help = "Exclude a crate from --workspace (repeatable)"
279 )]
280 exclude: Vec<String>,
281 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
282 pre: Option<String>,
283 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
284 exact: bool,
285 #[arg(
286 long,
287 help = "Proceed even if the working tree has uncommitted changes"
288 )]
289 allow_dirty: bool,
290 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
291 yes: bool,
292 #[arg(long, help = "Print the plan without editing any files")]
293 dry_run: bool,
294 #[arg(long, help = "Stage edits and create a single commit")]
295 commit: bool,
296 #[arg(
297 long,
298 requires = "commit",
299 help = "GPG-sign the commit (requires --commit)"
300 )]
301 sign: bool,
302 #[arg(long, help = "Override the default commit message template")]
303 commit_message: Option<String>,
304 #[arg(
305 long,
306 default_value = "text",
307 help = "Output format: text | json (json requires --dry-run)"
308 )]
309 output: String,
310 },
311 Announce {
313 #[arg(long, help = "Run full pipeline without side effects")]
314 dry_run: bool,
315 #[arg(long, help = "Custom dist directory (overrides config)")]
316 dist: Option<PathBuf>,
317 #[arg(
318 long,
319 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
320 )]
321 token: Option<String>,
322 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
323 skip: Vec<String>,
324 },
325}
326
327pub fn detect_host_target() -> anyhow::Result<String> {
330 anodizer_core::partial::detect_host_target()
331}
332
333pub fn num_cpus() -> usize {
335 std::thread::available_parallelism()
336 .map(|n| n.get())
337 .unwrap_or(4)
338}
339
340pub fn build_cli() -> clap::Command {
342 <Cli as clap::CommandFactory>::command()
343}