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 ripgrep\n  \
60        upstream install sharkdp/bat rg")]
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    /// Restore or prune stored rollback artifacts
193    #[command(long_about = "Manage package rollback points.\n\n\
194        Restore previously captured installs, or prune stored rollback artifacts. \
195        When no package names are provided, rollback restores the latest reversible \
196        transaction recorded in upstream's transaction history.\n\n\
197        EXAMPLES:\n  \
198        upstream rollback\n  \
199        upstream rollback rg\n  \
200        upstream rollback rg fd --dry-run\n  \
201        upstream rollback --prune\n  \
202        upstream rollback --prune rg")]
203    Rollback {
204        /// Package names to restore or prune
205        names: Vec<String>,
206
207        /// Prune rollback artifacts instead of restoring
208        #[arg(long, default_value_t = false)]
209        prune: bool,
210
211        /// Preview rollback/prune actions without modifying files or metadata
212        #[arg(long, default_value_t = false)]
213        dry_run: bool,
214    },
215
216    /// Reinstall one or more packages (remove then install)
217    #[command(
218        long_about = "Reinstall packages by uninstalling and then installing them again.\n\n\
219        Reinstall uses each package's stored source metadata. Release installs attempt \
220        the currently recorded version tag; build installs rebuild from source.\n\n\
221        EXAMPLES:\n  \
222        upstream reinstall nvim\n  \
223        upstream reinstall rg fd\n  \
224        upstream reinstall rg --force\n  \
225        upstream reinstall rg --trust none"
226    )]
227    Reinstall {
228        /// Names of packages to reinstall
229        names: Vec<String>,
230
231        /// Trust verification mode for release-asset reinstalls
232        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
233        trust_mode: TrustMode,
234
235        /// Ignore uninstall errors and remove metadata anyway before reinstalling
236        #[arg(long, default_value_t = false)]
237        force: bool,
238
239        /// Preview reinstall resolution without removing, building, or writing files
240        #[arg(long, default_value_t = false)]
241        dry_run: bool,
242    },
243
244    /// Upgrade installed packages to their latest versions
245    #[command(long_about = "Check for and install updates to packages.\n\n\
246        Without arguments, upgrades all packages. Specify package names to upgrade \
247        only those packages. Use --check to preview available updates.\n\n\
248        EXAMPLES:\n  \
249        upstream upgrade              # Upgrade all\n  \
250        upstream upgrade nvim rg      # Upgrade specific packages\n  \
251        upstream upgrade --check      # Check for updates\n  \
252        upstream upgrade --check --json # Check for updates as JSON\n  \
253        upstream upgrade --check --machine-readable # Script-friendly output\n  \
254        upstream upgrade nvim --force # Force reinstall\n  \
255        upstream upgrade --trust none")]
256    Upgrade {
257        /// Packages to upgrade (upgrades all if omitted)
258        names: Option<Vec<String>>,
259
260        /// Force upgrade even if already up to date
261        #[arg(long, default_value_t = false)]
262        force: bool,
263
264        /// Check for available upgrades without applying them
265        #[arg(long, default_value_t = false)]
266        check: bool,
267
268        /// Use script-friendly check output: one line per update, "name oldver newver"
269        #[arg(long, default_value_t = false, requires = "check")]
270        machine_readable: bool,
271
272        /// Print check results as JSON
273        #[arg(
274            long,
275            default_value_t = false,
276            requires = "check",
277            conflicts_with = "machine_readable"
278        )]
279        json: bool,
280
281        /// Trust verification mode for downloaded assets
282        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
283        trust_mode: TrustMode,
284
285        /// Preview upgrade resolution without downloading or writing files
286        #[arg(long, default_value_t = false)]
287        dry_run: bool,
288    },
289
290    /// List installed packages and their metadata
291    #[command(long_about = "Display information about installed packages.\n\n\
292        Without arguments, shows a summary of all installed packages. \
293        Provide a package name to see detailed information.\n\n\
294        EXAMPLES:\n  \
295        upstream list       # List all packages\n  \
296        upstream list nvim  # Show details for nvim")]
297    List {
298        /// Package name for detailed information
299        name: Option<String>,
300
301        /// Print raw package metadata as JSON
302        #[arg(long, default_value_t = false)]
303        json: bool,
304    },
305
306    /// Show upstream release notes for an installed package
307    #[command(long_about = "Show release notes for an installed package.\n\n\
308        By default, prints release bodies newer than the installed version up to \
309        the latest release for the package's tracked channel. Use --from and --to \
310        to override the range endpoints by release tag.\n\n\
311        EXAMPLES:\n  \
312        upstream changelog nvim\n  \
313        upstream changelog nvim --from v0.10.0\n  \
314        upstream changelog nvim --from v0.10.0 --to v0.11.0")]
315    Changelog {
316        /// Installed package name
317        name: String,
318
319        /// Override the starting release tag
320        #[arg(long = "from")]
321        from_tag: Option<String>,
322
323        /// Override the ending release tag
324        #[arg(long = "to")]
325        to_tag: Option<String>,
326    },
327
328    /// Inspect releases visible from a provider without installing
329    #[command(long_about = "Probe a repository/source and show parsed releases.\n\n\
330        Useful for validating what upstream can see before installation.\n\n\
331        EXAMPLES:\n  \
332        upstream probe neovim/neovim\n  \
333        upstream probe https://ziglang.org/download/ -p scraper --limit 20\n  \
334        upstream probe owner/repo --channel nightly --verbose\n  \
335        upstream probe owner/repo --json")]
336    Probe {
337        /// Repository identifier or URL to probe
338        repo_slug: String,
339
340        /// Source provider (defaults to github, or scraper for URLs)
341        #[arg(short = 'p', long)]
342        provider: Option<Provider>,
343
344        /// Custom base URL for self-hosted providers
345        #[arg(long)]
346        base_url: Option<String>,
347
348        /// Channel view to display
349        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
350        channel: Channel,
351
352        /// Maximum number of releases to display
353        #[arg(long, default_value_t = 10)]
354        limit: u32,
355
356        /// Include scored candidate assets for each release
357        #[arg(long, default_value_t = false)]
358        verbose: bool,
359
360        /// Print probe results as JSON
361        #[arg(long, default_value_t = false)]
362        json: bool,
363    },
364
365    /// Search provider repositories by keyword(s)
366    #[command(long_about = "Search for repositories on a provider.\n\n\
367        Defaults to GitHub when provider is omitted.\n\n\
368        EXAMPLES:\n  \
369        upstream search\n  \
370        upstream search ripgrep\n  \
371        upstream search editor --language Rust --min-stars 100\n  \
372        upstream search rip grep --limit 5\n  \
373        upstream search my tool -p github\n  \
374        upstream search widget -p gitlab --base-url https://gitlab.example.com\n  \
375        upstream search ripgrep --json")]
376    Search {
377        /// Optional query words (joined with spaces)
378        #[arg(num_args(0..), value_delimiter = ' ')]
379        query_words: Vec<String>,
380
381        /// Source provider to search (defaults to github)
382        #[arg(short = 'p', long)]
383        provider: Option<Provider>,
384
385        /// Custom base URL for self-hosted providers
386        #[arg(long, requires = "provider")]
387        base_url: Option<String>,
388
389        /// Maximum number of results to display
390        #[arg(long, default_value_t = 10)]
391        limit: u32,
392
393        /// Restrict results to repositories with this primary language
394        #[arg(long)]
395        language: Option<String>,
396
397        /// Restrict results to repositories tagged with this topic
398        #[arg(long)]
399        topic: Option<String>,
400
401        /// Restrict results to repositories with at least this many stars
402        #[arg(long, value_name = "N")]
403        min_stars: Option<u64>,
404
405        /// Restrict results to repositories with at most this many stars
406        #[arg(long, value_name = "N")]
407        max_stars: Option<u64>,
408
409        /// Restrict results to repositories pushed on or after YYYY-MM-DD
410        #[arg(long, value_name = "YYYY-MM-DD", value_parser = parse_search_date)]
411        pushed_after: Option<NaiveDate>,
412
413        /// Include forked repositories in provider search results
414        #[arg(long, default_value_t = false)]
415        include_forks: bool,
416
417        /// Include archived repositories in provider search results
418        #[arg(long, default_value_t = false)]
419        include_archived: bool,
420
421        /// Print search results as JSON
422        #[arg(long, default_value_t = false)]
423        json: bool,
424    },
425
426    /// Search repositories interactively and install a selected result
427    #[command(
428        long_about = "Search for repositories on a provider, choose a result interactively, \
429        and install the selected repository.\n\n\
430        Defaults to GitHub when provider is omitted. After selection, prompts for the package \
431        name with the selected repository name as the default; submitting an empty name uses \
432        that inferred default. Use --name to skip the prompt.\n\n\
433        EXAMPLES:\n  \
434        upstream find ripgrep\n  \
435        upstream find terminal emulator --limit 20\n  \
436        upstream find ripgrep --name rg -k binary\n  \
437        upstream find app -p github --desktop --trust none"
438    )]
439    Find {
440        /// Query words (joined with spaces)
441        #[arg(required = true, num_args(1..), value_delimiter = ' ')]
442        query_words: Vec<String>,
443
444        /// Source provider to search (defaults to github)
445        #[arg(short = 'p', long)]
446        provider: Option<Provider>,
447
448        /// Custom base URL for self-hosted providers
449        #[arg(long, requires = "provider")]
450        base_url: Option<String>,
451
452        /// Maximum number of results to display
453        #[arg(long, default_value_t = 10)]
454        limit: u32,
455
456        /// Package name to register without prompting
457        #[arg(long)]
458        name: Option<String>,
459
460        /// File type to install
461        #[arg(short, long, value_enum, default_value_t = Filetype::Auto)]
462        kind: Filetype,
463
464        /// Update channel to track
465        #[arg(short, long, value_enum, default_value_t = Channel::Stable)]
466        channel: Channel,
467
468        /// Match pattern to use as a hint for which asset to prefer
469        #[arg(short = 'm', long, name = "match")]
470        match_pattern: Option<String>,
471
472        /// Exclude pattern to filter out unwanted assets (e.g., "rocm", "debug")
473        #[arg(short = 'e', long, name = "exclude")]
474        exclude_pattern: Option<String>,
475
476        /// Whether or not to create a .desktop entry for GUI applications
477        #[arg(short, long, default_value_t = false)]
478        desktop: bool,
479
480        /// Trust verification mode for downloaded assets
481        #[arg(long = "trust", value_enum, default_value_t = TrustMode::BestEffort)]
482        trust_mode: TrustMode,
483
484        /// Preview install resolution without downloading or writing files
485        #[arg(long, default_value_t = false)]
486        dry_run: bool,
487    },
488
489    /// Manage upstream configuration
490    #[command(long_about = "View and modify upstream's configuration.\n\n\
491        Configuration is stored in TOML format and includes settings like \
492        API tokens, default providers, and installation preferences.\n\n\
493        EXAMPLES:\n  \
494        upstream config set github.api_token=ghp_xxx\n  \
495        upstream config get trust\n  \
496        upstream config list\n  \
497        upstream config edit")]
498    Config {
499        #[command(subcommand)]
500        action: ConfigAction,
501    },
502
503    /// Manage package-specific behavior
504    #[command(long_about = "Control package behavior.\n\n\
505        Pin packages to prevent upgrades or rename installed package aliases.\n\n\
506        EXAMPLES:\n  \
507        upstream package pin nvim\n  \
508        upstream package unpin nvim\n  \
509        upstream package rename nvim neovim")]
510    Package {
511        #[command(subcommand)]
512        action: PackageAction,
513    },
514
515    /// Manage shell integration hooks and local upstream data
516    #[command(long_about = "Manage upstream shell integration hooks.\n\n\
517        Use these commands to add, verify, or remove shell PATH hooks. \
518        Purge removes shell hooks and deletes the local upstream data directory.\n\n\
519        EXAMPLES:\n  \
520        upstream hooks init\n  \
521        upstream hooks check\n  \
522        upstream hooks clean\n  \
523        upstream --yes hooks purge")]
524    Hooks {
525        #[command(subcommand)]
526        action: HooksAction,
527    },
528
529    /// Import trusted keys, package metadata manifests, or full snapshots
530    #[command(
531        long_about = "Import trusted keys, package metadata manifests, or full snapshots.\n\n\
532        Autodetects the input type by content/extension, prompts for confirmation by default, \
533        and then performs the selected import operation.\n\n\
534        EXAMPLES:\n  \
535        upstream import ./minisign.pub            # Import trusted minisign keys\n  \
536        upstream import ./cosign.pub              # Import trusted cosign PEM keys\n  \
537        upstream import ./packages.json           # Import package metadata manifest\n  \
538        upstream import ./backup.tar.gz           # Restore full snapshot\n  \
539        upstream --yes import ./input.bin --as keys"
540    )]
541    Import {
542        /// Path to a keys file, metadata manifest, or snapshot archive
543        path: std::path::PathBuf,
544
545        /// Continue importing remaining entries when metadata manifest processing fails
546        #[arg(long, default_value_t = false)]
547        skip_failed: bool,
548
549        /// Force the input type instead of autodetection
550        #[arg(long = "as", value_enum)]
551        import_as: Option<ImportAs>,
552    },
553
554    /// Export packages to a manifest or full snapshot
555    #[command(long_about = "Export installed packages for backup or transfer.\n\n\
556        By default, writes a lightweight manifest containing just enough info to \
557        reinstall each package. Use --full to instead create a tarball of the entire \
558        upstream directory (a full snapshot).\n\n\
559        EXAMPLES:\n  \
560        upstream export ./packages.json           # Export manifest\n  \
561        upstream export ./backup.tar.gz --full    # Full snapshot")]
562    Export {
563        /// Output path for the manifest or snapshot archive
564        path: std::path::PathBuf,
565        /// Export a full snapshot of the upstream directory instead of a manifest
566        #[arg(long, default_value_t = false)]
567        full: bool,
568    },
569
570    /// Migrate local upstream data after breaking changes
571    #[command(
572        long_about = "Migrate existing upstream data to the current application format.\n\n\
573        Use this after upgrading across breaking changes that affect local data, \
574        metadata, package paths, or integration files. The migration is designed to \
575        be run manually when release notes or diagnostics ask for it. The current \
576        migration moves legacy package directories into the packages layout and rewrites \
577        affected metadata paths.\n\n\
578        EXAMPLE:\n  \
579        upstream migrate"
580    )]
581    Migrate,
582
583    /// Run diagnostics to detect installation and integration issues
584    #[command(
585        long_about = "Inspect upstream installation health and package state.\n\n\
586        Checks package paths, symlinks, shell PATH integration, cached completions, \
587        desktop/icon files, and metadata. \
588        Reports a compact summary by default and includes actionable hints. \
589        Use --verbose to print each individual check result. Use --fix to repair \
590        supported issues such as PATH hooks, missing symlinks, executable bits, \
591        executable metadata, and cached completion drift.\n\n\
592        EXAMPLES:\n  \
593        upstream doctor\n  \
594        upstream doctor --verbose\n  \
595        upstream doctor --fix\n  \
596        upstream doctor nvim ripgrep\n  \
597        upstream doctor --json"
598    )]
599    Doctor {
600        /// Package names to check (all installed packages if omitted)
601        names: Vec<String>,
602
603        /// Print each check result line in addition to summary output
604        #[arg(long, default_value_t = false)]
605        verbose: bool,
606
607        /// Attempt automatic repairs for detected issues
608        #[arg(long, default_value_t = false)]
609        fix: bool,
610
611        /// Print diagnostic report as JSON
612        #[arg(long, default_value_t = false)]
613        json: bool,
614    },
615}
616
617fn parse_search_date(raw: &str) -> Result<NaiveDate, String> {
618    NaiveDate::parse_from_str(raw, "%Y-%m-%d")
619        .map_err(|_| format!("expected date in YYYY-MM-DD format, got '{raw}'"))
620}
621
622impl Commands {
623    pub fn requires_lock(&self) -> bool {
624        match self {
625            Commands::List { .. } => false,
626            Commands::Changelog { .. } => false,
627            Commands::Doctor { fix, .. } => *fix,
628            Commands::Search { .. } => false,
629            Commands::Find { .. } => true,
630            Commands::Hooks { action } => !matches!(action, HooksAction::Check),
631            Commands::Package { .. } => true,
632            Commands::Config { action } => {
633                !matches!(action, ConfigAction::Get { .. } | ConfigAction::List)
634            }
635            Commands::Install { .. }
636            | Commands::Build { .. }
637            | Commands::Remove { .. }
638            | Commands::Rollback { .. }
639            | Commands::Reinstall { .. }
640            | Commands::Upgrade { .. }
641            | Commands::Probe { .. }
642            | Commands::Import { .. }
643            | Commands::Export { .. }
644            | Commands::Migrate => true,
645        }
646    }
647}
648
649#[derive(Subcommand)]
650pub enum HooksAction {
651    /// Add upstream shell integration hooks
652    #[command(
653        long_about = "Add upstream shell integration hooks and create required local directories.\n\n\
654        EXAMPLE:\n  \
655        upstream hooks init"
656    )]
657    Init,
658
659    /// Check upstream shell integration hooks
660    #[command(
661        long_about = "Check upstream shell integration hooks and required local directories.\n\n\
662        EXAMPLE:\n  \
663        upstream hooks check"
664    )]
665    Check,
666
667    /// Remove upstream shell integration hooks
668    #[command(
669        long_about = "Remove upstream shell integration hooks without deleting installed package data.\n\n\
670        EXAMPLE:\n  \
671        upstream hooks clean"
672    )]
673    Clean,
674
675    /// Remove hooks and delete the local upstream data directory
676    #[command(
677        long_about = "Remove upstream shell integration hooks and delete the local upstream data directory.\n\n\
678        This deletes installed package files and metadata under ~/.upstream. \
679        Pass global --yes to skip the confirmation prompt.\n\n\
680        EXAMPLE:\n  \
681        upstream --yes hooks purge"
682    )]
683    Purge,
684}
685
686#[derive(Subcommand)]
687pub enum ConfigAction {
688    /// Set configuration values
689    #[command(long_about = "Set one or more configuration values.\n\n\
690        Use dot notation for nested keys. Multiple key=value pairs can be set at once.\n\n\
691        EXAMPLES:\n  \
692        upstream config set github.api_token=ghp_xxx\n  \
693        upstream config set gitlab.api_token=glpat_xxx")]
694    Set {
695        /// Configuration assignments (format: key.path=value)
696        #[arg(required = true)]
697        keys: Vec<String>,
698    },
699
700    /// Get configuration values
701    #[command(long_about = "Retrieve one or more configuration values.\n\n\
702        Use dot notation to access nested keys.\n\n\
703        EXAMPLES:\n  \
704        upstream config get trust\n  \
705        upstream config get github.api_token gitlab.api_token")]
706    Get {
707        /// Configuration keys to retrieve (format: key.path)
708        #[arg(required = true)]
709        keys: Vec<String>,
710    },
711
712    /// List all configuration keys
713    List,
714
715    /// Open configuration file in your default editor
716    Edit,
717
718    /// Reset configuration to defaults
719    Reset,
720}
721
722#[derive(Subcommand)]
723pub enum PackageAction {
724    /// Pin a package to its current version
725    #[command(long_about = "Prevent a package from being upgraded.\n\n\
726        Pinned packages are skipped during 'upstream upgrade' operations.\n\n\
727        EXAMPLE:\n  \
728        upstream package pin nvim")]
729    Pin {
730        /// Name of package to pin
731        name: String,
732
733        /// Optional reason for pinning this package
734        #[arg(long)]
735        reason: Option<String>,
736    },
737
738    /// Unpin a package to allow updates
739    #[command(long_about = "Remove version pin from a package.\n\n\
740        Unpinned packages will be included in future upgrade operations.\n\n\
741        EXAMPLE:\n  \
742        upstream package unpin nvim")]
743    Unpin {
744        /// Name of package to unpin
745        name: String,
746    },
747
748    /// Rename package alias without reinstalling
749    #[command(long_about = "Rename the local alias of an installed package.\n\n\
750        This changes how upstream tracks the package and updates integration aliases \
751        (symlink/desktop entry) when possible.\n\n\
752        EXAMPLE:\n  \
753        upstream package rename nvim neovim")]
754    Rename {
755        /// Existing package alias
756        old_name: String,
757
758        /// New package alias
759        new_name: String,
760    },
761}