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)]
37#[allow(clippy::large_enum_variant)]
42pub enum Commands {
43 Release {
45 #[arg(long = "crate", visible_alias = "id", action = clap::ArgAction::Append, help = "Release a specific crate (repeatable; --id is accepted as a GoReleaser-compat alias)")]
46 crate_names: Vec<String>,
47 #[arg(long, help = "Release all crates with unreleased changes")]
48 all: bool,
49 #[arg(long, help = "Force release even without unreleased changes")]
50 force: bool,
51 #[arg(long, help = "Build without publishing (snapshot mode)")]
52 snapshot: bool,
53 #[arg(long, help = "Create a nightly release with date-based version")]
54 nightly: bool,
55 #[arg(long, help = "Run full pipeline without side effects")]
56 dry_run: bool,
57 #[arg(long, help = "Remove dist directory before starting")]
58 clean: bool,
59 #[arg(
60 long,
61 value_delimiter = ',',
62 help = "Skip stages (comma-separated, e.g. docker,announce)"
63 )]
64 skip: Vec<String>,
65 #[arg(
66 long,
67 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
68 )]
69 token: Option<String>,
70 #[arg(
71 long,
72 default_value = "60m",
73 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
74 )]
75 timeout: String,
76 #[arg(
77 long,
78 short = 'p',
79 help = "Maximum number of parallel build jobs (default: number of CPUs)"
80 )]
81 parallelism: Option<usize>,
82 #[arg(long, help = "Automatically set --snapshot if the git repo is dirty")]
83 auto_snapshot: bool,
84 #[arg(long, help = "Build only for the host target triple")]
85 single_target: bool,
86 #[arg(
87 long,
88 value_name = "csv",
89 conflicts_with = "single_target",
90 help = "Restrict the build to a comma-separated subset of configured target triples (e.g. x86_64-apple-darwin,aarch64-apple-darwin). Used by the Determinism Harness's sharded job matrix; conflicts with --single-target."
91 )]
92 targets: Option<String>,
93 #[arg(
94 long = "host-targets",
95 conflicts_with = "single_target",
96 conflicts_with = "targets",
97 help = "Build every configured target this host can build, skipping cross-compile-only targets (apple targets on a non-macOS host). Only valid with --snapshot or --dry-run. Used by `task prepush` to do a real host-scoped build without aborting on un-buildable targets."
98 )]
99 host_targets: bool,
100 #[arg(
101 long,
102 help = "Path to a custom release notes file (overrides changelog)"
103 )]
104 release_notes: Option<PathBuf>,
105 #[arg(
106 long,
107 conflicts_with = "crate_names",
108 help = "Release a specific workspace in a monorepo config"
109 )]
110 workspace: Option<String>,
111 #[arg(
112 long,
113 conflicts_with = "no_preflight",
114 help = "Run pre-flight publisher-state check and exit (don't start the pipeline)"
115 )]
116 preflight: bool,
117 #[arg(
118 long,
119 conflicts_with = "preflight",
120 help = "Skip the automatic pre-flight publisher-state check"
121 )]
122 no_preflight: bool,
123 #[arg(
124 long,
125 help = "Alias for --strict (also treats Unknown publisher state as a blocker during pre-flight)"
126 )]
127 strict_preflight: bool,
128 #[arg(long, help = "Set the release as a draft")]
129 draft: bool,
130 #[arg(long, help = "Path to a file containing custom release header text")]
131 release_header: Option<PathBuf>,
132 #[arg(
133 long,
134 help = "Path to a template file for release header (rendered with template variables)"
135 )]
136 release_header_tmpl: Option<PathBuf>,
137 #[arg(long, help = "Path to a file containing custom release footer text")]
138 release_footer: Option<PathBuf>,
139 #[arg(
140 long,
141 help = "Path to a template file for release footer (rendered with template variables)"
142 )]
143 release_footer_tmpl: Option<PathBuf>,
144 #[arg(
145 long,
146 help = "Path to a template file for release notes (rendered with template variables, overrides --release-notes)"
147 )]
148 release_notes_tmpl: Option<PathBuf>,
149 #[arg(long, help = "Abort immediately on first error during publishing")]
150 fail_fast: bool,
151 #[arg(
152 long = "no-gate-submitter",
153 help = "Disable the Submitter gate: dispatch Submitter publishers even when required Assets/Manager publishers failed"
154 )]
155 no_gate_submitter: bool,
156 #[arg(
157 long = "rollback",
158 value_name = "none|best-effort",
159 help = "Rollback policy after publish stage. Defaults to best-effort when preflight is clean, none otherwise."
160 )]
161 rollback: Option<String>,
162 #[arg(
163 long = "simulate-failure",
164 value_name = "publisher",
165 action = clap::ArgAction::Append,
166 hide = true,
167 help = "(TEST HARNESS) Force a named publisher to fail. Gated by ANODIZE_TEST_HARNESS=1."
168 )]
169 simulate_failure: Vec<String>,
170 #[arg(
171 long = "rollback-only",
172 requires = "from_run",
173 conflicts_with = "clean",
174 help = "Skip publish; re-attempt rollback from a prior run report. Requires --from-run=<id>."
175 )]
176 rollback_only: bool,
177 #[arg(
178 long = "from-run",
179 value_name = "id",
180 requires = "rollback_only",
181 value_parser = parse_run_id,
182 help = "Prior run id whose state to load when running --rollback-only. \
183 Loads <dist>/run-<id>/rollback.json if present (a prior replay's state), \
184 otherwise <dist>/run-<id>/report.json. Delete rollback.json to force a \
185 full re-roll. Must match the run_id format written by the release pipeline \
186 (alphanumeric, dot, dash, underscore; no path separators)."
187 )]
188 from_run: Option<String>,
189 #[arg(
190 long = "allow-rerun",
191 conflicts_with = "rollback_only",
192 help = "DANGEROUS: force publish to proceed even when a prior \
193 dist/run-<id>/report.json exists for this tag. PR-based publishers \
194 (homebrew, scoop, nix, krew, MCP) will open DUPLICATE pull requests. \
195 Recover from partial failures with --rollback-only --from-run=<id> first. \
196 Cannot be combined with --rollback-only (which has its own idempotency)."
197 )]
198 allow_rerun: bool,
199 #[arg(
200 long = "allow-nondeterministic",
201 value_name = "name=reason",
202 action = clap::ArgAction::Append,
203 help = "Runtime non-determinism opt-out for a specific artifact (repeatable). Mutually exclusive with --strict."
204 )]
205 allow_nondeterministic: Vec<String>,
206 #[arg(
207 long = "summary-json",
208 value_name = "path",
209 help = "Write the per-publisher run summary JSON to this path."
210 )]
211 summary_json: Option<PathBuf>,
212 #[arg(
213 long = "allow-ai-failure",
214 help = "If `changelog.ai` is configured and the AI provider fails, log a warning and keep the pre-AI release notes instead of aborting the release."
215 )]
216 allow_ai_failure: bool,
217 #[arg(
218 long,
219 conflicts_with = "merge",
220 help = "Run only the build stage for split CI fan-out (outputs artifacts JSON to dist/)"
221 )]
222 split: bool,
223 #[arg(
224 long,
225 conflicts_with = "split",
226 help = "Merge artifacts from split build jobs and resume the pipeline from post-build stages"
227 )]
228 merge: bool,
229 #[arg(
230 long = "publish-only",
231 conflicts_with_all = ["split", "merge", "prepare", "announce_only", "snapshot", "rollback_only", "clean"],
232 help = "Load artifacts from dist/ (preserved by `anodize check determinism --preserve-dist`) and run only the sign + publish pipeline. Skips build/archive/nfpm/sbom/checksum — those stages' outputs must already be present in dist/."
233 )]
234 publish_only: bool,
235 #[arg(
236 long,
237 alias = "prepare-only",
238 conflicts_with_all = ["publish_only", "announce_only", "rollback_only"],
239 help = "Run local build + archive + sign + checksum + sbom stages but skip release / publish / announce (GoReleaser Pro parity). Artifacts stay in dist/ for inspection. `--prepare-only` is accepted as an alias for GR-imported scripts."
240 )]
241 prepare: bool,
242 #[arg(
243 long = "announce-only",
244 conflicts_with_all = ["prepare", "publish_only", "snapshot", "rollback_only", "split", "merge", "clean"],
245 help = "Re-fire announcers only. Loads `<dist>/run-<id>/report.json` written by a prior run, skips every pipeline stage except announce (which itself short-circuits on nightly), then runs after-hooks. Use this to retry a transient announcer failure (Slack 502, Discord 5xx) without re-creating the GitHub release or re-publishing to package managers. Fails fast when no `<dist>/run-<id>/report.json` is present."
246 )]
247 announce_only: bool,
248 #[arg(
249 long,
250 help = "Resume into an existing release left over from a prior failed attempt; bypasses the safety check that bails on partial assets."
251 )]
252 resume_release: bool,
253 #[arg(
254 long,
255 help = "Force release.replace_existing_artifacts: true regardless of config (overwrite conflicting assets on retry)."
256 )]
257 replace_existing: bool,
258 #[arg(
259 long = "no-post-publish-poll",
260 help = "Skip post-publish polling for chocolatey moderation / winget PR validation; report NotPolled for affected publishers."
261 )]
262 no_post_publish_poll: bool,
263 },
264 Build {
266 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Build a specific crate (repeatable)")]
267 crate_names: Vec<String>,
268 #[arg(
269 long,
270 default_value = "60m",
271 help = "Pipeline timeout duration (e.g., 60m, 1h, 5s)"
272 )]
273 timeout: String,
274 #[arg(
275 long,
276 short = 'p',
277 help = "Maximum number of parallel build jobs (default: number of CPUs)"
278 )]
279 parallelism: Option<usize>,
280 #[arg(long, help = "Build only for the host target triple")]
281 single_target: bool,
282 #[arg(
283 long,
284 conflicts_with = "crate_names",
285 help = "Build a specific workspace in a monorepo config"
286 )]
287 workspace: Option<String>,
288 #[arg(
289 long,
290 short = 'o',
291 help = "Copy the built binary to this path (requires --single-target and single crate)"
292 )]
293 output: Option<PathBuf>,
294 #[arg(
295 long,
296 value_delimiter = ',',
297 help = "Skip stages (comma-separated: pre-hooks, post-hooks, validate, before)"
298 )]
299 skip: Vec<String>,
300 },
301 Check {
303 #[command(subcommand)]
304 cmd: CheckCmd,
305 },
306 Init {
308 #[arg(
309 long,
310 help = "Discover repo files that embed the current version and enroll the selection into version_files in .anodizer.yaml"
311 )]
312 version_files: bool,
313 #[arg(
314 long,
315 value_delimiter = ',',
316 requires = "version_files",
317 help = "Glob(s) to drop from discovered candidates (repeatable or comma-separated); only with --version-files"
318 )]
319 exclude: Vec<String>,
320 #[arg(
321 long,
322 short = 'y',
323 requires = "version_files",
324 help = "Non-interactive: enroll all discovered candidates without prompting"
325 )]
326 yes: bool,
327 },
328 Changelog {
330 #[arg(
331 value_name = "tag|range",
332 help = "Commit range to render: a single tag (predecessor-resolved against its crate), an explicit `from..to` range, or omitted to refresh each crate's pending section against its last tag"
333 )]
334 range: Option<String>,
335 #[arg(
336 long,
337 value_enum,
338 default_value = "keep-a-changelog",
339 help = "Output format: keep-a-changelog (refresh the [Unreleased] section), release-notes (grouped-bullet GitHub body to stdout), or json"
340 )]
341 format: ChangelogFormat,
342 #[arg(
343 long,
344 help = "Apply the regenerated [Unreleased] section to the configured CHANGELOG.md file(s) in place (keep-a-changelog only)"
345 )]
346 write: bool,
347 #[arg(long = "crate", help = "Restrict to a specific crate in a workspace")]
348 crate_name: Option<String>,
349 #[arg(
350 long,
351 help = "Preview as a snapshot release (release-notes format only)"
352 )]
353 snapshot: bool,
354 },
355 Completion {
357 #[arg(value_enum, help = "Shell to generate completions for")]
358 shell: Shell,
359 },
360 Healthcheck,
362 Man,
364 Jsonschema,
366 ResolveTag {
368 #[arg(help = "Tag to resolve (e.g. 'v1.2.3', 'core-v0.2.3')")]
369 tag: String,
370 #[arg(long, help = "Output as JSON")]
371 json: bool,
372 },
373 Targets {
379 #[arg(long, help = "Output as JSON (include-form matrix)")]
380 json: bool,
381 #[arg(long = "crate", action = clap::ArgAction::Append, help = "Restrict to specific crate(s)")]
382 crate_names: Vec<String>,
383 },
384 Tag {
386 #[arg(long, help = "Show what tag would be created without pushing")]
387 dry_run: bool,
388 #[arg(long, help = "Override bump logic with a specific tag value")]
389 custom_tag: Option<String>,
390 #[arg(long = "version", value_name = "VERSION")]
401 version_override: Option<String>,
402 #[arg(long, help = "Override default bump type (patch/minor/major)")]
403 default_bump: Option<String>,
404 #[arg(long = "crate", help = "Tag a specific crate in a workspace")]
405 crate_name: Option<String>,
406 #[arg(
407 long,
408 help = "Push the version-sync bump commit to the release branch atomically with the tag"
409 )]
410 push: bool,
411 #[arg(
412 long,
413 conflicts_with = "push",
414 help = "Push the tag only, leaving the version-sync bump commit local"
415 )]
416 no_push: bool,
417 #[arg(
418 long,
419 value_name = "NAME",
420 help = "Remote to push to (default: origin)"
421 )]
422 push_remote: Option<String>,
423 #[arg(
424 long,
425 help = "Create the tag + bump commit locally but only print (not run) the git push commands --push would use; pass --dry-run to also preview tagging"
426 )]
427 push_dry_run: bool,
428 #[arg(
429 long = "changelog",
430 help = "Refresh CHANGELOG.md as part of this tag (requires a `changelog:` config block)"
431 )]
432 changelog: bool,
433 #[command(subcommand)]
439 sub: Option<TagSub>,
440 },
441 Continue {
461 #[arg(
462 long,
463 help = "Merge artifacts from split build jobs and run post-build stages"
464 )]
465 merge: bool,
466 #[arg(long, help = "Custom dist directory (overrides config)")]
467 dist: Option<PathBuf>,
468 #[arg(long, help = "Run full pipeline without side effects")]
469 dry_run: bool,
470 #[arg(
471 long,
472 value_delimiter = ',',
473 help = "Skip stages (comma-separated, e.g. docker,announce)"
474 )]
475 skip: Vec<String>,
476 #[arg(
477 long,
478 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
479 )]
480 token: Option<String>,
481 },
482 Publish {
495 #[arg(long, help = "Run full pipeline without side effects")]
496 dry_run: bool,
497 #[arg(
498 long,
499 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
500 )]
501 token: Option<String>,
502 #[arg(long, help = "Custom dist directory (overrides config)")]
503 dist: Option<PathBuf>,
504 #[arg(
505 long,
506 help = "Merge artifacts from `release --split` workers (dist/<subdir>/context.json) before running the publish-only pipeline. Mirrors `goreleaser publish --merge`."
507 )]
508 merge: bool,
509 #[arg(
510 long,
511 help = "Force re-publish even when a prior report.json exists. \
512 WARNING: PR-based publishers will open duplicate pull requests."
513 )]
514 allow_rerun: bool,
515 },
516 Bump {
522 #[arg(help = "patch | minor | major | <version> | release (omit to infer)")]
523 level_or_version: Option<String>,
524 #[arg(
525 long,
526 short = 'p',
527 visible_alias = "crate",
528 action = clap::ArgAction::Append,
529 help = "Bump a specific crate (repeatable)"
530 )]
531 package: Vec<String>,
532 #[arg(
533 long,
534 alias = "all",
535 conflicts_with = "package",
536 help = "Bump every workspace member (excluding publish=false)"
537 )]
538 workspace: bool,
539 #[arg(
540 long,
541 action = clap::ArgAction::Append,
542 help = "Exclude a crate from --workspace (repeatable)"
543 )]
544 exclude: Vec<String>,
545 #[arg(long, help = "Append a prerelease identifier (e.g. rc.1)")]
546 pre: Option<String>,
547 #[arg(long, help = "Do not rewrite dependents' [dependencies] version specs")]
548 exact: bool,
549 #[arg(
550 long,
551 help = "Proceed even if the working tree has uncommitted changes"
552 )]
553 allow_dirty: bool,
554 #[arg(long, short = 'y', help = "Skip confirmation prompt")]
555 yes: bool,
556 #[arg(long, help = "Print the plan without editing any files")]
557 dry_run: bool,
558 #[arg(long, help = "Stage edits and create a single commit")]
559 commit: bool,
560 #[arg(
561 long = "changelog",
562 requires = "commit",
563 help = "Refresh CHANGELOG.md in the bump commit (requires --commit and a `changelog:` config block)"
564 )]
565 changelog: bool,
566 #[arg(
567 long,
568 requires = "commit",
569 help = "GPG-sign the commit (requires --commit)"
570 )]
571 sign: bool,
572 #[arg(long, help = "Override the default commit message template")]
573 commit_message: Option<String>,
574 #[arg(
575 long,
576 default_value = "text",
577 help = "Output format: text | json (json requires --dry-run)"
578 )]
579 output: String,
580 },
581 Announce {
590 #[arg(long, help = "Run full pipeline without side effects")]
591 dry_run: bool,
592 #[arg(long, help = "Custom dist directory (overrides config)")]
593 dist: Option<PathBuf>,
594 #[arg(
595 long,
596 help = "GitHub token (overrides ANODIZER_GITHUB_TOKEN / GITHUB_TOKEN env vars)"
597 )]
598 token: Option<String>,
599 #[arg(long, value_delimiter = ',', help = "Skip stages (comma-separated)")]
600 skip: Vec<String>,
601 #[arg(
602 long,
603 help = "Merge artifact lists from `release --split` workers (dist/<subdir>/context.json) before announcing. Mirrors `goreleaser announce --merge`."
604 )]
605 merge: bool,
606 },
607 Notify {
614 message: String,
617 #[arg(long = "publishers", value_delimiter = ',')]
621 publishers: Vec<String>,
622 #[arg(long = "skip", value_delimiter = ',')]
624 skip: Vec<String>,
625 #[arg(long)]
627 dry_run: bool,
628 },
629}
630
631#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
633pub enum ChangelogFormat {
634 #[default]
638 #[value(name = "keep-a-changelog", alias = "kac")]
639 KeepAChangelog,
640 ReleaseNotes,
643 Json,
646}
647
648#[derive(Subcommand)]
655pub enum TagSub {
656 Rollback {
659 #[arg(
660 value_name = "sha",
661 help = "Commit SHA to roll back from. Defaults to HEAD."
662 )]
663 sha: Option<String>,
664 #[arg(long, help = "Print what would happen without mutating anything")]
665 dry_run: bool,
666 #[arg(
667 long = "no-push",
668 help = "Skip remote tag delete and branch push (local-only)"
669 )]
670 no_push: bool,
671 #[arg(
672 long,
673 default_value = "all",
674 help = "Tag-shape filter: all | lockstep | per-crate"
675 )]
676 scope: String,
677 #[arg(
678 long,
679 default_value = "revert",
680 help = "Rollback strategy: revert (default; history-preserving) | reset (opt-in; rewrites history, requires --force-with-lease to push)"
681 )]
682 mode: String,
683 #[arg(
684 long,
685 value_name = "name",
686 help = "Branch name to push the revert commit to. Required when HEAD is detached and no local branch points at it (typical CI tag-push context, where GITHUB_REF_NAME is the tag — not the bump-commit branch). Pass --branch master (or whichever branch the bump commit was created on)."
687 )]
688 branch: Option<String>,
689 },
690}
691
692#[derive(Subcommand)]
698pub enum CheckCmd {
699 Config {
701 #[arg(long, help = "Validate a specific workspace in a monorepo config")]
702 workspace: Option<String>,
703 },
704 Determinism(CheckDeterminismArgs),
706 VersionFiles,
708}
709
710#[derive(clap::Args)]
711pub struct CheckDeterminismArgs {
712 #[arg(
713 long,
714 default_value = "2",
715 help = "Number of from-clean rebuilds to diff"
716 )]
717 pub runs: u32,
718 #[arg(
719 long,
720 value_name = "stages",
721 help = "Optional stage subset (build,archive,sbom,sign,checksum,cargo-package,docker). `cargo-package` is harness-only — drives `cargo package --no-verify --allow-dirty` per workspace member to probe `.crate` byte-stability without hitting a registry. `docker` is harness-only — drives `docker buildx build --output=type=oci,rewrite-timestamp=true,dest=…` against `<repo>/Dockerfile` to probe OCI image byte-stability without pushing to a registry; skipped when `docker buildx` is unavailable or no Dockerfile exists."
722 )]
723 pub stages: Option<String>,
724 #[arg(
725 long,
726 value_name = "csv",
727 help = "Restrict the harness to a comma-separated subset of configured target triples. Used by the sharded release workflow so each runner only validates targets it can natively build (Linux runner skips macOS targets, etc.). Forwarded to the child `anodize release --snapshot` subprocess."
728 )]
729 pub targets: Option<String>,
730 #[arg(
731 long,
732 value_name = "path",
733 help = "JSON report path; default dist/run-<id>/determinism.json"
734 )]
735 pub report: Option<PathBuf>,
736 #[arg(
737 long,
738 conflicts_with = "no_snapshot",
739 help = "Force snapshot mode on the child release subprocess (artifacts get a `-SNAPSHOT-<sha>` suffix). Default: auto — snapshot off when HEAD is at a tag, on otherwise."
740 )]
741 pub snapshot: bool,
742 #[arg(
743 long = "no-snapshot",
744 conflicts_with = "snapshot",
745 help = "Force snapshot mode OFF on the child release subprocess (artifacts emit the actual release version). Default: auto — see --snapshot."
746 )]
747 pub no_snapshot: bool,
748 #[arg(
749 long = "inject-drift",
750 value_name = "stage",
751 hide = true,
752 help = "(TEST HARNESS) Append 1 random byte to the first artifact emitted by <stage>. Gated by ANODIZE_TEST_HARNESS=1."
753 )]
754 pub inject_drift: Option<String>,
755 #[arg(
756 long = "preserve-dist",
757 value_name = "path",
758 help = "When the harness greens, copy run-0's `<worktree>/dist/**` to <path> and emit `<path>/context.json` describing the artifact set. The release workflow's publish-only path consumes this to ship the determinism step's output directly (eliminates the redundant `build:` recompilation). Local operators can pass this too — useful for inspecting a hermetic dist tree without re-running the release pipeline."
759 )]
760 pub preserve_dist: Option<PathBuf>,
761 #[arg(
762 long = "crate",
763 value_name = "name",
764 help = "When --preserve-dist is set, write the preserved dist tree to \
765 <dest>/<name>/ instead of directly into <dest>/. Used by the \
766 sharded matrix to produce per-crate subdirectories so a \
767 `release --publish-only` job can merge all crates into a single \
768 dist/ without context.json collision."
769 )]
770 pub crate_name: Option<String>,
771}
772
773fn parse_run_id(s: &str) -> Result<String, String> {
786 anodizer_stage_publish::rollback_only::validate_run_id(s)
787 .map(|()| s.to_string())
788 .map_err(|err| format!("{:#}", err))
789}
790
791pub fn detect_host_target() -> anyhow::Result<String> {
794 anodizer_core::partial::detect_host_target()
795}
796
797pub fn num_cpus() -> usize {
799 std::thread::available_parallelism()
800 .map(|n| n.get())
801 .unwrap_or(4)
802}
803
804pub fn build_cli() -> clap::Command {
806 <Cli as clap::CommandFactory>::command()
807}