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 {
224 #[arg(
225 long,
226 help = "Merge artifacts from split build jobs and run post-build stages"
227 )]
228 merge: bool,
229 #[arg(long, help = "Custom dist directory (overrides config)")]
230 dist: Option<PathBuf>,
231 #[arg(long, help = "Run full pipeline without side effects")]
232 dry_run: bool,
233 #[arg(
234 long,
235 value_delimiter = ',',
236 help = "Skip stages (comma-separated, e.g. docker,announce)"
237 )]
238 skip: Vec<String>,
239 #[arg(
240 long,
241 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
242 )]
243 token: Option<String>,
244 },
245 Publish {
247 #[arg(long, help = "Run full pipeline without side effects")]
248 dry_run: bool,
249 #[arg(
250 long,
251 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
252 )]
253 token: Option<String>,
254 #[arg(long, help = "Custom dist directory (overrides config)")]
255 dist: Option<PathBuf>,
256 },
257 Bump {
263 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
264 level_or_version: Option<String>,
265 #[arg(
266 long,
267 short = 'p',
268 visible_alias = "crate",
269 action = clap::ArgAction::Append,
270 help = "Bump a specific crate (repeatable)"
271 )]
272 package: Vec<String>,
273 #[arg(
274 long,
275 alias = "all",
276 conflicts_with = "package",
277 help = "Bump every workspace member (excluding publish=false)"
278 )]
279 workspace: bool,
280 #[arg(
281 long,
282 action = clap::ArgAction::Append,
283 help = "Exclude a crate from --workspace (repeatable)"
284 )]
285 exclude: Vec<String>,
286 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
287 pre: Option<String>,
288 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
289 exact: bool,
290 #[arg(
291 long,
292 help = "Proceed even if the working tree has uncommitted changes"
293 )]
294 allow_dirty: bool,
295 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
296 yes: bool,
297 #[arg(long, help = "Print the plan without editing any files")]
298 dry_run: bool,
299 #[arg(long, help = "Stage edits and create a single commit")]
300 commit: bool,
301 #[arg(
302 long,
303 requires = "commit",
304 help = "GPG-sign the commit (requires --commit)"
305 )]
306 sign: bool,
307 #[arg(long, help = "Override the default commit message template")]
308 commit_message: Option<String>,
309 #[arg(
310 long,
311 default_value = "text",
312 help = "Output format: text | json (json requires --dry-run)"
313 )]
314 output: String,
315 },
316 Announce {
318 #[arg(long, help = "Run full pipeline without side effects")]
319 dry_run: bool,
320 #[arg(long, help = "Custom dist directory (overrides config)")]
321 dist: Option<PathBuf>,
322 #[arg(
323 long,
324 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
325 )]
326 token: Option<String>,
327 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
328 skip: Vec<String>,
329 },
330}
331
332pub fn detect_host_target() -> anyhow::Result<String> {
335 anodizer_core::partial::detect_host_target()
336}
337
338pub fn num_cpus() -> usize {
340 std::thread::available_parallelism()
341 .map(|n| n.get())
342 .unwrap_or(4)
343}
344
345pub fn build_cli() -> clap::Command {
347 <Cli as clap::CommandFactory>::command()
348}