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    /// Inspect releases visible from a provider without installing
325    #[command(long_about = "Probe a repository/source and show parsed releases.\n\n\
326        Useful for validating what upstream can see before installation.\n\n\
327        EXAMPLES:\n  \
328        upstream probe neovim/neovim\n  \
329        upstream probe https://ziglang.org/download/ -p scraper --limit 20\n  \
330        upstream probe owner/repo --channel nightly --verbose\n  \
331        upstream probe owner/repo --json")]
332    Probe {
333        /// Repository identifier or URL to probe
334        repo_slug: String,
335
336        /// Source provider (defaults to github, or scraper for URLs)
337        #[arg(short = 'p', long)]
338        provider: Option<Provider>,
339
340        /// Custom base URL for self-hosted providers
341        #[arg(long)]
342        base_url: Option<String>,
343
344        /// Channel view to display
345        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
346        channel: Channel,
347
348        /// Maximum number of releases to display
349        #[arg(long, default_value_t = 10)]
350        limit: u32,
351
352        /// Include scored candidate assets for each release
353        #[arg(long, default_value_t = false)]
354        verbose: bool,
355
356        /// Print probe results as JSON
357        #[arg(long, default_value_t = false)]
358        json: bool,
359    },
360
361    /// Search provider repositories by keyword(s)
362    #[command(long_about = "Search for repositories on a provider.\n\n\
363        Defaults to GitHub when provider is omitted.\n\n\
364        EXAMPLES:\n  \
365        upstream search\n  \
366        upstream search ripgrep\n  \
367        upstream search editor --language Rust --min-stars 100 --max-stars 50000\n  \
368        upstream search rip grep --limit 5\n  \
369        upstream search my tool -p github\n  \
370        upstream search cli --topic terminal\n  \
371        upstream search ripgrep --json")]
372    Search {
373        /// Optional query words (joined with spaces)
374        #[arg(num_args(0..), value_delimiter = ' ')]
375        query_words: Vec<String>,
376
377        /// Source provider to search (defaults to github)
378        #[arg(short = 'p', long)]
379        provider: Option<Provider>,
380
381        /// Custom base URL for self-hosted providers
382        #[arg(long, requires = "provider")]
383        base_url: Option<String>,
384
385        /// Maximum number of results to display
386        #[arg(long, default_value_t = 10)]
387        limit: u32,
388
389        /// Restrict results to repositories with this primary language
390        #[arg(long)]
391        language: Option<String>,
392
393        /// Restrict results to repositories tagged with this topic
394        #[arg(long)]
395        topic: Option<String>,
396
397        /// Restrict results to repositories with at least this many stars
398        #[arg(long, value_name = "N")]
399        min_stars: Option<u64>,
400
401        /// Restrict results to repositories with at most this many stars
402        #[arg(long, value_name = "N")]
403        max_stars: Option<u64>,
404
405        /// Restrict results to repositories pushed on or after YYYY-MM-DD
406        #[arg(long, value_name = "YYYY-MM-DD", value_parser = parse_search_date)]
407        pushed_after: Option<NaiveDate>,
408
409        /// Include forked repositories in provider search results
410        #[arg(long, default_value_t = false)]
411        include_forks: bool,
412
413        /// Include archived repositories in provider search results
414        #[arg(long, default_value_t = false)]
415        include_archived: bool,
416
417        /// Print search results as JSON
418        #[arg(long, default_value_t = false)]
419        json: bool,
420    },
421
422    /// Search repositories interactively and install a selected result
423    #[command(
424        long_about = "Search for repositories on a provider, choose a result interactively, \
425        and install the selected repository.\n\n\
426        Defaults to GitHub when provider is omitted. After selection, prompts for the package \
427        name with the selected repository name as the default; submitting an empty name uses \
428        that inferred default. Use --name to skip the prompt.\n\n\
429        EXAMPLES:\n  \
430        upstream find ripgrep\n  \
431        upstream find terminal emulator --limit 20\n  \
432        upstream find cli --language Rust --topic cli\n  \
433        upstream find ripgrep --name rg -k binary\n  \
434        upstream find app -p github --desktop --trust none"
435    )]
436    Find {
437        /// Query words (joined with spaces)
438        #[arg(required = true, num_args(1..), value_delimiter = ' ')]
439        query_words: Vec<String>,
440
441        /// Source provider to search (defaults to github)
442        #[arg(short = 'p', long)]
443        provider: Option<Provider>,
444
445        /// Custom base URL for self-hosted providers
446        #[arg(long, requires = "provider")]
447        base_url: Option<String>,
448
449        /// Maximum number of results to display
450        #[arg(long, default_value_t = 10)]
451        limit: u32,
452
453        /// Restrict results to repositories with this primary language
454        #[arg(long)]
455        language: Option<String>,
456
457        /// Restrict results to repositories tagged with this topic
458        #[arg(long)]
459        topic: Option<String>,
460
461        /// Restrict results to repositories with at least this many stars
462        #[arg(long, value_name = "N")]
463        min_stars: Option<u64>,
464
465        /// Restrict results to repositories with at most this many stars
466        #[arg(long, value_name = "N")]
467        max_stars: Option<u64>,
468
469        /// Restrict results to repositories pushed on or after YYYY-MM-DD
470        #[arg(long, value_name = "YYYY-MM-DD", value_parser = parse_search_date)]
471        pushed_after: Option<NaiveDate>,
472
473        /// Include forked repositories in provider search results
474        #[arg(long, default_value_t = false)]
475        include_forks: bool,
476
477        /// Include archived repositories in provider search results
478        #[arg(long, default_value_t = false)]
479        include_archived: bool,
480
481        /// Package name to register without prompting
482        #[arg(long)]
483        name: Option<String>,
484
485        /// File type to install
486        #[arg(short, long, value_enum, default_value_t = Filetype::Auto)]
487        kind: Filetype,
488
489        /// Update channel to track
490        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
491        channel: Channel,
492
493        /// Match pattern to use as a hint for which asset to prefer
494        #[arg(short = 'm', long, name = "match")]
495        match_pattern: Option<String>,
496
497        /// Exclude pattern to filter out unwanted assets (e.g., "rocm", "debug")
498        #[arg(short = 'e', long, name = "exclude")]
499        exclude_pattern: Option<String>,
500
501        /// Whether or not to create a .desktop entry for GUI applications
502        #[arg(short, long, default_value_t = false)]
503        desktop: bool,
504
505        /// Trust verification mode for downloaded assets
506        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
507        trust_mode: TrustMode,
508
509        /// Preview install resolution without downloading or writing files
510        #[arg(long, default_value_t = false)]
511        dry_run: bool,
512    },
513
514    /// Manage upstream configuration
515    #[command(long_about = "View and modify upstream's configuration.\n\n\
516        Configuration is stored in TOML format and includes settings like \
517        API tokens, default providers, and installation preferences.\n\n\
518        EXAMPLES:\n  \
519        upstream config set github.api_token=ghp_xxx\n  \
520        upstream config get trust\n  \
521        upstream config list\n  \
522        upstream config edit")]
523    Config {
524        #[command(subcommand)]
525        action: ConfigAction,
526    },
527
528    /// Manage package-specific behavior
529    #[command(long_about = "Control package behavior.\n\n\
530        Pin packages to prevent upgrades or rename installed package aliases.\n\n\
531        EXAMPLES:\n  \
532        upstream package pin nvim\n  \
533        upstream package unpin nvim\n  \
534        upstream package rename nvim neovim")]
535    Package {
536        #[command(subcommand)]
537        action: PackageAction,
538    },
539
540    /// Manage shell integration hooks and local upstream data
541    #[command(long_about = "Manage upstream shell integration hooks.\n\n\
542        Use these commands to add, verify, or remove shell PATH hooks. \
543        Purge removes shell hooks and deletes the local upstream data directory.\n\n\
544        EXAMPLES:\n  \
545        upstream hooks init\n  \
546        upstream hooks check\n  \
547        upstream hooks clean\n  \
548        upstream --yes hooks purge")]
549    Hooks {
550        #[command(subcommand)]
551        action: HooksAction,
552    },
553
554    /// Import trusted keys, package metadata manifests, or full snapshots
555    #[command(
556        long_about = "Import trusted keys, package metadata manifests, or full snapshots.\n\n\
557        Autodetects the input type by content/extension, prompts for confirmation by default, \
558        and then performs the selected import operation.\n\n\
559        EXAMPLES:\n  \
560        upstream import ./minisign.pub            # Import trusted minisign keys\n  \
561        upstream import ./cosign.pub              # Import trusted cosign PEM keys\n  \
562        upstream import ./packages.json           # Import package metadata manifest\n  \
563        upstream import ./backup.tar.gz           # Restore full snapshot\n  \
564        upstream --yes import ./input.bin --as keys"
565    )]
566    Import {
567        /// Path to a keys file, metadata manifest, or snapshot archive
568        path: std::path::PathBuf,
569
570        /// Continue importing remaining entries when metadata manifest processing fails
571        #[arg(long, default_value_t = false)]
572        skip_failed: bool,
573
574        /// Force the input type instead of autodetection
575        #[arg(long = "as", value_enum)]
576        import_as: Option<ImportAs>,
577    },
578
579    /// Export packages to a manifest or full snapshot
580    #[command(long_about = "Export installed packages for backup or transfer.\n\n\
581        By default, writes a lightweight manifest containing just enough info to \
582        reinstall each package. Use --full to instead create a tarball of the entire \
583        upstream directory (a full snapshot).\n\n\
584        EXAMPLES:\n  \
585        upstream export ./packages.json           # Export manifest\n  \
586        upstream export ./backup.tar.gz --full    # Full snapshot")]
587    Export {
588        /// Output path for the manifest or snapshot archive
589        path: std::path::PathBuf,
590        /// Export a full snapshot of the upstream directory instead of a manifest
591        #[arg(long, default_value_t = false)]
592        full: bool,
593    },
594
595    /// Migrate local upstream data after breaking changes
596    #[command(
597        long_about = "Migrate existing upstream data to the current application format.\n\n\
598        Use this after upgrading across breaking changes that affect local data, \
599        metadata, package paths, or integration files. The migration is designed to \
600        be run manually when release notes or diagnostics ask for it. The current \
601        migration moves legacy package directories into the packages layout and rewrites \
602        affected metadata paths.\n\n\
603        EXAMPLE:\n  \
604        upstream migrate"
605    )]
606    Migrate,
607
608    /// Run diagnostics to detect installation and integration issues
609    #[command(
610        long_about = "Inspect upstream installation health and package state.\n\n\
611        Checks package paths, symlinks, shell PATH integration, cached completions, \
612        desktop/icon files, and metadata. \
613        Reports a compact summary by default and includes actionable hints. \
614        Use --verbose to print each individual check result. Use --fix to repair \
615        supported issues such as PATH hooks, missing symlinks, executable bits, \
616        executable metadata, and cached completion drift.\n\n\
617        EXAMPLES:\n  \
618        upstream doctor\n  \
619        upstream doctor --verbose\n  \
620        upstream doctor --fix\n  \
621        upstream doctor nvim ripgrep\n  \
622        upstream doctor --json"
623    )]
624    Doctor {
625        /// Package names to check (all installed packages if omitted)
626        names: Vec<String>,
627
628        /// Print each check result line in addition to summary output
629        #[arg(long, default_value_t = false)]
630        verbose: bool,
631
632        /// Attempt automatic repairs for detected issues
633        #[arg(long, default_value_t = false)]
634        fix: bool,
635
636        /// Print diagnostic report as JSON
637        #[arg(long, default_value_t = false)]
638        json: bool,
639    },
640}
641
642fn parse_search_date(raw: &str) -> Result<NaiveDate, String> {
643    NaiveDate::parse_from_str(raw, "%Y-%m-%d")
644        .map_err(|_| format!("expected date in YYYY-MM-DD format, got '{raw}'"))
645}
646
647#[derive(Subcommand)]
648pub enum RollbackAction {
649    /// Restore rollback artifacts
650    #[command(long_about = "Restore stored rollback artifacts.\n\n\
651        Without package names, restores the latest reversible transaction recorded \
652        in upstream's transaction history.\n\n\
653        EXAMPLES:\n  \
654        upstream rollback restore\n  \
655        upstream rollback restore rg\n  \
656        upstream rollback restore rg fd\n  \
657        upstream rollback restore rg --dry-run")]
658    Restore {
659        /// Package names to restore (latest reversible transaction if omitted)
660        #[arg(num_args(0..))]
661        names: Vec<String>,
662
663        /// Preview rollback restore actions without modifying files or metadata
664        #[arg(long, default_value_t = false)]
665        dry_run: bool,
666    },
667
668    /// Prune stored rollback artifacts
669    #[command(long_about = "Delete stored rollback artifacts.\n\n\
670        Without package names, prunes all stored rollback artifacts.\n\n\
671        EXAMPLES:\n  \
672        upstream rollback prune\n  \
673        upstream rollback prune rg\n  \
674        upstream rollback prune rg fd --dry-run")]
675    Prune {
676        /// Package names to prune (all rollback artifacts if omitted)
677        names: Vec<String>,
678
679        /// Preview rollback prune actions without deleting artifacts or metadata
680        #[arg(long, default_value_t = false)]
681        dry_run: bool,
682    },
683
684    /// List stored rollback artifacts
685    #[command(long_about = "List packages with stored rollback artifacts.\n\n\
686        EXAMPLE:\n  \
687        upstream rollback list")]
688    List,
689}
690
691impl Commands {
692    pub fn requires_lock(&self) -> bool {
693        match self {
694            Commands::List { .. } => false,
695            Commands::Changelog { .. } => false,
696            Commands::Doctor { fix, .. } => *fix,
697            Commands::Search { .. } => false,
698            Commands::Find { .. } => true,
699            Commands::Rollback {
700                action: RollbackAction::List,
701            } => false,
702            Commands::Hooks { action } => !matches!(action, HooksAction::Check),
703            Commands::Package { .. } => true,
704            Commands::Config { action } => {
705                !matches!(action, ConfigAction::Get { .. } | ConfigAction::List)
706            }
707            Commands::Install { .. }
708            | Commands::Build { .. }
709            | Commands::Remove { .. }
710            | Commands::Rollback { .. }
711            | Commands::Reinstall { .. }
712            | Commands::Upgrade { .. }
713            | Commands::Probe { .. }
714            | Commands::Import { .. }
715            | Commands::Export { .. }
716            | Commands::Migrate => true,
717        }
718    }
719}
720
721#[derive(Subcommand)]
722pub enum HooksAction {
723    /// Add upstream shell integration hooks
724    #[command(
725        long_about = "Add upstream shell integration hooks and create required local directories.\n\n\
726        EXAMPLE:\n  \
727        upstream hooks init"
728    )]
729    Init,
730
731    /// Check upstream shell integration hooks
732    #[command(
733        long_about = "Check upstream shell integration hooks and required local directories.\n\n\
734        EXAMPLE:\n  \
735        upstream hooks check"
736    )]
737    Check,
738
739    /// Remove upstream shell integration hooks
740    #[command(
741        long_about = "Remove upstream shell integration hooks without deleting installed package data.\n\n\
742        EXAMPLE:\n  \
743        upstream hooks clean"
744    )]
745    Clean,
746
747    /// Remove hooks and delete the local upstream data directory
748    #[command(
749        long_about = "Remove upstream shell integration hooks and delete the local upstream data directory.\n\n\
750        This deletes installed package files and metadata under ~/.upstream. \
751        Pass global --yes to skip the confirmation prompt.\n\n\
752        EXAMPLE:\n  \
753        upstream --yes hooks purge"
754    )]
755    Purge,
756}
757
758#[derive(Subcommand)]
759pub enum ConfigAction {
760    /// Set configuration values
761    #[command(long_about = "Set one or more configuration values.\n\n\
762        Use dot notation for nested keys. Multiple key=value pairs can be set at once.\n\n\
763        EXAMPLES:\n  \
764        upstream config set github.api_token=ghp_xxx\n  \
765        upstream config set gitlab.api_token=glpat_xxx")]
766    Set {
767        /// Configuration assignments (format: key.path=value)
768        #[arg(required = true)]
769        keys: Vec<String>,
770    },
771
772    /// Get configuration values
773    #[command(long_about = "Retrieve one or more configuration values.\n\n\
774        Use dot notation to access nested keys.\n\n\
775        EXAMPLES:\n  \
776        upstream config get trust\n  \
777        upstream config get github.api_token gitlab.api_token")]
778    Get {
779        /// Configuration keys to retrieve (format: key.path)
780        #[arg(required = true)]
781        keys: Vec<String>,
782    },
783
784    /// List all configuration keys
785    List,
786
787    /// Open configuration file in your default editor
788    Edit,
789
790    /// Reset configuration to defaults
791    Reset,
792}
793
794#[derive(Subcommand)]
795pub enum PackageAction {
796    /// Pin a package to its current version
797    #[command(long_about = "Prevent a package from being upgraded.\n\n\
798        Pinned packages are skipped during 'upstream upgrade' operations.\n\n\
799        EXAMPLE:\n  \
800        upstream package pin nvim")]
801    Pin {
802        /// Name of package to pin
803        name: String,
804
805        /// Optional reason for pinning this package
806        #[arg(long)]
807        reason: Option<String>,
808    },
809
810    /// Unpin a package to allow updates
811    #[command(long_about = "Remove version pin from a package.\n\n\
812        Unpinned packages will be included in future upgrade operations.\n\n\
813        EXAMPLE:\n  \
814        upstream package unpin nvim")]
815    Unpin {
816        /// Name of package to unpin
817        name: String,
818    },
819
820    /// Rename package alias without reinstalling
821    #[command(long_about = "Rename the local alias of an installed package.\n\n\
822        This changes how upstream tracks the package and updates integration aliases \
823        (symlink/desktop entry) when possible.\n\n\
824        EXAMPLE:\n  \
825        upstream package rename nvim neovim")]
826    Rename {
827        /// Existing package alias
828        old_name: String,
829
830        /// New package alias
831        new_name: String,
832    },
833}