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