Skip to main content

simit/
cli.rs

1use std::ffi::OsString;
2
3use camino::Utf8PathBuf;
4use clap::{Args, Parser, Subcommand, ValueEnum};
5use semver::Version;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Parser)]
9#[command(
10    name = "simit",
11    version,
12    about = "Semver-aware Rust project helper",
13    long_about = "simit helps Rust projects prepare release commits, generate CI workflows, wire formatter/pre-commit hooks, and emit shell integration artifacts."
14)]
15pub struct Cli {
16    #[command(subcommand, help = "Command to run")]
17    pub command: Commands,
18}
19
20#[derive(Debug, Subcommand)]
21pub enum Commands {
22    #[command(about = "Bump package versions, commit the change, and optionally tag it")]
23    Commit(CommitCommand),
24    #[command(about = "Run local release checks, update the changelog, commit, and tag")]
25    Release(ReleaseCommand),
26    #[command(
27        about = "Bootstrap generated project files and distribution skeletons",
28        disable_help_subcommand = true
29    )]
30    Init(InitCommand),
31    #[command(about = "Distribution channel helpers")]
32    Dist(DistCommand),
33    #[command(about = "Manage a Keep a Changelog file")]
34    Changelog(ChangelogCommand),
35    #[command(about = "Install and verify project Git hooks")]
36    Hooks(HooksCommand),
37    #[command(about = "Manage user-scoped simit configuration")]
38    Config(ConfigCommand),
39    #[command(about = "Inspect and maintain the per-user project registry")]
40    Projects(ProjectsCommand),
41    #[command(about = "Apply deterministic simit project upgrades")]
42    Upgrade(UpgradeCommand),
43    #[command(about = "Print shell completion scripts")]
44    Completions(CompletionsCommand),
45    #[command(about = "Print a roff manpage for simit")]
46    Man(ManCommand),
47}
48
49#[derive(Debug, Args)]
50pub struct InitCommand {
51    #[command(subcommand)]
52    pub action: InitAction,
53}
54
55#[derive(Debug, Args)]
56pub struct UpgradeCommand {
57    #[arg(
58        long = "path",
59        value_name = "PATH",
60        action = clap::ArgAction::Append,
61        conflicts_with = "all",
62        help = "Workspace path to upgrade; repeatable. Defaults to the current workspace"
63    )]
64    pub paths: Vec<Utf8PathBuf>,
65    #[arg(
66        long,
67        conflicts_with = "paths",
68        help = "Upgrade every registered non-ephemeral simit-managed project"
69    )]
70    pub all: bool,
71    #[arg(
72        long,
73        conflicts_with = "check",
74        help = "Report intended changes without writing files"
75    )]
76    pub dry_run: bool,
77    #[arg(long, help = "Exit nonzero when an upgrade would change files")]
78    pub check: bool,
79    #[arg(long, help = "Show unified diffs; valid with --dry-run or --check")]
80    pub diff: bool,
81    #[arg(
82        long = "pages-only",
83        help = "Only apply Codeberg Pages workflow upgrades, skipping README badge upgrades"
84    )]
85    pub pages_only: bool,
86}
87
88#[derive(Debug, Args)]
89pub struct DistCommand {
90    #[command(subcommand)]
91    pub action: DistAction,
92}
93
94#[derive(Debug, Subcommand)]
95pub enum InitAction {
96    #[command(about = "Generate or verify Rust CI workflows")]
97    Ci(Box<InitCiCommand>),
98    #[command(about = "Generate or verify a canonical Rust crane flake and hook wiring")]
99    Flake(InitFlakeCommand),
100    #[command(about = "Bootstrap a Homebrew tap repo with a formula skeleton")]
101    HomebrewTap(InitHomebrewTapCommand),
102    #[command(about = "Bootstrap a Chocolatey package directory")]
103    Chocolatey(Box<InitChocolateyCommand>),
104    #[command(about = "Bootstrap a Scoop bucket repo with a manifest skeleton")]
105    ScoopBucket(InitScoopBucketCommand),
106    #[command(about = "Bootstrap AUR PKGBUILDs (source/-bin/-git flavors)")]
107    Aur(InitAurCommand),
108    #[command(about = "Bootstrap a Fedora COPR RPM spec and .copr/Makefile")]
109    Copr(InitCoprCommand),
110    #[command(about = "Bootstrap an apt (reprepro) repository config")]
111    Apt(InitAptCommand),
112    #[command(about = "Generate the comprehensive multi-channel release workflow")]
113    Release(InitReleaseCommand),
114}
115
116#[derive(Debug, Subcommand)]
117pub enum DistAction {
118    #[command(about = "Homebrew formula helpers (render, bump, push)")]
119    Homebrew(HomebrewCommand),
120    #[command(about = "Chocolatey package helpers (render, bump, push)")]
121    Chocolatey(ChocolateyCommand),
122    #[command(about = "Scoop manifest helpers (render, bump, push)")]
123    Scoop(ScoopCommand),
124    #[command(about = "Winget manifest submission helpers")]
125    Winget(WingetCommand),
126    #[command(about = "Windows community packager publishing helpers")]
127    Windows(WindowsCommand),
128    #[command(about = "AUR PKGBUILD helpers (render)")]
129    Aur(AurCommand),
130    #[command(about = "Fedora COPR helpers (render)")]
131    Copr(CoprCommand),
132    #[command(about = "apt repository helpers (render)")]
133    Apt(AptCommand),
134}
135
136#[derive(Debug, Args)]
137pub struct CommitCommand {
138    #[arg(
139        long = "package",
140        value_name = "NAME",
141        help = "Workspace package to bump; may be repeated"
142    )]
143    pub packages: Vec<String>,
144    #[arg(long, help = "Bump every package in the workspace")]
145    pub workspace: bool,
146    #[arg(long = "no-tag", help = "Skip creating the release tag")]
147    pub no_tag: bool,
148    #[arg(
149        long = "no-sign",
150        help = "Create an unsigned tag instead of a signed tag"
151    )]
152    pub no_sign: bool,
153    #[arg(long, help = "Print the planned release commit without changing files")]
154    pub dry_run: bool,
155    #[arg(value_enum, value_name = "BUMP", help = "Version bump to apply")]
156    pub bump: BumpKind,
157    #[arg(
158        long = "pre",
159        value_name = "ID",
160        help = "Prerelease identifier to attach, such as alpha.1 or rc.1"
161    )]
162    pub pre: Option<String>,
163    #[arg(
164        value_name = "GIT_ARGS",
165        trailing_var_arg = true,
166        allow_hyphen_values = true,
167        help = "Arguments passed through to git commit, such as -m <message>"
168    )]
169    pub git_args: Vec<OsString>,
170}
171
172#[derive(Debug, Args)]
173pub struct ReleaseCommand {
174    #[arg(
175        long = "package",
176        value_name = "NAME",
177        help = "Workspace package to release; may be repeated"
178    )]
179    pub packages: Vec<String>,
180    #[arg(long, help = "Release every package in the workspace")]
181    pub workspace: bool,
182    #[arg(long = "no-tag", help = "Skip creating the release tag")]
183    pub no_tag: bool,
184    #[arg(
185        long = "no-sign",
186        help = "Create an unsigned tag instead of a signed tag"
187    )]
188    pub no_sign: bool,
189    #[arg(long, help = "Print the planned release without changing files")]
190    pub dry_run: bool,
191    #[arg(
192        value_enum,
193        value_name = "ACTION",
194        help = "Version bump to apply, or sync-up to rerun a failed release from HEAD"
195    )]
196    pub action: ReleaseAction,
197    #[arg(value_enum, value_name = "TRUST_ACTION", help = "Release trust action")]
198    pub trust_action: Option<ReleaseTrustAction>,
199    #[arg(
200        value_enum,
201        value_name = "SECRETS_ACTION",
202        help = "Release secrets action"
203    )]
204    pub secrets_action: Option<ReleaseSecretsAction>,
205    #[arg(
206        long = "key",
207        value_name = "FINGERPRINT",
208        help = "OpenPGP key fingerprint for `simit release trust`"
209    )]
210    pub trust_key: Option<String>,
211    #[arg(
212        long = "trust-root",
213        value_name = "PATH",
214        help = "Path to the release maintainer public keyring"
215    )]
216    pub trust_root: Option<Utf8PathBuf>,
217    #[arg(
218        long = "pre",
219        value_name = "ID",
220        help = "Prerelease identifier to attach, such as alpha.1 or rc.1"
221    )]
222    pub pre: Option<String>,
223    #[arg(
224        short = 'm',
225        long = "message",
226        value_name = "MESSAGE",
227        help = "Release commit message"
228    )]
229    pub message: Option<String>,
230    #[arg(
231        long = "no-changelog",
232        help = "Skip promoting CHANGELOG.md even when it exists"
233    )]
234    pub no_changelog: bool,
235    #[arg(long, help = "Push a sync-up tag move to the remote")]
236    pub push: bool,
237    #[arg(
238        long,
239        value_name = "REMOTE",
240        default_value = "origin",
241        help = "Remote to push sync-up tag moves to"
242    )]
243    pub remote: String,
244    #[arg(
245        long,
246        help = "Print machine-readable JSON for `simit release verify`, `simit release plan`, or `simit release secrets contract`"
247    )]
248    pub json: bool,
249    #[arg(
250        long = "dry-run-package",
251        help = "Run `cargo package` checks for each crate selected by `simit release plan`"
252    )]
253    pub dry_run_package: bool,
254    #[arg(
255        long = "version",
256        value_name = "VERSION",
257        help = "Version to verify instead of the selected package's current Cargo.toml version"
258    )]
259    pub verify_version: Option<Version>,
260    #[arg(
261        long = "push-target",
262        value_name = "REMOTE",
263        help = "Remote checked by `simit release verify` for the release tag"
264    )]
265    pub push_target: Option<String>,
266    #[arg(
267        long = "repo",
268        value_name = "OWNER/REPO",
269        help = "Forgejo/Codeberg repository for `simit release secrets`"
270    )]
271    pub secrets_repo: Option<String>,
272    #[arg(
273        long = "api-base",
274        value_name = "URL",
275        default_value = "https://codeberg.org/api/v1",
276        help = "Forgejo/Codeberg API base URL for `simit release secrets`"
277    )]
278    pub secrets_api_base: String,
279    #[arg(
280        long = "token-file",
281        value_name = "PATH",
282        help = "File containing the Forgejo/Codeberg API token for `simit release secrets`"
283    )]
284    pub secrets_token_file: Option<Utf8PathBuf>,
285    #[arg(
286        long = "assume-account-secret",
287        value_name = "NAME",
288        action = clap::ArgAction::Append,
289        help = "Secret name managed at account/org scope and not visible in repo secret listings"
290    )]
291    pub assumed_account_secrets: Vec<String>,
292    #[arg(
293        long = "minisign-secret-key-file",
294        value_name = "PATH",
295        help = "Existing minisign secret key file to import as MINISIGN_SECRET_KEY"
296    )]
297    pub minisign_secret_key_file: Option<Utf8PathBuf>,
298    #[arg(
299        long = "minisign-password-file",
300        value_name = "PATH",
301        help = "Existing minisign password file to import as MINISIGN_PASSWORD"
302    )]
303    pub minisign_password_file: Option<Utf8PathBuf>,
304    #[arg(
305        long = "minisign-public-key",
306        value_name = "PATH",
307        default_value = "keys/minisign.pub",
308        help = "Committed minisign public key path"
309    )]
310    pub minisign_public_key: Utf8PathBuf,
311    #[arg(
312        long = "rotate-minisign",
313        help = "Generate a new encrypted minisign keypair and replace the committed public key"
314    )]
315    pub rotate_minisign: bool,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
319pub enum ReleaseAction {
320    #[value(help = "Increment patch: 1.2.3 -> 1.2.4")]
321    Patch,
322    #[value(help = "Increment minor and reset patch: 1.2.3 -> 1.3.0")]
323    Minor,
324    #[value(help = "Increment major and reset minor/patch: 1.2.3 -> 2.0.0")]
325    Major,
326    #[value(help = "Keep the numeric version and replace prerelease metadata")]
327    Prerelease,
328    #[value(
329        name = "sync-up",
330        help = "Move the current-version release tag to HEAD"
331    )]
332    SyncUp,
333    #[value(name = "trust", help = "Manage release maintainer trust roots")]
334    Trust,
335    #[value(
336        name = "verify",
337        help = "Run the read-only release-bar verification report"
338    )]
339    Verify,
340    #[value(
341        name = "plan",
342        help = "Print the dependency-ordered publish plan for the current workspace"
343    )]
344    Plan,
345    #[value(name = "secrets", help = "Manage remote release workflow secrets")]
346    Secrets,
347}
348
349impl ReleaseAction {
350    pub fn bump_kind(self) -> Option<BumpKind> {
351        match self {
352            Self::Patch => Some(BumpKind::Patch),
353            Self::Minor => Some(BumpKind::Minor),
354            Self::Major => Some(BumpKind::Major),
355            Self::Prerelease => Some(BumpKind::Prerelease),
356            Self::SyncUp | Self::Trust | Self::Verify | Self::Plan | Self::Secrets => None,
357        }
358    }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
362pub enum ReleaseTrustAction {
363    #[value(help = "Show detected release signing key and trust-root state")]
364    Status,
365    #[value(help = "Export the maintainer public key into the release trust root")]
366    Init,
367    #[value(help = "Verify the release trust root exists and matches the signing key")]
368    Check,
369    #[value(
370        name = "inspect-minisign-input",
371        help = "Classify minisign import inputs without printing secret values"
372    )]
373    InspectMinisignInput,
374    #[value(name = "contract", help = "Print the release secrets contract")]
375    Contract,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
379pub enum ReleaseSecretsAction {
380    #[value(help = "Upload or rotate release workflow secrets")]
381    Init,
382    #[value(help = "Check release workflow secret names without reading values")]
383    Check,
384    #[value(
385        name = "contract",
386        help = "Print the generic release credential contract as JSON"
387    )]
388    Contract,
389    #[value(
390        name = "inspect-minisign-input",
391        help = "Classify minisign import inputs without printing secret values"
392    )]
393    InspectMinisignInput,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
397pub enum BumpKind {
398    #[value(help = "Increment patch: 1.2.3 -> 1.2.4")]
399    Patch,
400    #[value(help = "Increment minor and reset patch: 1.2.3 -> 1.3.0")]
401    Minor,
402    #[value(help = "Increment major and reset minor/patch: 1.2.3 -> 2.0.0")]
403    Major,
404    #[value(help = "Keep the numeric version and replace prerelease metadata")]
405    Prerelease,
406}
407
408#[derive(Debug, Args)]
409pub struct InitCiCommand {
410    #[arg(
411        long = "package",
412        value_name = "NAME",
413        conflicts_with = "workspace",
414        help = "Workspace package to generate CI for; may be repeated"
415    )]
416    pub packages: Vec<String>,
417    #[arg(long, help = "Generate CI for every package in the workspace")]
418    pub workspace: bool,
419    #[arg(
420        long,
421        value_enum,
422        value_name = "PLATFORM",
423        help = "Workflow platform to generate"
424    )]
425    pub platform: Platform,
426    #[arg(
427        long,
428        value_enum,
429        value_name = "RUNTIME",
430        help = "Command runtime used inside generated workflows"
431    )]
432    pub runtime: Option<RuntimeChoice>,
433    #[arg(
434        long,
435        value_name = "LABEL",
436        help = "Override the generated runner label for Linux jobs"
437    )]
438    pub runner: Option<String>,
439    #[arg(
440        long = "windows-runner",
441        value_name = "LABEL",
442        help = "Override the generated runner label for Windows artifact jobs"
443    )]
444    pub windows_runner: Option<String>,
445    #[arg(
446        long = "step-runner",
447        value_name = "STEP=LABEL",
448        help = "Override runner for a specific step key (repeatable, e.g. --step-runner nix-check=atlas-nix-trusted)"
449    )]
450    pub step_runner: Vec<String>,
451    #[arg(
452        long,
453        help = "Generate granular split of CI steps across Codeberg runner tiers (fmt→tiny, clippy/deny/audit→small, test/doc/package→medium); forgejo-only"
454    )]
455    pub granular: bool,
456    #[arg(
457        long = "maintainer-key",
458        value_name = "FINGERPRINT",
459        help = "OpenPGP key fingerprint to export into keys/maintainers.gpg"
460    )]
461    pub maintainer_key: Option<String>,
462    #[arg(
463        long = "maintainers-gpg",
464        value_name = "PATH",
465        help = "Path to the generated maintainer public keyring"
466    )]
467    pub maintainers_gpg: Option<Utf8PathBuf>,
468    #[arg(
469        long = "release-smoke-command",
470        value_name = "COMMAND",
471        help = "Command to run after signing release artifacts and before publishing"
472    )]
473    pub release_smoke_command: Option<String>,
474    #[arg(long, help = "Verify committed workflow files match generated output")]
475    pub check: bool,
476    #[arg(long, help = "Show a unified diff when --check finds stale files")]
477    pub diff: bool,
478    #[arg(
479        long = "with-nextest",
480        num_args = 0..=1,
481        default_missing_value = "true",
482        action = clap::ArgAction::Set,
483        help = "Use cargo-nextest for test steps; pass `--with-nextest=false` to override project config"
484    )]
485    pub with_nextest: Option<bool>,
486    #[arg(
487        long = "with-msrv",
488        num_args = 0..=1,
489        default_missing_value = "true",
490        action = clap::ArgAction::Set,
491        help = "Add an MSRV check using package.rust-version; pass `--with-msrv=false` to override project config"
492    )]
493    pub with_msrv: Option<bool>,
494    #[arg(
495        long = "with-audit",
496        num_args = 0..=1,
497        default_missing_value = "true",
498        action = clap::ArgAction::Set,
499        help = "Install and run cargo-audit; pass `--with-audit=false` to override project config"
500    )]
501    pub with_audit: Option<bool>,
502    #[arg(
503        long = "with-deny",
504        num_args = 0..=1,
505        default_missing_value = "true",
506        action = clap::ArgAction::Set,
507        help = "Install and run cargo-deny; pass `--with-deny=false` to override project config"
508    )]
509    pub with_deny: Option<bool>,
510    #[arg(
511        long = "with-docs",
512        num_args = 0..=1,
513        default_missing_value = "true",
514        action = clap::ArgAction::Set,
515        help = "Build package documentation in CI; pass `--with-docs=false` to override project config"
516    )]
517    pub with_docs: Option<bool>,
518    #[arg(
519        long = "with-om-ci",
520        num_args = 0..=1,
521        default_missing_value = "true",
522        action = clap::ArgAction::Set,
523        help = "Replace flake check and cargo dev-shell steps with om ci run (requires --runtime nix)"
524    )]
525    pub with_om_ci: Option<bool>,
526    #[arg(
527        long = "om-ci-augment",
528        num_args = 0..=1,
529        default_missing_value = "true",
530        action = clap::ArgAction::Set,
531        help = "Run om ci run alongside the legacy cargo dev-shell steps; implies --with-om-ci"
532    )]
533    pub om_ci_augment: Option<bool>,
534    #[arg(
535        long = "omnix-ref",
536        value_name = "REF",
537        help = "Override the pinned omnix flakeref"
538    )]
539    pub omnix_ref: Option<String>,
540    #[arg(
541        long = "with-artifacts",
542        num_args = 0..=1,
543        default_missing_value = "true",
544        action = clap::ArgAction::Set,
545        help = "Generate a tagged-release artifact workflow; pass `--with-artifacts=false` to override project config"
546    )]
547    pub with_artifacts: Option<bool>,
548    #[arg(
549        long = "with-pypi-publish",
550        num_args = 0..=1,
551        default_missing_value = "true",
552        action = clap::ArgAction::Set,
553        help = "Generate a PyPI publish workflow for Python uv projects; pass `--with-pypi-publish=false` to override project config"
554    )]
555    pub with_pypi_publish: Option<bool>,
556    #[arg(
557        long = "publish-crates",
558        num_args = 0..=1,
559        default_missing_value = "true",
560        action = clap::ArgAction::Set,
561        help = "Generate crates.io publish workflows for publishable Rust packages; pass `--publish-crates=false` for CI-only projects"
562    )]
563    pub publish_crates: Option<bool>,
564    #[arg(
565        long = "with-homebrew",
566        help = "Add a Homebrew tap publishing step (forgejo + nix only)"
567    )]
568    pub with_homebrew: bool,
569    #[arg(
570        long = "with-chocolatey",
571        help = "Validate Chocolatey package publishing configuration"
572    )]
573    pub with_chocolatey: bool,
574    #[arg(
575        long = "with-scoop",
576        help = "Validate Scoop bucket publishing configuration"
577    )]
578    pub with_scoop: bool,
579    #[arg(
580        long = "with-codeberg-pages",
581        help = "Generate a Forgejo workflow that publishes .#site with .#deploy-pages"
582    )]
583    pub with_codeberg_pages: bool,
584    #[arg(
585        long = "with-vscode",
586        help = "Generate a Forgejo workflow that publishes a VS Code/Open VSX extension"
587    )]
588    pub with_vscode: bool,
589    #[arg(
590        long = "pages-repo",
591        value_name = "OWNER/REPO",
592        requires = "with_codeberg_pages",
593        help = "Codeberg repository receiving the generated pages branch"
594    )]
595    pub pages_repo: Option<String>,
596    #[arg(
597        long = "pages-canonical-domain",
598        value_name = "DOMAIN",
599        requires = "with_codeberg_pages",
600        help = "Canonical domain that must appear in the generated Pages .domains file"
601    )]
602    pub pages_canonical_domain: Option<String>,
603    #[arg(
604        long = "pages-site-output",
605        value_name = "INSTALLABLE",
606        requires = "with_codeberg_pages",
607        help = "Nix installable used to build the Pages site for domain validation"
608    )]
609    pub pages_site_output: Option<String>,
610    #[arg(
611        long = "pages-token-secret",
612        value_name = "SECRET",
613        requires = "with_codeberg_pages",
614        help = "Actions secret exposed as CODEBERG_TOKEN for Pages deployment"
615    )]
616    pub pages_token_secret: Option<String>,
617    #[arg(
618        long = "pages-source-branch",
619        value_name = "BRANCH",
620        requires = "with_codeberg_pages",
621        help = "Branch whose pushes publish Codeberg Pages"
622    )]
623    pub pages_source_branch: Option<String>,
624    #[arg(
625        long = "pages-deploy-app",
626        value_name = "INSTALLABLE",
627        requires = "with_codeberg_pages",
628        help = "Nix app used to build and push the generated Pages branch"
629    )]
630    pub pages_deploy_app: Option<String>,
631    #[command(flatten)]
632    pub homebrew: HomebrewOverridesArgs,
633    #[command(flatten)]
634    pub chocolatey: ChocolateyOverridesArgs,
635    #[command(flatten)]
636    pub scoop: ScoopOverridesArgs,
637}
638
639#[derive(Debug, Args)]
640pub struct HomebrewOverridesArgs {
641    #[arg(
642        long = "homebrew-name",
643        value_name = "NAME",
644        help = "Homebrew formula name"
645    )]
646    pub name: Option<String>,
647    #[arg(
648        long = "homebrew-tap",
649        id = "homebrew_tap",
650        value_name = "URL",
651        help = "Homebrew tap repo URL, e.g. https://codeberg.org/foo/homebrew-bar.git"
652    )]
653    pub tap: Option<String>,
654    #[arg(
655        long = "homebrew-binary",
656        value_name = "NAME",
657        help = "Binary to install via the formula; repeatable"
658    )]
659    pub binary: Vec<String>,
660    #[arg(
661        long = "homebrew-description",
662        value_name = "TEXT",
663        help = "Formula description (<= 80 chars)"
664    )]
665    pub description: Option<String>,
666    #[arg(
667        long = "homebrew-homepage",
668        value_name = "URL",
669        help = "Project homepage URL"
670    )]
671    pub homepage: Option<String>,
672    #[arg(
673        long = "homebrew-license",
674        value_name = "SPDX",
675        help = "SPDX license identifier (defaults to package.license)"
676    )]
677    pub license: Option<String>,
678    #[arg(
679        long = "homebrew-download-repo",
680        value_name = "OWNER/REPO",
681        help = "Codeberg/GitHub owner/repo for release downloads"
682    )]
683    pub download_repo: Option<String>,
684    #[arg(
685        long = "homebrew-archive-pattern",
686        value_name = "PATTERN",
687        help = "Filename pattern for release archives"
688    )]
689    pub archive_pattern: Option<String>,
690    #[arg(
691        long = "homebrew-no-platform",
692        value_name = "KEY",
693        help = "Disable a platform (darwin_arm|darwin_intel|linux_arm|linux_intel); repeatable"
694    )]
695    pub no_platform: Vec<String>,
696}
697
698impl HomebrewOverridesArgs {
699    pub fn as_overrides(&self) -> crate::config::HomebrewOverrides<'_> {
700        crate::config::HomebrewOverrides {
701            name: self.name.as_deref(),
702            binaries: Some(&self.binary),
703            tap_url: self.tap.as_deref(),
704            description: self.description.as_deref(),
705            homepage: self.homepage.as_deref(),
706            license: self.license.as_deref(),
707            download_repo: self.download_repo.as_deref(),
708            archive_pattern: self.archive_pattern.as_deref(),
709            disabled_platforms: &self.no_platform,
710        }
711    }
712}
713
714#[derive(Debug, Default, Args)]
715pub struct ChocolateyOverridesArgs {
716    #[arg(
717        long = "choco-name",
718        id = "choco_name",
719        value_name = "NAME",
720        help = "Chocolatey package display name"
721    )]
722    pub name: Option<String>,
723    #[arg(
724        long = "choco-id",
725        id = "choco_id",
726        value_name = "ID",
727        help = "Chocolatey nuspec package id"
728    )]
729    pub id: Option<String>,
730    #[arg(
731        long = "choco-title",
732        id = "choco_title",
733        value_name = "TEXT",
734        help = "Chocolatey nuspec title"
735    )]
736    pub title: Option<String>,
737    #[arg(
738        long = "choco-authors",
739        id = "choco_authors",
740        value_name = "TEXT",
741        help = "Chocolatey nuspec authors"
742    )]
743    pub authors: Option<String>,
744    #[arg(
745        long = "choco-description",
746        id = "choco_description",
747        value_name = "TEXT",
748        help = "Chocolatey package description"
749    )]
750    pub description: Option<String>,
751    #[arg(
752        long = "choco-summary",
753        id = "choco_summary",
754        value_name = "TEXT",
755        help = "Chocolatey package summary"
756    )]
757    pub summary: Option<String>,
758    #[arg(
759        long = "choco-project-url",
760        id = "choco_project_url",
761        value_name = "URL",
762        help = "Chocolatey project URL"
763    )]
764    pub project_url: Option<String>,
765    #[arg(
766        long = "choco-license-url",
767        id = "choco_license_url",
768        value_name = "URL",
769        help = "Chocolatey license URL"
770    )]
771    pub license_url: Option<String>,
772    #[arg(
773        long = "choco-icon-url",
774        id = "choco_icon_url",
775        value_name = "URL",
776        help = "Chocolatey icon URL"
777    )]
778    pub icon_url: Option<String>,
779    #[arg(
780        long = "choco-package-source-url",
781        id = "choco_package_source_url",
782        value_name = "URL",
783        help = "Chocolatey package source URL"
784    )]
785    pub package_source_url: Option<String>,
786    #[arg(
787        long = "choco-docs-url",
788        id = "choco_docs_url",
789        value_name = "URL",
790        help = "Chocolatey documentation URL"
791    )]
792    pub docs_url: Option<String>,
793    #[arg(
794        long = "choco-bug-tracker-url",
795        id = "choco_bug_tracker_url",
796        value_name = "URL",
797        help = "Chocolatey bug tracker URL"
798    )]
799    pub bug_tracker_url: Option<String>,
800    #[arg(
801        long = "choco-project-source-url",
802        id = "choco_project_source_url",
803        value_name = "URL",
804        help = "Chocolatey project source URL"
805    )]
806    pub project_source_url: Option<String>,
807    #[arg(
808        long = "choco-tags",
809        id = "choco_tags",
810        value_name = "TEXT",
811        help = "Chocolatey package tags"
812    )]
813    pub tags: Option<String>,
814    #[arg(
815        long = "choco-release-notes-url",
816        id = "choco_release_notes_url",
817        value_name = "URL",
818        help = "Chocolatey release notes URL"
819    )]
820    pub release_notes_url: Option<String>,
821    #[arg(
822        long = "choco-download-repo",
823        id = "choco_download_repo",
824        value_name = "OWNER/REPO",
825        help = "Codeberg/GitHub owner/repo for Chocolatey release downloads"
826    )]
827    pub download_repo: Option<String>,
828    #[arg(
829        long = "choco-archive-pattern",
830        id = "choco_archive_pattern",
831        value_name = "PATTERN",
832        help = "Filename pattern for Chocolatey release archives"
833    )]
834    pub archive_pattern: Option<String>,
835    #[arg(
836        long = "choco-push-source",
837        id = "choco_push_source",
838        value_name = "URL",
839        help = "Chocolatey push source URL"
840    )]
841    pub push_source: Option<String>,
842}
843
844impl ChocolateyOverridesArgs {
845    pub fn as_overrides(&self) -> crate::config::ChocolateyOverrides<'_> {
846        crate::config::ChocolateyOverrides {
847            name: self.name.as_deref(),
848            id: self.id.as_deref(),
849            title: self.title.as_deref(),
850            authors: self.authors.as_deref(),
851            description: self.description.as_deref(),
852            summary: self.summary.as_deref(),
853            project_url: self.project_url.as_deref(),
854            license_url: self.license_url.as_deref(),
855            icon_url: self.icon_url.as_deref(),
856            package_source_url: self.package_source_url.as_deref(),
857            docs_url: self.docs_url.as_deref(),
858            bug_tracker_url: self.bug_tracker_url.as_deref(),
859            project_source_url: self.project_source_url.as_deref(),
860            tags: self.tags.as_deref(),
861            release_notes_url: self.release_notes_url.as_deref(),
862            download_repo: self.download_repo.as_deref(),
863            archive_pattern: self.archive_pattern.as_deref(),
864            push_source: self.push_source.as_deref(),
865        }
866    }
867}
868
869#[derive(Debug, Default, Args)]
870pub struct ScoopOverridesArgs {
871    #[arg(
872        long = "scoop-name",
873        id = "scoop_name",
874        value_name = "NAME",
875        help = "Scoop manifest name"
876    )]
877    pub name: Option<String>,
878    #[arg(
879        long = "scoop-bucket",
880        id = "scoop_bucket",
881        value_name = "URL",
882        help = "Scoop bucket repo URL"
883    )]
884    pub bucket: Option<String>,
885    #[arg(
886        long = "scoop-description",
887        id = "scoop_description",
888        value_name = "TEXT",
889        help = "Scoop manifest description"
890    )]
891    pub description: Option<String>,
892    #[arg(
893        long = "scoop-homepage",
894        id = "scoop_homepage",
895        value_name = "URL",
896        help = "Scoop manifest homepage"
897    )]
898    pub homepage: Option<String>,
899    #[arg(
900        long = "scoop-license",
901        id = "scoop_license",
902        value_name = "SPDX",
903        help = "Scoop manifest license"
904    )]
905    pub license: Option<String>,
906    #[arg(
907        long = "scoop-download-repo",
908        id = "scoop_download_repo",
909        value_name = "OWNER/REPO",
910        help = "Codeberg/GitHub owner/repo for Scoop release downloads"
911    )]
912    pub download_repo: Option<String>,
913    #[arg(
914        long = "scoop-archive-pattern",
915        id = "scoop_archive_pattern",
916        value_name = "PATTERN",
917        help = "Filename pattern for Scoop release archives"
918    )]
919    pub archive_pattern: Option<String>,
920    #[arg(
921        long = "scoop-binary",
922        id = "scoop_binary",
923        value_name = "NAME",
924        help = "Binary to expose in the Scoop manifest; repeatable"
925    )]
926    pub binary: Vec<String>,
927    #[arg(
928        long = "scoop-no-arch",
929        id = "scoop_no_arch",
930        value_name = "KEY",
931        help = "Disable an architecture (x64|arm64); repeatable"
932    )]
933    pub no_arch: Vec<String>,
934}
935
936impl ScoopOverridesArgs {
937    pub fn as_overrides(&self) -> crate::config::ScoopOverrides<'_> {
938        crate::config::ScoopOverrides {
939            name: self.name.as_deref(),
940            bucket_url: self.bucket.as_deref(),
941            description: self.description.as_deref(),
942            homepage: self.homepage.as_deref(),
943            license: self.license.as_deref(),
944            download_repo: self.download_repo.as_deref(),
945            archive_pattern: self.archive_pattern.as_deref(),
946            binaries: Some(&self.binary),
947            disabled_architectures: &self.no_arch,
948        }
949    }
950}
951
952#[derive(Debug, Args)]
953pub struct InitHomebrewTapCommand {
954    #[arg(
955        long,
956        value_name = "DIR",
957        help = "Path to the tap repo to bootstrap (created if missing)"
958    )]
959    pub target: Utf8PathBuf,
960    #[arg(
961        long,
962        help = "Verify the existing Formula/<name>.rb matches the rendered template"
963    )]
964    pub check: bool,
965    #[arg(long, help = "Show a unified diff when --check finds drift")]
966    pub diff: bool,
967    #[arg(long, help = "Print the rendered formula without writing files")]
968    pub print: bool,
969    #[arg(long, help = "Skip `git init` and remote wiring (formula write only)")]
970    pub no_git: bool,
971    #[command(flatten)]
972    pub homebrew: HomebrewOverridesArgs,
973}
974
975#[derive(Debug, Args)]
976pub struct HomebrewCommand {
977    #[command(subcommand)]
978    pub action: HomebrewAction,
979}
980
981#[derive(Debug, Subcommand)]
982pub enum HomebrewAction {
983    #[command(about = "Render the formula to stdout or a file (no sha256)")]
984    Render(HomebrewRenderArgs),
985    #[command(about = "Render with real sha256 sums and write to a tap repo")]
986    Bump(HomebrewBumpArgs),
987}
988
989#[derive(Debug, Args)]
990pub struct HomebrewRenderArgs {
991    #[arg(
992        long,
993        value_name = "VERSION",
994        help = "Version to render (no leading 'v'). Defaults to cargo package.version"
995    )]
996    pub version: Option<String>,
997    #[arg(long, value_name = "PATH", help = "Write to PATH instead of stdout")]
998    pub output: Option<Utf8PathBuf>,
999    #[command(flatten)]
1000    pub homebrew: HomebrewOverridesArgs,
1001}
1002
1003#[derive(Debug, Args)]
1004pub struct HomebrewBumpArgs {
1005    #[arg(
1006        long,
1007        value_name = "VERSION",
1008        help = "Release version (no leading 'v')"
1009    )]
1010    pub version: String,
1011    #[arg(
1012        long,
1013        value_name = "DIR",
1014        help = "Tap repo directory (Formula/<name>.rb written here)"
1015    )]
1016    pub tap: Utf8PathBuf,
1017    #[arg(
1018        long,
1019        value_name = "PLATFORM=PATH",
1020        help = "Per-platform archive path for sha256 computation"
1021    )]
1022    pub archive: Vec<String>,
1023    #[arg(
1024        long,
1025        help = "After writing, run git add + commit + push. Requires git credentials"
1026    )]
1027    pub push: bool,
1028    #[arg(
1029        long,
1030        value_name = "MESSAGE",
1031        help = "Commit message (defaults to '<name> <version>')"
1032    )]
1033    pub commit_message: Option<String>,
1034    #[command(flatten)]
1035    pub homebrew: HomebrewOverridesArgs,
1036}
1037
1038#[derive(Debug, Args)]
1039pub struct InitScoopBucketCommand {
1040    #[arg(
1041        long,
1042        value_name = "DIR",
1043        help = "Path to the bucket repo to bootstrap (created if missing)"
1044    )]
1045    pub target: Utf8PathBuf,
1046    #[arg(
1047        long,
1048        help = "Verify the existing bucket/<name>.json matches the rendered template"
1049    )]
1050    pub check: bool,
1051    #[arg(long, help = "Show a unified diff when --check finds drift")]
1052    pub diff: bool,
1053    #[arg(long, help = "Print the rendered manifest without writing files")]
1054    pub print: bool,
1055    #[arg(long, help = "Skip `git init` and remote wiring (manifest write only)")]
1056    pub no_git: bool,
1057    #[command(flatten)]
1058    pub scoop: ScoopOverridesArgs,
1059}
1060
1061#[derive(Debug, Args)]
1062pub struct ScoopCommand {
1063    #[command(subcommand)]
1064    pub action: ScoopAction,
1065}
1066
1067#[derive(Debug, Subcommand)]
1068pub enum ScoopAction {
1069    #[command(about = "Render the manifest to stdout or a file with placeholder hashes")]
1070    Render(ScoopRenderArgs),
1071    #[command(about = "Render with real sha256 sums and write to a bucket repo")]
1072    Bump(ScoopBumpArgs),
1073}
1074
1075#[derive(Debug, Args)]
1076pub struct ScoopRenderArgs {
1077    #[arg(
1078        long,
1079        value_name = "VERSION",
1080        help = "Version to render (no leading 'v'). Defaults to cargo package.version"
1081    )]
1082    pub version: Option<String>,
1083    #[arg(long, value_name = "PATH", help = "Write to PATH instead of stdout")]
1084    pub output: Option<Utf8PathBuf>,
1085    #[command(flatten)]
1086    pub scoop: ScoopOverridesArgs,
1087}
1088
1089#[derive(Debug, Args)]
1090pub struct ScoopBumpArgs {
1091    #[arg(
1092        long,
1093        value_name = "VERSION",
1094        help = "Release version (no leading 'v')"
1095    )]
1096    pub version: String,
1097    #[arg(
1098        long,
1099        value_name = "DIR",
1100        help = "Bucket repo directory (bucket/<name>.json written here)"
1101    )]
1102    pub bucket: Option<Utf8PathBuf>,
1103    #[arg(
1104        long = "bucket-url",
1105        value_name = "URL",
1106        help = "Clone this bucket repo URL before writing when --bucket is omitted"
1107    )]
1108    pub bucket_url: Option<String>,
1109    #[arg(
1110        long = "bucket-token-env",
1111        value_name = "ENV",
1112        help = "Environment variable containing a token for authenticated HTTPS bucket pushes"
1113    )]
1114    pub bucket_token_env: Option<String>,
1115    #[arg(
1116        long = "work-dir",
1117        value_name = "DIR",
1118        help = "Working directory for cloned bucket repos (defaults to target/simit-scoop)"
1119    )]
1120    pub work_dir: Option<Utf8PathBuf>,
1121    #[arg(
1122        long,
1123        value_name = "ARCH=PATH",
1124        help = "Per-architecture archive path for sha256 computation"
1125    )]
1126    pub archive: Vec<String>,
1127    #[arg(
1128        long,
1129        help = "After writing, run git add + commit + push. Requires git credentials"
1130    )]
1131    pub push: bool,
1132    #[arg(
1133        long,
1134        value_name = "MESSAGE",
1135        help = "Commit message (defaults to '<name> <version>')"
1136    )]
1137    pub commit_message: Option<String>,
1138    #[arg(
1139        long,
1140        help = "Print the planned bucket update without writing or pushing"
1141    )]
1142    pub dry_run: bool,
1143    #[command(flatten)]
1144    pub scoop: ScoopOverridesArgs,
1145}
1146
1147#[derive(Debug, Args)]
1148pub struct WingetCommand {
1149    #[command(subcommand)]
1150    pub action: WingetAction,
1151}
1152
1153#[derive(Debug, Subcommand)]
1154pub enum WingetAction {
1155    #[command(about = "Submit a winget-pkgs manifest update with wingetcreate")]
1156    Submit(WingetSubmitArgs),
1157}
1158
1159#[derive(Debug, Args)]
1160pub struct WingetSubmitArgs {
1161    #[arg(
1162        long,
1163        value_name = "VERSION",
1164        help = "Release version (no leading 'v')"
1165    )]
1166    pub version: String,
1167    #[arg(
1168        long = "package-id",
1169        value_name = "ID",
1170        help = "Winget PackageIdentifier"
1171    )]
1172    pub package_id: Option<String>,
1173    #[arg(
1174        long = "download-repo",
1175        value_name = "OWNER/REPO",
1176        help = "Release download repo"
1177    )]
1178    pub download_repo: Option<String>,
1179    #[arg(
1180        long = "zip-archive",
1181        value_name = "NAME",
1182        help = "Windows zip filename or pattern containing {version}"
1183    )]
1184    pub zip_archive: Option<String>,
1185    #[arg(long = "url", value_name = "URL", help = "Installer URL to submit")]
1186    pub url: Option<String>,
1187    #[arg(
1188        long = "token-env",
1189        value_name = "ENV",
1190        help = "Environment variable containing the GitHub PAT"
1191    )]
1192    pub token_env: Option<String>,
1193    #[arg(
1194        long = "wingetcreate",
1195        value_name = "PATH",
1196        help = "wingetcreate executable path"
1197    )]
1198    pub wingetcreate: Option<Utf8PathBuf>,
1199    #[arg(
1200        long = "wingetcreate-url",
1201        value_name = "URL",
1202        help = "Download wingetcreate.exe from this URL instead of GitHub latest"
1203    )]
1204    pub wingetcreate_url: Option<String>,
1205    #[arg(
1206        long = "work-dir",
1207        value_name = "DIR",
1208        help = "Working directory for downloaded wingetcreate.exe"
1209    )]
1210    pub work_dir: Option<Utf8PathBuf>,
1211    #[arg(
1212        long = "wine",
1213        value_name = "PATH",
1214        default_value = "wine",
1215        help = "Wine executable used to run wingetcreate"
1216    )]
1217    pub wine: String,
1218    #[arg(long, help = "Print the wingetcreate command without running it")]
1219    pub dry_run: bool,
1220}
1221
1222#[derive(Debug, Args)]
1223pub struct WindowsCommand {
1224    #[command(subcommand)]
1225    pub action: WindowsAction,
1226}
1227
1228#[derive(Debug, Subcommand)]
1229pub enum WindowsAction {
1230    #[command(about = "Publish configured Windows package channels")]
1231    Publish(WindowsPublishArgs),
1232}
1233
1234#[derive(Debug, Args)]
1235pub struct WindowsPublishArgs {
1236    #[arg(
1237        long,
1238        value_name = "VERSION",
1239        help = "Release version (no leading 'v')"
1240    )]
1241    pub version: String,
1242    #[arg(
1243        long,
1244        value_name = "ARCH=PATH_OR_URL",
1245        help = "Per-architecture release archive; local paths are required for checksum-based channels"
1246    )]
1247    pub archive: Vec<String>,
1248    #[arg(
1249        long,
1250        value_name = "DIR",
1251        help = "Working directory for generated package state"
1252    )]
1253    pub work_dir: Utf8PathBuf,
1254    #[arg(long, help = "Publish Chocolatey")]
1255    pub chocolatey: bool,
1256    #[arg(long, help = "Publish Scoop")]
1257    pub scoop: bool,
1258    #[arg(long, help = "Submit Winget")]
1259    pub winget: bool,
1260    #[arg(long, help = "Run all configured Windows channels")]
1261    pub all: bool,
1262    #[arg(long, help = "Print planned work without writing or publishing")]
1263    pub dry_run: bool,
1264    #[arg(long, help = "Bypass Chocolatey duplicate-version protection")]
1265    pub force_resubmit: bool,
1266    #[arg(
1267        long = "choco-push-source",
1268        value_name = "URL",
1269        help = "Chocolatey push source override"
1270    )]
1271    pub choco_push_source: Option<String>,
1272    #[arg(
1273        long = "scoop-bucket-url",
1274        value_name = "URL",
1275        help = "Scoop bucket URL override"
1276    )]
1277    pub scoop_bucket_url: Option<String>,
1278    #[arg(
1279        long = "scoop-bucket-token-env",
1280        value_name = "ENV",
1281        help = "Scoop bucket token environment variable override"
1282    )]
1283    pub scoop_bucket_token_env: Option<String>,
1284}
1285
1286#[derive(Debug, Args)]
1287pub struct InitChocolateyCommand {
1288    #[arg(
1289        long,
1290        value_name = "DIR",
1291        help = "Path to the Chocolatey package directory to bootstrap"
1292    )]
1293    pub target: Utf8PathBuf,
1294    #[arg(long, help = "Verify the package files match the rendered template")]
1295    pub check: bool,
1296    #[arg(long, help = "Show a unified diff when --check finds drift")]
1297    pub diff: bool,
1298    #[arg(long, help = "Print the rendered package files without writing files")]
1299    pub print: bool,
1300    #[command(flatten)]
1301    pub chocolatey: ChocolateyOverridesArgs,
1302}
1303
1304#[derive(Debug, Args)]
1305pub struct ChocolateyCommand {
1306    #[command(subcommand)]
1307    pub action: ChocolateyAction,
1308}
1309
1310#[derive(Debug, Subcommand)]
1311pub enum ChocolateyAction {
1312    #[command(about = "Render package files to stdout or a directory (no sha256)")]
1313    Render(ChocolateyRenderArgs),
1314    #[command(about = "Render with real sha256 sums and optionally push")]
1315    Bump(ChocolateyBumpArgs),
1316}
1317
1318#[derive(Debug, Args)]
1319pub struct ChocolateyRenderArgs {
1320    #[arg(
1321        long,
1322        value_name = "VERSION",
1323        help = "Version to render (no leading 'v'). Defaults to cargo package.version"
1324    )]
1325    pub version: Option<String>,
1326    #[arg(long, value_name = "DIR", help = "Write package files under DIR")]
1327    pub output_dir: Option<Utf8PathBuf>,
1328    #[command(flatten)]
1329    pub chocolatey: ChocolateyOverridesArgs,
1330}
1331
1332#[derive(Debug, Args)]
1333pub struct ChocolateyBumpArgs {
1334    #[arg(
1335        long,
1336        value_name = "VERSION",
1337        help = "Release version (no leading 'v')"
1338    )]
1339    pub version: String,
1340    #[arg(
1341        long,
1342        value_name = "DIR",
1343        help = "Chocolatey package directory to update"
1344    )]
1345    pub package_dir: Utf8PathBuf,
1346    #[arg(
1347        long,
1348        value_name = "ARCH=PATH",
1349        help = "Per-architecture archive path for sha256 computation (x64 or x86)"
1350    )]
1351    pub archive: Vec<String>,
1352    #[arg(long, help = "After writing, run choco pack and choco push")]
1353    pub push: bool,
1354    #[arg(
1355        long,
1356        value_name = "URL",
1357        help = "Chocolatey push source URL (defaults to config or community feed)"
1358    )]
1359    pub push_source: Option<String>,
1360    #[arg(
1361        long,
1362        value_name = "ENV",
1363        help = "Environment variable containing the Chocolatey API key"
1364    )]
1365    pub api_key_env: Option<String>,
1366    #[arg(
1367        long,
1368        help = "Bypass Chocolatey duplicate-version protection before pushing"
1369    )]
1370    pub force_resubmit: bool,
1371    #[arg(long, help = "Print planned work without writing or pushing")]
1372    pub dry_run: bool,
1373    #[command(flatten)]
1374    pub chocolatey: ChocolateyOverridesArgs,
1375}
1376
1377#[derive(Debug, Args)]
1378pub struct AurOverridesArgs {
1379    #[arg(long = "aur-name", value_name = "NAME", help = "AUR pkgbase")]
1380    pub name: Option<String>,
1381    #[arg(
1382        long = "aur-description",
1383        value_name = "TEXT",
1384        help = "Package description (pkgdesc)"
1385    )]
1386    pub description: Option<String>,
1387    #[arg(long = "aur-url", value_name = "URL", help = "Project url")]
1388    pub url: Option<String>,
1389    #[arg(
1390        long = "aur-license",
1391        value_name = "SPDX",
1392        help = "SPDX license identifier"
1393    )]
1394    pub license: Option<String>,
1395    #[arg(
1396        long = "aur-download-repo",
1397        value_name = "OWNER/REPO",
1398        help = "Codeberg/GitHub owner/repo for release downloads"
1399    )]
1400    pub download_repo: Option<String>,
1401}
1402
1403impl AurOverridesArgs {
1404    pub fn as_overrides(&self) -> crate::config::AurOverrides<'_> {
1405        crate::config::AurOverrides {
1406            name: self.name.as_deref(),
1407            description: self.description.as_deref(),
1408            url: self.url.as_deref(),
1409            license: self.license.as_deref(),
1410            download_repo: self.download_repo.as_deref(),
1411        }
1412    }
1413}
1414
1415#[derive(Debug, Args)]
1416pub struct CoprOverridesArgs {
1417    #[arg(long = "copr-name", value_name = "NAME", help = "RPM package name")]
1418    pub name: Option<String>,
1419    #[arg(long = "copr-summary", value_name = "TEXT", help = "RPM Summary")]
1420    pub summary: Option<String>,
1421    #[arg(
1422        long = "copr-description",
1423        value_name = "TEXT",
1424        help = "RPM %description prose"
1425    )]
1426    pub description: Option<String>,
1427    #[arg(
1428        long = "copr-license",
1429        value_name = "SPDX",
1430        help = "SPDX license identifier"
1431    )]
1432    pub license: Option<String>,
1433    #[arg(long = "copr-url", value_name = "URL", help = "Project URL")]
1434    pub url: Option<String>,
1435    #[arg(
1436        long = "copr-download-repo",
1437        value_name = "OWNER/REPO",
1438        help = "Codeberg/GitHub owner/repo for the source archive"
1439    )]
1440    pub download_repo: Option<String>,
1441    #[arg(
1442        long = "copr-project",
1443        value_name = "OWNER/PROJECT",
1444        help = "COPR project for stable releases"
1445    )]
1446    pub project: Option<String>,
1447}
1448
1449impl CoprOverridesArgs {
1450    pub fn as_overrides(&self) -> crate::config::CoprOverrides<'_> {
1451        crate::config::CoprOverrides {
1452            name: self.name.as_deref(),
1453            summary: self.summary.as_deref(),
1454            description: self.description.as_deref(),
1455            license: self.license.as_deref(),
1456            url: self.url.as_deref(),
1457            download_repo: self.download_repo.as_deref(),
1458            project: self.project.as_deref(),
1459        }
1460    }
1461}
1462
1463#[derive(Debug, Args)]
1464pub struct AptOverridesArgs {
1465    #[arg(
1466        long = "apt-repo-url",
1467        value_name = "URL",
1468        help = "Git remote of the apt repository"
1469    )]
1470    pub repo_url: Option<String>,
1471    #[arg(
1472        long = "apt-label",
1473        value_name = "TEXT",
1474        help = "reprepro Origin/Label"
1475    )]
1476    pub label: Option<String>,
1477}
1478
1479impl AptOverridesArgs {
1480    pub fn as_overrides(&self) -> crate::config::AptOverrides<'_> {
1481        crate::config::AptOverrides {
1482            repo_url: self.repo_url.as_deref(),
1483            label: self.label.as_deref(),
1484        }
1485    }
1486}
1487
1488#[derive(Debug, Args)]
1489pub struct InitAurCommand {
1490    #[arg(
1491        long = "package",
1492        value_name = "NAME",
1493        help = "Workspace package providing metadata fallbacks"
1494    )]
1495    pub package: Option<String>,
1496    #[arg(long, help = "Verify dist/aur/*/PKGBUILD match the rendered template")]
1497    pub check: bool,
1498    #[arg(long, help = "Show a unified diff when --check finds drift")]
1499    pub diff: bool,
1500    #[arg(long, help = "Print the rendered PKGBUILDs without writing files")]
1501    pub print: bool,
1502    #[command(flatten)]
1503    pub aur: AurOverridesArgs,
1504}
1505
1506#[derive(Debug, Args)]
1507pub struct InitCoprCommand {
1508    #[arg(
1509        long = "package",
1510        value_name = "NAME",
1511        help = "Workspace package providing metadata fallbacks"
1512    )]
1513    pub package: Option<String>,
1514    #[arg(
1515        long,
1516        help = "Verify the spec and .copr/Makefile match the rendered template"
1517    )]
1518    pub check: bool,
1519    #[arg(long, help = "Show a unified diff when --check finds drift")]
1520    pub diff: bool,
1521    #[arg(long, help = "Print the rendered files without writing them")]
1522    pub print: bool,
1523    #[command(flatten)]
1524    pub copr: CoprOverridesArgs,
1525}
1526
1527#[derive(Debug, Args)]
1528pub struct InitAptCommand {
1529    #[arg(
1530        long = "package",
1531        value_name = "NAME",
1532        help = "Workspace package providing metadata fallbacks"
1533    )]
1534    pub package: Option<String>,
1535    #[arg(
1536        long,
1537        help = "Verify dist/apt/conf/distributions matches the rendered template"
1538    )]
1539    pub check: bool,
1540    #[arg(long, help = "Show a unified diff when --check finds drift")]
1541    pub diff: bool,
1542    #[arg(long, help = "Print the rendered config without writing files")]
1543    pub print: bool,
1544    #[command(flatten)]
1545    pub apt: AptOverridesArgs,
1546}
1547
1548#[derive(Debug, Args)]
1549pub struct InitReleaseCommand {
1550    #[arg(
1551        long = "package",
1552        value_name = "NAME",
1553        help = "Workspace package providing metadata fallbacks"
1554    )]
1555    pub package: Option<String>,
1556    #[arg(
1557        long,
1558        help = "Verify .forgejo/workflows/release.yml matches generated output"
1559    )]
1560    pub check: bool,
1561    #[arg(long, help = "Show a unified diff when --check finds drift")]
1562    pub diff: bool,
1563    #[arg(long, help = "Print the rendered workflow without writing files")]
1564    pub print: bool,
1565}
1566
1567#[derive(Debug, Args)]
1568pub struct AurCommand {
1569    #[command(subcommand)]
1570    pub action: AurAction,
1571}
1572
1573#[derive(Debug, Subcommand)]
1574pub enum AurAction {
1575    #[command(about = "Render PKGBUILDs to stdout for the given version")]
1576    Render(AurRenderArgs),
1577}
1578
1579#[derive(Debug, Args)]
1580pub struct AurRenderArgs {
1581    #[arg(
1582        long,
1583        value_name = "VERSION",
1584        help = "Version to render (no leading 'v')"
1585    )]
1586    pub version: Option<String>,
1587    #[arg(
1588        long = "package",
1589        value_name = "NAME",
1590        help = "Metadata-fallback package"
1591    )]
1592    pub package: Option<String>,
1593    #[command(flatten)]
1594    pub aur: AurOverridesArgs,
1595}
1596
1597#[derive(Debug, Args)]
1598pub struct CoprCommand {
1599    #[command(subcommand)]
1600    pub action: CoprAction,
1601}
1602
1603#[derive(Debug, Subcommand)]
1604pub enum CoprAction {
1605    #[command(about = "Render the RPM spec and .copr/Makefile to stdout")]
1606    Render(CoprRenderArgs),
1607}
1608
1609#[derive(Debug, Args)]
1610pub struct CoprRenderArgs {
1611    #[arg(
1612        long,
1613        value_name = "VERSION",
1614        help = "Version to render (no leading 'v')"
1615    )]
1616    pub version: Option<String>,
1617    #[arg(
1618        long = "package",
1619        value_name = "NAME",
1620        help = "Metadata-fallback package"
1621    )]
1622    pub package: Option<String>,
1623    #[command(flatten)]
1624    pub copr: CoprOverridesArgs,
1625}
1626
1627#[derive(Debug, Args)]
1628pub struct AptCommand {
1629    #[command(subcommand)]
1630    pub action: AptAction,
1631}
1632
1633#[derive(Debug, Subcommand)]
1634pub enum AptAction {
1635    #[command(about = "Render the reprepro distributions config to stdout")]
1636    Render(AptRenderArgs),
1637}
1638
1639#[derive(Debug, Args)]
1640pub struct AptRenderArgs {
1641    #[arg(
1642        long = "package",
1643        value_name = "NAME",
1644        help = "Metadata-fallback package"
1645    )]
1646    pub package: Option<String>,
1647    #[command(flatten)]
1648    pub apt: AptOverridesArgs,
1649}
1650
1651#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
1652pub enum Platform {
1653    #[value(help = "Forgejo Actions workflows under .forgejo/workflows")]
1654    Forgejo,
1655    #[value(help = "GitHub Actions workflows under .github/workflows")]
1656    Github,
1657}
1658
1659impl Platform {
1660    pub fn workflow_dir(self) -> &'static str {
1661        match self {
1662            Self::Forgejo => ".forgejo/workflows",
1663            Self::Github => ".github/workflows",
1664        }
1665    }
1666
1667    pub fn as_str(self) -> &'static str {
1668        match self {
1669            Self::Forgejo => "forgejo",
1670            Self::Github => "github",
1671        }
1672    }
1673}
1674
1675#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ValueEnum)]
1676#[serde(rename_all = "lowercase")]
1677pub enum RuntimeChoice {
1678    #[value(help = "Use simit's default runtime choice for the target platform")]
1679    Auto,
1680    #[value(help = "Use direct cargo commands and Rust toolchain setup")]
1681    Cargo,
1682    #[value(help = "Use nix develop and flake checks; requires flake.nix")]
1683    Nix,
1684}
1685
1686#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
1687#[serde(rename_all = "lowercase")]
1688pub enum Runtime {
1689    Cargo,
1690    Nix,
1691}
1692
1693#[derive(Debug, Args)]
1694pub struct InitFlakeCommand {
1695    #[arg(
1696        long,
1697        value_enum,
1698        help = "Generated ownership scope: hooks-only manages nix/pre-commit.nix; full also manages flake.nix and formatter wiring"
1699    )]
1700    pub scope: Option<FlakeScopeArg>,
1701    #[arg(long, help = "Verify flake and hook files match generated output")]
1702    pub check: bool,
1703    #[arg(
1704        long,
1705        help = "Print generated flake and hook files without writing them"
1706    )]
1707    pub print: bool,
1708    #[arg(long, help = "Show a unified diff when --check finds stale files")]
1709    pub diff: bool,
1710    #[arg(
1711        long,
1712        help = "Emit an rs-harbor multi-target flake that builds via rs-harbor.lib.mkCrossPackages"
1713    )]
1714    pub cross: bool,
1715    #[arg(
1716        long = "target",
1717        value_enum,
1718        value_name = "TARGET",
1719        requires = "cross",
1720        action = clap::ArgAction::Append,
1721        help = "Cross target to build; repeatable. Defaults to all targets when --cross is set without any --target"
1722    )]
1723    pub targets: Vec<FlakeTargetArg>,
1724}
1725
1726#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
1727pub enum FlakeScopeArg {
1728    HooksOnly,
1729    Full,
1730}
1731
1732#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
1733pub enum FlakeTargetArg {
1734    #[value(help = "Native craneLib.buildPackage for the host system")]
1735    Native,
1736    #[value(
1737        name = "aarch64-linux",
1738        help = "Cross-compile to aarch64-unknown-linux-gnu"
1739    )]
1740    Aarch64Linux,
1741    #[value(help = "Cross-compile to x86_64-pc-windows-gnu via mingw")]
1742    Windows,
1743    #[value(
1744        name = "darwin-x86_64",
1745        help = "Cross-compile to x86_64-apple-darwin via osxcross"
1746    )]
1747    DarwinX86_64,
1748    #[value(
1749        name = "darwin-aarch64",
1750        help = "Cross-compile to aarch64-apple-darwin via osxcross"
1751    )]
1752    DarwinAarch64,
1753}
1754
1755impl FlakeTargetArg {
1756    /// The mkCrossPackages target name (the key in the `targets = [ ... ]` list).
1757    pub fn key(self) -> &'static str {
1758        match self {
1759            Self::Native => "native",
1760            Self::Aarch64Linux => "aarch64-linux",
1761            Self::Windows => "windows",
1762            Self::DarwinX86_64 => "darwin-x86_64",
1763            Self::DarwinAarch64 => "darwin-aarch64",
1764        }
1765    }
1766
1767    /// Every target, in canonical order. Used as the default when `--cross` is
1768    /// passed without any explicit `--target`.
1769    pub fn all() -> [Self; 5] {
1770        [
1771            Self::Native,
1772            Self::Aarch64Linux,
1773            Self::Windows,
1774            Self::DarwinX86_64,
1775            Self::DarwinAarch64,
1776        ]
1777    }
1778}
1779
1780#[derive(Debug, Args)]
1781pub struct ChangelogCommand {
1782    #[arg(
1783        long,
1784        default_value = "CHANGELOG.md",
1785        value_name = "PATH",
1786        help = "Path to the changelog file"
1787    )]
1788    pub file: Utf8PathBuf,
1789    #[command(subcommand)]
1790    pub action: ChangelogAction,
1791}
1792
1793#[derive(Debug, Args)]
1794pub struct HooksCommand {
1795    #[command(subcommand, help = "Hooks action to run")]
1796    pub action: HooksAction,
1797}
1798
1799#[derive(Debug, Subcommand)]
1800pub enum HooksAction {
1801    #[command(about = "Install pre-commit hooks into this repository")]
1802    Install(HooksInstallCommand),
1803}
1804
1805#[derive(Debug, Args)]
1806pub struct HooksInstallCommand {
1807    #[arg(long, help = "Verify installed hook wrappers without changing files")]
1808    pub check: bool,
1809    #[arg(long, help = "Show a unified diff for stale hook wrappers")]
1810    pub diff: bool,
1811    #[arg(
1812        long,
1813        help = "Unset rogue repo-local core.hooksPath values that shadow a friendly system dispatcher (no-op otherwise)"
1814    )]
1815    pub fix: bool,
1816}
1817
1818#[derive(Debug, Args)]
1819pub struct ConfigCommand {
1820    #[command(subcommand, help = "User config action to run")]
1821    pub action: ConfigAction,
1822}
1823
1824#[derive(Debug, Args)]
1825pub struct ProjectsCommand {
1826    #[command(subcommand, help = "Project registry action to run")]
1827    pub action: ProjectsAction,
1828}
1829
1830#[derive(Debug, Subcommand)]
1831pub enum ProjectsAction {
1832    #[command(about = "List registered projects")]
1833    List(ProjectsListArgs),
1834    #[command(about = "Show one registered project's feature status")]
1835    Show(ProjectsShowArgs),
1836    #[command(about = "Re-run feature detection across registered projects")]
1837    Scan(ProjectsScanArgs),
1838    #[command(about = "Discover Rust workspaces under a filesystem subtree")]
1839    Discover(ProjectsDiscoverArgs),
1840    #[command(about = "Forget one registered project")]
1841    Forget(ProjectsForgetArgs),
1842    #[command(about = "Remove registered projects whose paths no longer exist")]
1843    Prune(ProjectsPruneArgs),
1844    #[command(about = "Clear all per-user project registry state")]
1845    ClearState(ProjectsClearStateArgs),
1846}
1847
1848#[derive(Debug, Args)]
1849pub struct ProjectsListArgs {
1850    #[arg(
1851        long = "feature",
1852        value_name = "NAME[=STATUS]",
1853        action = clap::ArgAction::Append,
1854        help = "Filter to projects where feature NAME has STATUS, or any non-absent status"
1855    )]
1856    pub features: Vec<String>,
1857    #[arg(
1858        long = "include-ephemeral",
1859        help = "Include ephemeral /tmp project paths in the listing"
1860    )]
1861    pub include_ephemeral: bool,
1862    #[arg(long, help = "Print a machine-readable JSON array")]
1863    pub json: bool,
1864    #[arg(
1865        long,
1866        conflicts_with = "no_issues",
1867        help = "Mark projects with drift, hook conflicts, or missing paths and print an issues footer"
1868    )]
1869    pub issues: bool,
1870    #[arg(
1871        long = "no-issues",
1872        help = "Suppress issue markers and footer in human output"
1873    )]
1874    pub no_issues: bool,
1875    #[arg(
1876        long,
1877        value_enum,
1878        value_name = "KEY",
1879        default_value = "last-seen",
1880        help = "Sort by name, path, last-seen, or first-seen"
1881    )]
1882    pub sort: ProjectsSort,
1883}
1884
1885#[derive(Debug, Args)]
1886pub struct ProjectsShowArgs {
1887    #[arg(
1888        value_name = "PATH",
1889        help = "Project path; defaults to the current workspace root"
1890    )]
1891    pub path: Option<Utf8PathBuf>,
1892    #[arg(long, help = "Print machine-readable JSON")]
1893    pub json: bool,
1894    #[arg(
1895        long,
1896        conflicts_with = "no_issues",
1897        help = "Print an attention header for drift, hook conflicts, or missing paths"
1898    )]
1899    pub issues: bool,
1900    #[arg(long = "no-issues", help = "Suppress the attention header")]
1901    pub no_issues: bool,
1902}
1903
1904#[derive(Debug, Args)]
1905pub struct ProjectsScanArgs {
1906    #[arg(long, help = "Also drop entries whose paths no longer exist")]
1907    pub prune: bool,
1908    #[arg(long, help = "Print intended changes without writing the registry")]
1909    pub dry_run: bool,
1910}
1911
1912#[derive(Debug, Args)]
1913pub struct ProjectsDiscoverArgs {
1914    #[arg(
1915        value_name = "ROOT",
1916        help = "Filesystem subtree to walk; defaults to the current directory"
1917    )]
1918    pub root: Option<Utf8PathBuf>,
1919    #[arg(
1920        long,
1921        help = "Walk and report discoverable projects without writing the registry"
1922    )]
1923    pub dry_run: bool,
1924    #[arg(long, help = "Print the discovery report as machine-readable JSON")]
1925    pub json: bool,
1926    #[arg(
1927        long,
1928        value_name = "NAME",
1929        action = clap::ArgAction::Append,
1930        help = "Also skip directories with this basename, in addition to built-in skips; may be repeated"
1931    )]
1932    pub skip: Vec<String>,
1933    #[arg(
1934        long,
1935        value_name = "N",
1936        help = "Maximum recursion depth from ROOT; defaults to 8"
1937    )]
1938    pub max_depth: Option<usize>,
1939    #[arg(long, help = "Follow symlinked directories during discovery")]
1940    pub follow_symlinks: bool,
1941    #[arg(
1942        long,
1943        help = "Register Cargo workspaces even when no simit features are detected"
1944    )]
1945    pub include_empty: bool,
1946}
1947
1948#[derive(Debug, Args)]
1949pub struct ProjectsForgetArgs {
1950    #[arg(value_name = "PATH", help = "Project path to remove from the registry")]
1951    pub path: Utf8PathBuf,
1952}
1953
1954#[derive(Debug, Args)]
1955pub struct ProjectsPruneArgs {
1956    #[arg(long, help = "Print stale entries without writing the registry")]
1957    pub dry_run: bool,
1958}
1959
1960#[derive(Debug, Args)]
1961pub struct ProjectsClearStateArgs {
1962    #[arg(
1963        long,
1964        help = "Print the registry path that would be cleared without writing"
1965    )]
1966    pub dry_run: bool,
1967}
1968
1969#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
1970pub enum ProjectsSort {
1971    Name,
1972    Path,
1973    #[value(name = "last-seen")]
1974    LastSeen,
1975    #[value(name = "first-seen")]
1976    FirstSeen,
1977}
1978
1979#[derive(Debug, Subcommand)]
1980pub enum ConfigAction {
1981    #[command(about = "Print the resolved user config path")]
1982    Path,
1983    #[command(about = "Print the normalized user config")]
1984    Show,
1985    #[command(about = "Validate the user config")]
1986    Check,
1987    #[command(about = "Write a starter user config if one does not exist")]
1988    Init,
1989}
1990
1991#[derive(Debug, Subcommand)]
1992pub enum ChangelogAction {
1993    #[command(about = "Create a Keep a Changelog skeleton")]
1994    Init,
1995    #[command(about = "Add an entry under [Unreleased]")]
1996    Add {
1997        #[arg(value_enum, value_name = "KIND", help = "Entry kind to append")]
1998        kind: ChangelogEntryKind,
1999        #[arg(value_name = "TEXT", help = "Entry text to add")]
2000        text: String,
2001    },
2002    #[command(about = "Promote [Unreleased] into a dated release section")]
2003    Release {
2004        #[arg(value_name = "VERSION", help = "Release version to create")]
2005        version: Version,
2006        #[arg(
2007            long,
2008            value_name = "YYYY-MM-DD",
2009            help = "Override the release date instead of using today in UTC"
2010        )]
2011        date: Option<String>,
2012        #[arg(
2013            long = "repo-url",
2014            value_name = "URL",
2015            help = "Repository URL used for compare links"
2016        )]
2017        repo_url: Option<String>,
2018    },
2019    #[command(about = "Validate that the file matches simit's Keep a Changelog rules")]
2020    Check,
2021    #[command(about = "Print the body of one changelog section")]
2022    Show {
2023        #[arg(
2024            value_name = "VERSION",
2025            help = "Version to print; omit to show [Unreleased]"
2026        )]
2027        version: Option<String>,
2028    },
2029}
2030
2031#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
2032pub enum ChangelogEntryKind {
2033    Added,
2034    Changed,
2035    Deprecated,
2036    Removed,
2037    Fixed,
2038    Security,
2039}
2040
2041#[derive(Debug, Args)]
2042pub struct CompletionsCommand {
2043    #[arg(
2044        value_enum,
2045        value_name = "SHELL",
2046        help = "Shell to generate completions for"
2047    )]
2048    pub shell: clap_complete::Shell,
2049}
2050
2051#[derive(Debug, Args)]
2052pub struct ManCommand {}