Skip to main content

upstream_rs/application/cli/
arguments.rs

1use crate::models::common::enums::{Channel, Filetype, Provider, TrustMode};
2use chrono::NaiveDate;
3use clap::{Parser, Subcommand};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
6pub enum BuildProfile {
7    Rust,
8    Dotnet,
9    Go,
10    Zig,
11    Cmake,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
15pub enum ImportAs {
16    Keys,
17    Manifest,
18    Snapshot,
19}
20
21#[derive(Parser)]
22#[command(name = "upstream")]
23#[command(about = "A package manager for everything else.")]
24#[command(
25    long_about = "Upstream is a lightweight package manager that installs and manages \
26    applications from most software sources that do not have their own package manager.\n\n\
27    Install binaries, AppImages, and other artifacts with automatic updates, \
28    version pinning, rollback support, and minimal configuration.\n\n\
29    EXAMPLES:\n  \
30    upstream install neovim/neovim nvim --desktop\n  \
31    upstream install BurntSushi/ripgrep        # name inferred as ripgrep\n  \
32    upstream upgrade                # Upgrade all packages\n  \
33    upstream list                   # Show installed packages\n  \
34    upstream config set github.api_token=ghp_xxx"
35)]
36#[command(
37    version,
38    long_version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")")
39)]
40pub struct Cli {
41    /// Accept confirmation prompts
42    #[arg(short = 'y', long, global = true, default_value_t = false)]
43    pub yes: bool,
44
45    #[command(subcommand)]
46    pub command: Commands,
47}
48
49#[derive(Subcommand)]
50pub enum Commands {
51    /// Install a package from an upstream release source
52    #[command(long_about = "Install a new package from a download source.\n\n\
53        Downloads the specified file type from the latest release (or specified channel) \
54        and registers it under the given name for future updates. If the name is omitted \
55        for a git repository, upstream falls back to the repository name. \
56        Direct HTTP sources may still require an explicit name.\n\n\
57        EXAMPLES:\n  \
58        upstream install BurntSushi/ripgrep rg -k binary\n  \
59        upstream install bootandy/dust       # name inferred as dust\n  \
60        upstream install sharkdp/bat bat")]
61    Install {
62        /// Repository identifier or URL
63        repo_slug: String,
64
65        /// Name to register the application under (falls back to git repository name when omitted)
66        name: Option<String>,
67
68        /// Version tag to install (defaults to latest)
69        #[arg(short, long)]
70        tag: Option<String>,
71
72        /// File type to install
73        #[arg(short, long, value_enum, default_value_t = Filetype::Auto)]
74        kind: Filetype,
75
76        /// Source provider hosting the repository. Defaults to auto-detection.
77        #[arg(short = 'p', long)]
78        provider: Option<Provider>,
79
80        /// Custom base URL. Defaults to provider's root
81        #[arg(long, requires = "provider")]
82        base_url: Option<String>,
83
84        /// Update channel to track
85        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
86        channel: Channel,
87
88        /// Match pattern to use as a hint for which asset to prefer
89        #[arg(short = 'm', long, name = "match")]
90        match_pattern: Option<String>,
91
92        /// Exclude pattern to filter out unwanted assets (e.g., "rocm", "debug")
93        #[arg(short = 'e', long, name = "exclude")]
94        exclude_pattern: Option<String>,
95
96        /// Whether or not to create a .desktop entry for GUI applications
97        #[arg(short, long, default_value_t = false)]
98        desktop: bool,
99
100        /// Trust verification mode for downloaded assets
101        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
102        trust_mode: TrustMode,
103
104        /// Preview install resolution without downloading or writing files
105        #[arg(long, default_value_t = false)]
106        dry_run: bool,
107    },
108
109    /// Build and install from source for release tags without artifacts
110    #[command(long_about = "Build and install a package from source.\n\n\
111        Use this command when release tags exist but prebuilt artifacts are missing \
112        or unsuitable for your system.\n\n\
113        Mirrors install-style source resolution, with optional automatic profile detection. \
114        Git workspaces are cached under upstream's cache directory so upgrades can reuse \
115        prior build outputs when the project build system supports incremental rebuilds. \
116        If the name is omitted for a git repository, upstream falls back to the repository name.\n\n\
117        EXAMPLES:\n  \
118        upstream build BurntSushi/ripgrep rg\n  \
119        upstream build BurntSushi/ripgrep       # name inferred as ripgrep\n  \
120        upstream build BurntSushi/ripgrep rg --branch main\n  \
121        upstream build BurntSushi/ripgrep rg --build-profile rust\n  \
122        upstream build owner/repo app --build-profile dotnet --tag v1.2.3")]
123    Build {
124        /// Repository identifier or URL
125        repo_slug: String,
126
127        /// Name to register the application under (falls back to git repository name when omitted)
128        name: Option<String>,
129
130        /// Version tag to build (defaults to latest)
131        #[arg(short, long, conflicts_with = "branch")]
132        tag: Option<String>,
133
134        /// Branch name to build from (uses latest commit from that branch)
135        #[arg(long, conflicts_with = "tag")]
136        branch: Option<String>,
137
138        /// Source provider hosting the repository. Defaults to auto-detection.
139        #[arg(short = 'p', long)]
140        provider: Option<Provider>,
141
142        /// Custom base URL. Defaults to provider's root
143        #[arg(long, requires = "provider")]
144        base_url: Option<String>,
145
146        /// Update channel to track
147        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
148        channel: Channel,
149
150        /// Whether or not to create a .desktop entry for GUI applications
151        #[arg(short, long, default_value_t = false)]
152        desktop: bool,
153
154        /// Build profile used to compile/install from source (auto-detected when omitted)
155        #[arg(long, value_enum)]
156        build_profile: Option<BuildProfile>,
157
158        /// Preview build resolution without compiling or writing files
159        #[arg(long, default_value_t = false)]
160        dry_run: bool,
161    },
162
163    /// Remove one or more installed packages
164    #[command(
165        long_about = "Uninstall packages and optionally remove cached data.\n\n\
166        By default, removes the package binary/files but preserves cached release data. \
167        Use --purge to remove everything. Use --force to ignore uninstall errors \
168        (for example, missing files) and still remove package metadata.\n\n\
169        EXAMPLES:\n  \
170        upstream remove nvim\n  \
171        upstream remove rg fd bat --purge\n  \
172        upstream remove rg --force\n  \
173        upstream remove rg --dry-run"
174    )]
175    Remove {
176        /// Names of packages to remove
177        names: Vec<String>,
178
179        /// Remove all associated cached data
180        #[arg(long, default_value_t = false)]
181        purge: bool,
182
183        /// Ignore uninstall errors and remove metadata anyway
184        #[arg(long, default_value_t = false)]
185        force: bool,
186
187        /// Preview removal actions without deleting files or metadata
188        #[arg(long, default_value_t = false)]
189        dry_run: bool,
190    },
191
192    /// Manage stored rollback artifacts
193    #[command(long_about = "Manage package rollback points.\n\n\
194        Use restore without package names to restore the latest reversible transaction \
195        recorded in upstream's transaction history. Use explicit package names to restore \
196        selected packages, list rollback artifacts, or prune stored rollback data.\n\n\
197        EXAMPLES:\n  \
198        upstream rollback restore\n  \
199        upstream rollback restore rg\n  \
200        upstream rollback restore rg fd --dry-run\n  \
201        upstream rollback list\n  \
202        upstream rollback prune\n  \
203        upstream rollback prune rg")]
204    Rollback {
205        #[command(subcommand)]
206        action: RollbackAction,
207    },
208
209    /// Reinstall one or more packages (remove then install)
210    #[command(
211        long_about = "Reinstall packages by uninstalling and then installing them again.\n\n\
212        Reinstall uses each package's stored source metadata. Release installs attempt \
213        the currently recorded version tag; build installs rebuild from source.\n\n\
214        EXAMPLES:\n  \
215        upstream reinstall nvim\n  \
216        upstream reinstall rg fd\n  \
217        upstream reinstall rg --force\n  \
218        upstream reinstall rg --trust none"
219    )]
220    Reinstall {
221        /// Names of packages to reinstall
222        names: Vec<String>,
223
224        /// Trust verification mode for release-asset reinstalls
225        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
226        trust_mode: TrustMode,
227
228        /// Ignore uninstall errors and remove metadata anyway before reinstalling
229        #[arg(long, default_value_t = false)]
230        force: bool,
231
232        /// Preview reinstall resolution without removing, building, or writing files
233        #[arg(long, default_value_t = false)]
234        dry_run: bool,
235    },
236
237    /// Upgrade installed packages to their latest versions
238    #[command(long_about = "Check for and install updates to packages.\n\n\
239        Without arguments, upgrades all packages. Specify package names to upgrade \
240        only those packages. Use --check to preview available updates. At the \
241        confirmation prompt, enter c to view release notes before deciding.\n\n\
242        EXAMPLES:\n  \
243        upstream upgrade              # Upgrade all\n  \
244        upstream upgrade nvim rg      # Upgrade specific packages\n  \
245        upstream upgrade --check      # Check for updates\n  \
246        upstream upgrade --check --json # Check for updates as JSON\n  \
247        upstream upgrade --check --machine-readable # Script-friendly output\n  \
248        upstream upgrade nvim --force # Force reinstall\n  \
249        upstream upgrade --trust none")]
250    Upgrade {
251        /// Packages to upgrade (upgrades all if omitted)
252        names: Option<Vec<String>>,
253
254        /// Force upgrade even if already up to date
255        #[arg(long, default_value_t = false)]
256        force: bool,
257
258        /// Check for available upgrades without applying them
259        #[arg(long, default_value_t = false)]
260        check: bool,
261
262        /// Use script-friendly check output: one line per update, "name oldver newver"
263        #[arg(long, default_value_t = false, requires = "check")]
264        machine_readable: bool,
265
266        /// Print check results as JSON
267        #[arg(
268            long,
269            default_value_t = false,
270            requires = "check",
271            conflicts_with = "machine_readable"
272        )]
273        json: bool,
274
275        /// Trust verification mode for downloaded assets
276        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
277        trust_mode: TrustMode,
278
279        /// Preview upgrade resolution without downloading or writing files
280        #[arg(long, default_value_t = false)]
281        dry_run: bool,
282    },
283
284    /// List installed packages and their metadata
285    #[command(long_about = "Display information about installed packages.\n\n\
286        Without arguments, shows a summary of all installed packages. \
287        Provide a package name to see detailed information.\n\n\
288        EXAMPLES:\n  \
289        upstream list       # List all packages\n  \
290        upstream list nvim  # Show details for nvim")]
291    List {
292        /// Package name for detailed information
293        name: Option<String>,
294
295        /// Print raw package metadata as JSON
296        #[arg(long, default_value_t = false)]
297        json: bool,
298    },
299
300    /// Show upstream release notes for an installed package
301    #[command(long_about = "Show release notes for an installed package.\n\n\
302        By default, prints release bodies newer than the installed version up to \
303        the latest release for the package's tracked channel. Use --from and --to \
304        to override the range endpoints by release tag, or use current/latest for \
305        the installed or tracked latest release.\n\n\
306        EXAMPLES:\n  \
307        upstream changelog nvim\n  \
308        upstream changelog nvim --from current --to latest\n  \
309        upstream changelog nvim --from v0.10.0\n  \
310        upstream changelog nvim --from v0.10.0 --to v0.11.0")]
311    Changelog {
312        /// Installed package name
313        name: String,
314
315        /// Override the starting release tag
316        #[arg(long = "from")]
317        from_tag: Option<String>,
318
319        /// Override the ending release tag
320        #[arg(long = "to")]
321        to_tag: Option<String>,
322    },
323
324    /// Probe a repository/source, choose an asset, and install it
325    #[command(
326        long_about = "Probe a repository/source, choose a release asset interactively, \
327        and install it. Use --dry-run to show parsed releases without installing.\n\n\
328        EXAMPLES:\n  \
329        upstream probe neovim/neovim\n  \
330        upstream probe https://ziglang.org/download/ -p scraper --limit 20\n  \
331        upstream probe owner/repo tool --desktop\n  \
332        upstream probe owner/repo --include-incompatible\n  \
333        upstream probe owner/repo --limit 20\n  \
334        upstream probe owner/repo --tag v1.2.3\n  \
335        upstream probe owner/repo --channel nightly --verbose\n  \
336        upstream probe owner/repo --dry-run\n  \
337        upstream probe owner/repo --json"
338    )]
339    Probe {
340        /// Repository identifier or URL to probe
341        repo_slug: String,
342
343        /// Name to register the application under (prompts with inferred default when omitted)
344        name: Option<String>,
345
346        /// Source provider (defaults to github, or scraper for URLs)
347        #[arg(short = 'p', long)]
348        provider: Option<Provider>,
349
350        /// Custom base URL for self-hosted providers
351        #[arg(long)]
352        base_url: Option<String>,
353
354        /// Channel view to display
355        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
356        channel: Channel,
357
358        /// Number of releases to inspect instead of only the latest release
359        #[arg(long)]
360        limit: Option<u32>,
361
362        /// Release tag to inspect (defaults to latest when --limit is omitted)
363        #[arg(long, value_name = "TAG")]
364        tag: Option<String>,
365
366        /// Include scored candidate assets for each release
367        #[arg(long, default_value_t = false)]
368        verbose: bool,
369
370        /// Include assets that do not match the current OS/architecture or resolved install file type
371        #[arg(long, default_value_t = false)]
372        include_incompatible: bool,
373
374        /// Print dry-run probe results as JSON
375        #[arg(long, default_value_t = false)]
376        json: bool,
377
378        /// Whether or not to create a .desktop entry for GUI applications
379        #[arg(short, long, default_value_t = false)]
380        desktop: bool,
381
382        /// Trust verification mode for downloaded assets
383        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
384        trust_mode: TrustMode,
385
386        /// Show parsed releases without selecting, downloading, or installing
387        #[arg(long, default_value_t = false)]
388        dry_run: bool,
389    },
390
391    /// Search provider repositories by keyword(s)
392    #[command(long_about = "Search for repositories on a provider.\n\n\
393        Defaults to GitHub when provider is omitted.\n\n\
394        EXAMPLES:\n  \
395        upstream search\n  \
396        upstream search ripgrep\n  \
397        upstream search editor --language Rust --min-stars 100 --max-stars 50000\n  \
398        upstream search rip grep --limit 5\n  \
399        upstream search my tool -p github\n  \
400        upstream search cli --topic terminal\n  \
401        upstream search ripgrep --json")]
402    Search {
403        /// Optional query words (joined with spaces)
404        #[arg(num_args(0..), value_delimiter = ' ')]
405        query_words: Vec<String>,
406
407        /// Source provider to search (defaults to github)
408        #[arg(short = 'p', long)]
409        provider: Option<Provider>,
410
411        /// Custom base URL for self-hosted providers
412        #[arg(long, requires = "provider")]
413        base_url: Option<String>,
414
415        /// Maximum number of results to display
416        #[arg(long, default_value_t = 10)]
417        limit: u32,
418
419        /// Restrict results to repositories with this primary language
420        #[arg(long)]
421        language: Option<String>,
422
423        /// Restrict results to repositories tagged with this topic
424        #[arg(long)]
425        topic: Option<String>,
426
427        /// Restrict results to repositories with at least this many stars
428        #[arg(long, value_name = "N")]
429        min_stars: Option<u64>,
430
431        /// Restrict results to repositories with at most this many stars
432        #[arg(long, value_name = "N")]
433        max_stars: Option<u64>,
434
435        /// Restrict results to repositories pushed on or after YYYY-MM-DD
436        #[arg(long, value_name = "YYYY-MM-DD", value_parser = parse_search_date)]
437        pushed_after: Option<NaiveDate>,
438
439        /// Include forked repositories in provider search results
440        #[arg(long, default_value_t = false)]
441        include_forks: bool,
442
443        /// Include archived repositories in provider search results
444        #[arg(long, default_value_t = false)]
445        include_archived: bool,
446
447        /// Print search results as JSON
448        #[arg(long, default_value_t = false)]
449        json: bool,
450    },
451
452    /// Search repositories interactively and install a selected result
453    #[command(
454        long_about = "Search for repositories on a provider, choose a result interactively, \
455        and install the selected repository.\n\n\
456        Defaults to GitHub when provider is omitted. After selection, prompts for the package \
457        name with the selected repository name as the default; submitting an empty name uses \
458        that inferred default. Use --name to skip the prompt.\n\n\
459        EXAMPLES:\n  \
460        upstream find ripgrep\n  \
461        upstream find terminal emulator --limit 20\n  \
462        upstream find cli --language Rust --topic cli\n  \
463        upstream find ripgrep --name rg -k binary\n  \
464        upstream find app -p github --desktop --trust none"
465    )]
466    Find {
467        /// Query words (joined with spaces)
468        #[arg(required = true, num_args(1..), value_delimiter = ' ')]
469        query_words: Vec<String>,
470
471        /// Source provider to search (defaults to github)
472        #[arg(short = 'p', long)]
473        provider: Option<Provider>,
474
475        /// Custom base URL for self-hosted providers
476        #[arg(long, requires = "provider")]
477        base_url: Option<String>,
478
479        /// Maximum number of results to display
480        #[arg(long, default_value_t = 10)]
481        limit: u32,
482
483        /// Restrict results to repositories with this primary language
484        #[arg(long)]
485        language: Option<String>,
486
487        /// Restrict results to repositories tagged with this topic
488        #[arg(long)]
489        topic: Option<String>,
490
491        /// Restrict results to repositories with at least this many stars
492        #[arg(long, value_name = "N")]
493        min_stars: Option<u64>,
494
495        /// Restrict results to repositories with at most this many stars
496        #[arg(long, value_name = "N")]
497        max_stars: Option<u64>,
498
499        /// Restrict results to repositories pushed on or after YYYY-MM-DD
500        #[arg(long, value_name = "YYYY-MM-DD", value_parser = parse_search_date)]
501        pushed_after: Option<NaiveDate>,
502
503        /// Include forked repositories in provider search results
504        #[arg(long, default_value_t = false)]
505        include_forks: bool,
506
507        /// Include archived repositories in provider search results
508        #[arg(long, default_value_t = false)]
509        include_archived: bool,
510
511        /// Package name to register without prompting
512        #[arg(long)]
513        name: Option<String>,
514
515        /// File type to install
516        #[arg(short, long, value_enum, default_value_t = Filetype::Auto)]
517        kind: Filetype,
518
519        /// Update channel to track
520        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
521        channel: Channel,
522
523        /// Match pattern to use as a hint for which asset to prefer
524        #[arg(short = 'm', long, name = "match")]
525        match_pattern: Option<String>,
526
527        /// Exclude pattern to filter out unwanted assets (e.g., "rocm", "debug")
528        #[arg(short = 'e', long, name = "exclude")]
529        exclude_pattern: Option<String>,
530
531        /// Whether or not to create a .desktop entry for GUI applications
532        #[arg(short, long, default_value_t = false)]
533        desktop: bool,
534
535        /// Trust verification mode for downloaded assets
536        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
537        trust_mode: TrustMode,
538
539        /// Preview install resolution without downloading or writing files
540        #[arg(long, default_value_t = false)]
541        dry_run: bool,
542    },
543
544    /// Manage upstream configuration
545    #[command(long_about = "View and modify upstream's configuration.\n\n\
546        Configuration is stored in TOML format and includes settings like \
547        API tokens, default providers, and installation preferences.\n\n\
548        EXAMPLES:\n  \
549        upstream config set github.api_token=ghp_xxx\n  \
550        upstream config get download.high_threads\n  \
551        upstream config list\n  \
552        upstream config edit")]
553    Config {
554        #[command(subcommand)]
555        action: ConfigAction,
556    },
557
558    /// Manage package-specific behavior
559    #[command(long_about = "Control package behavior.\n\n\
560        Pin packages to prevent upgrades or rename installed package aliases.\n\n\
561        EXAMPLES:\n  \
562        upstream package pin nvim\n  \
563        upstream package unpin nvim\n  \
564        upstream package rename nvim neovim")]
565    Package {
566        #[command(subcommand)]
567        action: PackageAction,
568    },
569
570    /// Manage shell integration hooks and local upstream data
571    #[command(long_about = "Manage upstream shell integration hooks.\n\n\
572        Use these commands to add, verify, or remove shell PATH hooks. \
573        Purge removes shell hooks and deletes the local upstream data directory.\n\n\
574        EXAMPLES:\n  \
575        upstream hooks init\n  \
576        upstream hooks check\n  \
577        upstream hooks clean\n  \
578        upstream --yes hooks purge")]
579    Hooks {
580        #[command(subcommand)]
581        action: HooksAction,
582    },
583
584    /// Import trusted keys, package metadata manifests, or full snapshots
585    #[command(
586        long_about = "Import trusted keys, package metadata manifests, or full snapshots.\n\n\
587        Autodetects the input type by content/extension, prompts for confirmation by default, \
588        and then performs the selected import operation.\n\n\
589        EXAMPLES:\n  \
590        upstream import ./minisign.pub            # Import trusted minisign keys\n  \
591        upstream import ./cosign.pub              # Import trusted cosign PEM keys\n  \
592        upstream import ./packages.json           # Import package metadata manifest\n  \
593        upstream import ./backup.tar.gz           # Restore full snapshot\n  \
594        upstream --yes import ./input.bin --as keys"
595    )]
596    Import {
597        /// Path to a keys file, metadata manifest, or snapshot archive
598        path: std::path::PathBuf,
599
600        /// Continue importing remaining entries when metadata manifest processing fails
601        #[arg(long, default_value_t = false)]
602        skip_failed: bool,
603
604        /// Force the input type instead of autodetection
605        #[arg(long = "as", value_enum)]
606        import_as: Option<ImportAs>,
607    },
608
609    /// Export packages to a manifest or full snapshot
610    #[command(long_about = "Export installed packages for backup or transfer.\n\n\
611        By default, writes a lightweight manifest containing just enough info to \
612        reinstall each package. Use --full to instead create a tarball of the entire \
613        upstream directory (a full snapshot).\n\n\
614        EXAMPLES:\n  \
615        upstream export ./packages.json           # Export manifest\n  \
616        upstream export ./backup.tar.gz --full    # Full snapshot")]
617    Export {
618        /// Output path for the manifest or snapshot archive
619        path: std::path::PathBuf,
620        /// Export a full snapshot of the upstream directory instead of a manifest
621        #[arg(long, default_value_t = false)]
622        full: bool,
623    },
624
625    /// Migrate local upstream data after breaking changes
626    #[command(
627        long_about = "Migrate existing upstream data to the current application format.\n\n\
628        Use this after upgrading across breaking changes that affect local data, \
629        metadata, package paths, or integration files. The migration is designed to \
630        be run manually when release notes or diagnostics ask for it. The current \
631        migration moves legacy package directories into the packages layout and rewrites \
632        affected metadata paths.\n\n\
633        EXAMPLE:\n  \
634        upstream migrate"
635    )]
636    Migrate,
637
638    /// Run diagnostics to detect installation and integration issues
639    #[command(
640        long_about = "Inspect upstream installation health and package state.\n\n\
641        Checks package paths, symlinks, shell PATH integration, cached completions, \
642        desktop/icon files, and metadata. \
643        Reports a compact summary by default and includes actionable hints. \
644        Use --verbose to print each individual check result. Use --fix to repair \
645        supported issues such as PATH hooks, missing symlinks, executable bits, \
646        executable metadata, and cached completion drift.\n\n\
647        EXAMPLES:\n  \
648        upstream doctor\n  \
649        upstream doctor --verbose\n  \
650        upstream doctor --fix\n  \
651        upstream doctor nvim ripgrep\n  \
652        upstream doctor --json"
653    )]
654    Doctor {
655        /// Package names to check (all installed packages if omitted)
656        names: Vec<String>,
657
658        /// Print each check result line in addition to summary output
659        #[arg(long, default_value_t = false)]
660        verbose: bool,
661
662        /// Attempt automatic repairs for detected issues
663        #[arg(long, default_value_t = false)]
664        fix: bool,
665
666        /// Print diagnostic report as JSON
667        #[arg(long, default_value_t = false)]
668        json: bool,
669    },
670}
671
672fn parse_search_date(raw: &str) -> Result<NaiveDate, String> {
673    NaiveDate::parse_from_str(raw, "%Y-%m-%d")
674        .map_err(|_| format!("expected date in YYYY-MM-DD format, got '{raw}'"))
675}
676
677#[derive(Subcommand)]
678pub enum RollbackAction {
679    /// Restore rollback artifacts
680    #[command(long_about = "Restore stored rollback artifacts.\n\n\
681        Without package names, restores the latest reversible transaction recorded \
682        in upstream's transaction history.\n\n\
683        EXAMPLES:\n  \
684        upstream rollback restore\n  \
685        upstream rollback restore rg\n  \
686        upstream rollback restore rg fd\n  \
687        upstream rollback restore rg --dry-run")]
688    Restore {
689        /// Package names to restore (latest reversible transaction if omitted)
690        #[arg(num_args(0..))]
691        names: Vec<String>,
692
693        /// Preview rollback restore actions without modifying files or metadata
694        #[arg(long, default_value_t = false)]
695        dry_run: bool,
696    },
697
698    /// Prune stored rollback artifacts
699    #[command(long_about = "Delete stored rollback artifacts.\n\n\
700        Without package names, prunes all stored rollback artifacts.\n\n\
701        EXAMPLES:\n  \
702        upstream rollback prune\n  \
703        upstream rollback prune rg\n  \
704        upstream rollback prune rg fd --dry-run")]
705    Prune {
706        /// Package names to prune (all rollback artifacts if omitted)
707        names: Vec<String>,
708
709        /// Preview rollback prune actions without deleting artifacts or metadata
710        #[arg(long, default_value_t = false)]
711        dry_run: bool,
712    },
713
714    /// List stored rollback artifacts
715    #[command(long_about = "List packages with stored rollback artifacts.\n\n\
716        EXAMPLE:\n  \
717        upstream rollback list")]
718    List,
719}
720
721impl Commands {
722    pub fn requires_lock(&self) -> bool {
723        match self {
724            Commands::List { .. } => false,
725            Commands::Changelog { .. } => false,
726            Commands::Doctor { fix, .. } => *fix,
727            Commands::Search { .. } => false,
728            Commands::Find { .. } => true,
729            Commands::Rollback {
730                action: RollbackAction::List,
731            } => false,
732            Commands::Hooks { action } => !matches!(action, HooksAction::Check),
733            Commands::Package { .. } => true,
734            Commands::Config { action } => {
735                !matches!(action, ConfigAction::Get { .. } | ConfigAction::List)
736            }
737            Commands::Install { .. }
738            | Commands::Build { .. }
739            | Commands::Remove { .. }
740            | Commands::Rollback { .. }
741            | Commands::Reinstall { .. }
742            | Commands::Upgrade { .. }
743            | Commands::Probe { .. }
744            | Commands::Import { .. }
745            | Commands::Export { .. }
746            | Commands::Migrate => true,
747        }
748    }
749}
750
751#[derive(Subcommand)]
752pub enum HooksAction {
753    /// Add upstream shell integration hooks
754    #[command(
755        long_about = "Add upstream shell integration hooks and create required local directories.\n\n\
756        EXAMPLE:\n  \
757        upstream hooks init"
758    )]
759    Init,
760
761    /// Check upstream shell integration hooks
762    #[command(
763        long_about = "Check upstream shell integration hooks and required local directories.\n\n\
764        EXAMPLE:\n  \
765        upstream hooks check"
766    )]
767    Check,
768
769    /// Remove upstream shell integration hooks
770    #[command(
771        long_about = "Remove upstream shell integration hooks without deleting installed package data.\n\n\
772        EXAMPLE:\n  \
773        upstream hooks clean"
774    )]
775    Clean,
776
777    /// Remove hooks and delete the local upstream data directory
778    #[command(
779        long_about = "Remove upstream shell integration hooks and delete the local upstream data directory.\n\n\
780        This deletes installed package files and metadata under ~/.upstream. \
781        Pass global --yes to skip the confirmation prompt.\n\n\
782        EXAMPLE:\n  \
783        upstream --yes hooks purge"
784    )]
785    Purge,
786}
787
788#[derive(Subcommand)]
789pub enum ConfigAction {
790    /// Set configuration values
791    #[command(long_about = "Set one or more configuration values.\n\n\
792        Use dot notation for nested keys. Multiple key=value pairs can be set at once.\n\n\
793        EXAMPLES:\n  \
794        upstream config set github.api_token=ghp_xxx\n  \
795        upstream config set gitlab.api_token=glpat_xxx")]
796    Set {
797        /// Configuration assignments (format: key.path=value)
798        #[arg(required = true)]
799        keys: Vec<String>,
800    },
801
802    /// Get configuration values
803    #[command(long_about = "Retrieve one or more configuration values.\n\n\
804        Use dot notation to access nested keys.\n\n\
805        EXAMPLES:\n  \
806        upstream config get download.high_threads\n  \
807        upstream config get github.api_token gitlab.api_token")]
808    Get {
809        /// Configuration keys to retrieve (format: key.path)
810        #[arg(required = true)]
811        keys: Vec<String>,
812    },
813
814    /// List all configuration keys
815    List,
816
817    /// Open configuration file in your default editor
818    Edit,
819
820    /// Reset configuration to defaults
821    Reset,
822}
823
824#[derive(Subcommand)]
825pub enum PackageAction {
826    /// Pin a package to its current version
827    #[command(long_about = "Prevent a package from being upgraded.\n\n\
828        Pinned packages are skipped during 'upstream upgrade' operations.\n\n\
829        EXAMPLE:\n  \
830        upstream package pin nvim")]
831    Pin {
832        /// Name of package to pin
833        name: String,
834
835        /// Optional reason for pinning this package
836        #[arg(long)]
837        reason: Option<String>,
838    },
839
840    /// Unpin a package to allow updates
841    #[command(long_about = "Remove version pin from a package.\n\n\
842        Unpinned packages will be included in future upgrade operations.\n\n\
843        EXAMPLE:\n  \
844        upstream package unpin nvim")]
845    Unpin {
846        /// Name of package to unpin
847        name: String,
848    },
849
850    /// Rename package alias without reinstalling
851    #[command(long_about = "Rename the local alias of an installed package.\n\n\
852        This changes how upstream tracks the package and updates integration aliases \
853        (symlink/desktop entry) when possible.\n\n\
854        EXAMPLE:\n  \
855        upstream package rename nvim neovim")]
856    Rename {
857        /// Existing package alias
858        old_name: String,
859
860        /// New package alias
861        new_name: String,
862    },
863}