Skip to main content

zoi_cli/
cli.rs

1use std::io::{self};
2use std::path::PathBuf;
3
4use clap::builder::styling;
5use clap::{
6    ColorChoice, CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint
7};
8use clap_complete::{Shell, generate};
9use colored::Colorize;
10use zoi_common::Runnable;
11
12use crate::pkg::lock;
13use crate::{cmd, utils};
14
15// Development, Special, Public or Production
16/// The release branch of the current build.
17const BRANCH: &str = "Production";
18/// The release status of the current build.
19const STATUS: &str = "Release";
20/// The version number of the current build.
21const NUMBER: &str = "1.25.1";
22/// Help text for package source identifiers.
23const PKG_SOURCE_HELP: &str =
24    "Package identifier (e.g. @repo/name, #git@repo/name, path, or URL)";
25
26/// Zoi - The Advanced Package Manager & Environment Orchestrator.
27///
28/// Part of the Zillowe Development Suite (ZDS), Zoi is designed to streamline
29/// your development workflow by managing tools and project environments.
30#[derive(Parser)]
31#[command(name = "zoi", author, about, long_about = None, disable_version_flag = true,
32    trailing_var_arg = true,
33    color = ColorChoice::Auto,
34    arg_required_else_help = true,
35)]
36pub struct Cli {
37    /// The subcommand to execute.
38    #[command(subcommand)]
39    command: Option<Commands>,
40
41    /// Print detailed version information.
42    #[arg(
43        short = 'v',
44        long = "version",
45        help = "Print detailed version information"
46    )]
47    version_flag: bool,
48
49    /// Automatically answer yes to all prompts.
50    #[arg(
51        short = 'y',
52        long,
53        help = "Automatically answer yes to all prompts",
54        global = true
55    )]
56    yes: bool,
57
58    /// Operate on a different root directory.
59    #[arg(
60        long = "root",
61        help = "Operate on a different root directory",
62        global = true,
63        value_hint = ValueHint::DirPath
64    )]
65    pub root: Option<std::path::PathBuf>,
66
67    /// Do not attempt to connect to the network.
68    #[arg(
69        long = "offline",
70        help = "Do not attempt to connect to the network",
71        global = true
72    )]
73    pub offline: bool,
74
75    /// Additional directory to search for .zpa archives.
76    #[arg(
77        long = "pkg-dir",
78        help = "Additional directory to search for .zpa archives",
79        global = true,
80        value_hint = ValueHint::DirPath
81    )]
82    /// Additional directory to search for .zpa archives.
83    pub pkg_dirs: Vec<std::path::PathBuf>
84}
85
86/// The target scope for system-level operations.
87#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
88pub enum SetupScope {
89    /// The current user's scope.
90    User,
91    /// The system-wide scope.
92    System
93}
94
95/// The target scope for package installation.
96#[derive(clap::ValueEnum, Clone, Debug, Copy)]
97pub enum InstallScope {
98    /// The current user's scope.
99    User,
100    /// The system-wide scope.
101    System,
102    /// The current project's scope.
103    Project
104}
105
106/// The available subcommands for Zoi.
107#[derive(Subcommand)]
108enum Commands {
109    /// Generates shell completion scripts
110    #[command(hide = true)]
111    GenerateCompletions {
112        /// The shell to generate completions for
113        #[arg(value_enum)]
114        shell: Shell
115    },
116
117    /// Dynamic shell completions (internal use)
118    #[command(hide = true)]
119    Complete {
120        /// The shell to complete for
121        #[arg(value_enum)]
122        shell: Shell,
123        /// Current word index (1-based)
124        index: usize,
125        /// All words in the command line
126        words: Vec<String>
127    },
128
129    /// Generates man pages for zoi
130    #[command(hide = true)]
131    GenerateManual,
132
133    /// Prints concise version and build information
134    #[command(
135        alias = "v",
136        long_about = "Displays the version number, build status, branch, and \
137                      commit hash. This is the same output provided by the -v \
138                      and --version flags."
139    )]
140    Version,
141
142    /// Shows detailed application information and credits
143    #[command(long_about = "Displays the full application name, description, \
144                            author, license, and homepage information.")]
145    About,
146
147    /// Displays detected operating system and architecture information
148    #[command(long_about = "Detects and displays key system details, \
149                            including the OS, CPU architecture, Linux \
150                            distribution (if applicable), and available \
151                            package managers.")]
152    Info,
153
154    /// Downloads a package archive or source bundle
155    #[command(
156        alias = "dl",
157        long_about = "Downloads the binary archive (.zpa) or source bundle \
158                      (.zsa) for a package to the local cache or a specified \
159                      directory."
160    )]
161    Download {
162        /// Package identifier (e.g. @repo/name, path, or URL)
163        #[arg(value_name = "PACKAGE", required = true, help = PKG_SOURCE_HELP)]
164        package: String,
165
166        /// Download the binary archive (.zpa) [default]
167        #[arg(long, group = "type")]
168        archive: bool,
169
170        /// Download the source bundle (.zsa)
171        #[arg(long, group = "type")]
172        source: bool,
173
174        /// Directory to output the downloaded file to
175        #[arg(short, long)]
176        output_dir: Option<PathBuf>
177    },
178
179    /// Downloads or updates the package database from the remote repository
180    #[command(
181        alias = "sy",
182        long_about = "Clones the official package database from GitLab to \
183                      your local machine (~/.zoi/pkgs/db). If the database \
184                      already exists, it verifies the remote URL and pulls \
185                      the latest changes."
186    )]
187    Sync {
188        /// The sync subcommand to execute.
189        #[command(subcommand)]
190        command: Option<SyncCommands>,
191
192        /// Show the full git output
193        #[arg(short, long)]
194        verbose: bool,
195
196        /// Fallback to other mirrors if the default one fails
197        #[arg(long)]
198        fallback: bool,
199
200        /// Do not check for installed package managers
201        #[arg(long = "no-pm")]
202        no_package_managers: bool,
203
204        /// Force re-sync by removing existing databases and re-cloning from
205        /// scratch
206        #[arg(long)]
207        force: bool,
208
209        /// Sync registries to the project's local .zoi/pkgs/db/ using
210        /// revisions from zoi.lua
211        #[arg(long)]
212        local: bool,
213
214        /// When used with --local, sync using revisions from zoi.lock instead
215        /// of zoi.lua
216        #[arg(long)]
217        frozen: bool,
218
219        /// The scope to sync the registries to
220        #[arg(long, value_enum, conflicts_with = "local")]
221        scope: Option<SetupScope>
222    },
223
224    /// Migration helpers for converting external manifests to Zoi package files
225    Migrate(cmd::migrate::MigrateCommand),
226
227    /// Lists installed or all available packages
228    #[command(alias = "ls")]
229    List {
230        /// List all packages from the database, not just installed ones
231        #[arg(short, long)]
232        all: bool,
233        /// List only installed packages that have updates available
234        #[arg(short, long)]
235        outdated: bool,
236        /// Filter by registry handle (e.g. 'zoidberg')
237        #[arg(long)]
238        registry: Option<String>,
239        /// Filter by repository (e.g. 'main', 'extra')
240        #[arg(long)]
241        repo: Option<String>,
242        /// Filter by package type (package, app, collection, extension)
243        #[arg(short = 't', long = "type")]
244        package_type: Option<String>,
245        /// List packages not found in any configured registry
246        #[arg(short = 'm', long)]
247        foreign: bool,
248        /// List only package names (internal use for completions)
249        #[arg(long, hide = true)]
250        names: bool,
251        /// List packages with descriptions for completion
252        #[arg(long, hide = true)]
253        completion: bool
254    },
255
256    /// Shows detailed information about a package
257    Show {
258        /// The package identifier.
259        #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
260        package_name: String,
261        /// Display the raw, unformatted package file
262        #[arg(long)]
263        raw: bool,
264        /// Use PURL (Package URL) specification for resolving package
265        #[arg(long)]
266        purl: bool
267    },
268
269    /// Pin a package to a specific version
270    Pin {
271        /// The package identifier.
272        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
273        package: String,
274        /// The version to pin the package to
275        version: String
276    },
277
278    /// Find which package provides a specific command or file
279    Provides {
280        /// The command or file path to search for
281        term: String
282    },
283
284    /// Visualize the dependency tree of a package
285    Tree {
286        /// The package identifier(s).
287        #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
288        packages: Vec<String>
289    },
290
291    /// Unpin a package, allowing it to be updated
292    Unpin {
293        /// The package identifier.
294        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
295        package: String
296    },
297
298    /// Modify the installation reason of a package
299    #[command(
300        alias = "m",
301        long_about = "Changes whether a package is considered explicitly installed or a dependency. Explicit packages are not removed by 'autoremove', while dependencies are if no other package requires them.",
302        group(clap::ArgGroup::new("mode").required(true).args(["as_dependency", "as_explicit"]))
303    )]
304    Mark {
305        /// The package identifier(s).
306        #[arg(value_name = "INST_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
307        packages: Vec<String>,
308
309        /// Mark packages as dependencies
310        #[arg(long, aliases = ["asdeps"])]
311        as_dependency: bool,
312
313        /// Mark packages as explicitly installed
314        #[arg(long, aliases = ["asexpl"], conflicts_with = "as_dependency")]
315        as_explicit: bool
316    },
317
318    /// Updates one or more packages to their latest versions
319    #[command(
320        alias = "up",
321        arg_required_else_help = true,
322        group(clap::ArgGroup::new("target").required(true).args(["package_names", "all"]))
323    )]
324    Update {
325        /// The package names to update.
326        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
327        package_names: Vec<String>,
328
329        /// Update all installed packages
330        #[arg(long, conflicts_with = "package_names")]
331        all: bool,
332
333        /// Do not actually perform the update, just show what would be done
334        #[arg(long)]
335        dry_run: bool,
336        /// Explain why each selected update is included or skipped
337        #[arg(long)]
338        explain: bool,
339        /// Emit machine-readable update plan JSON
340        #[arg(long)]
341        plan_json: bool,
342        /// Interactively choose which upgradable packages to update (with
343        /// --all)
344        #[arg(long, requires = "all")]
345        interactive: bool
346    },
347
348    /// Installs one or more packages from a name, local file, URL, or git
349    /// repository
350    #[command(aliases = ["i", "in", "add"])]
351    Install(cmd::install::args::InstallArgs),
352
353    /// Add a tool to the current project or global configuration and install it
354    #[command(alias = "u")]
355    Use {
356        /// Package(s) to use (e.g. node@20)
357        #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
358        packages: Vec<String>,
359
360        /// Add to global configuration instead of project
361        #[arg(short, long)]
362        global: bool
363    },
364
365    /// Uninstalls one or more packages previously installed by Zoi
366    #[command(
367        aliases = ["un", "rm", "remove"],
368        long_about = "Removes one or more packages' files from the Zoi store and deletes their symlinks from the bin directory. This command will fail if a package was not installed by Zoi."
369    )]
370    Uninstall(cmd::uninstall::args::UninstallArgs),
371
372    /// Execute a command defined in a local zoi.yaml file
373    #[command(long_about = "Execute a command from zoi.yaml. If no command \
374                            is specified, it will launch an interactive \
375                            prompt to choose one.")]
376    Run {
377        /// The alias of the command to execute
378        cmd_alias: Option<String>,
379        /// Arguments to pass to the command
380        args: Vec<String>
381    },
382
383    /// Manage and set up project environments from a local zoi.yaml file
384    #[command(long_about = "Checks for required packages and runs setup \
385                            commands for a defined environment. If no \
386                            environment is specified, it launches an \
387                            interactive prompt.")]
388    Env {
389        /// The alias of the environment to set up
390        env_alias: Option<String>,
391
392        /// Export environment variables for the current shell
393        #[arg(long, value_enum, hide = true)]
394        export_shell: Option<Shell>
395    },
396
397    /// Enter a development shell for the current project
398    #[command(
399        alias = "develop",
400        long_about = "Loads the project configuration from zoi.yaml, ensures \
401                      all required packages are installed locally, sets up \
402                      environment variables (PATH, LD_LIBRARY_PATH, etc.), \
403                      and drops you into a subshell."
404    )]
405    Dev {
406        /// Command to run in the dev shell instead of an interactive shell
407        #[arg(short, long)]
408        run: Option<String>,
409        /// Temporary clone a repository and enter its development shell
410        #[arg(long)]
411        repo: Option<String>
412    },
413
414    /// Upgrades the Zoi binary to the latest version
415    #[command(
416        alias = "ug",
417        long_about = "Upgrades Zoi to the latest version. By default, it \
418                      attempts a delta upgrade (bsdiff) to minimize download \
419                      size. If the delta upgrade is unavailable or fails, it \
420                      automatically falls back to a full download."
421    )]
422    Upgrade {
423        /// Force a full download instead of a delta upgrade
424        #[arg(long)]
425        force: bool,
426
427        /// Upgrade to a specific git tag
428        #[arg(long)]
429        tag: Option<String>,
430
431        /// Upgrade to the latest release of a specific branch (e.g. Prod, Pub)
432        #[arg(long)]
433        branch: Option<String>
434    },
435
436    /// Removes packages that were installed as dependencies but are no longer
437    /// needed
438    Autoremove {
439        /// Do not actually remove packages, just show what would be done
440        #[arg(long)]
441        dry_run: bool
442    },
443
444    /// Explains why a package is installed
445    Why {
446        /// The package identifier.
447        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
448        package_name: String
449    },
450
451    /// Find which package owns a file
452    #[command(alias = "owns")]
453    Owner {
454        /// Path to the file
455        #[arg(value_hint = ValueHint::FilePath)]
456        path: std::path::PathBuf
457    },
458
459    /// List all files owned by a package
460    Files {
461        /// The package identifier.
462        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
463        package: String
464    },
465
466    /// Shows the history of package operations
467    History {
468        /// Verify audit log chain integrity instead of printing history
469        /// entries
470        #[arg(long, conflicts_with = "export")]
471        verify: bool,
472        /// Export audit history to a file (default format: JSON array with
473        /// chain fields)
474        #[arg(long, value_hint = ValueHint::FilePath, conflicts_with = "verify")]
475        export: Option<std::path::PathBuf>,
476        /// Export in newline-delimited JSON (ndjson) instead of a JSON array
477        #[arg(long, requires = "export")]
478        ndjson: bool
479    },
480
481    /// Searches for packages by name or description
482    #[command(
483        alias = "s",
484        long_about = "Searches for a case-insensitive term in the name, \
485                      description, and tags of all available packages in the \
486                      database. Filter by repo, type, or tags."
487    )]
488    Search {
489        /// The term to search for (e.g. 'editor', 'cli')
490        search_term: String,
491        /// Filter by registry handle (e.g. 'zoidberg')
492        #[arg(long)]
493        registry: Option<String>,
494        /// Filter by repository (e.g. 'main', 'extra')
495        #[arg(long)]
496        repo: Option<String>,
497        /// Filter by package type (package, app, collection, extension)
498        #[arg(long = "type")]
499        package_type: Option<String>,
500        /// Filter by tags (any match). Multiple via comma or repeated -t
501        #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
502        tags: Option<Vec<String>>,
503        /// Sort results by field (name, repo, type)
504        #[arg(long, default_value = "name")]
505        sort: String,
506        /// Search for files provided by packages instead of package names
507        #[arg(short, long)]
508        files: bool,
509        /// Open results in an interactive TUI
510        #[arg(short = 'i', long)]
511        interactive: bool
512    },
513
514    /// Manage background services for installed packages
515    #[command(alias = "svc")]
516    Service(cmd::service::ServiceCommand),
517
518    /// Set up shell completions or enter an ephemeral environment with specific
519    /// packages
520    #[command(
521        long_about = "If a shell is provided, it installs completion scripts. If 'hook' is provided, it outputs shell-specific hook scripts for auto-activation. If packages are provided via --package/-p, it enters a temporary subshell with those packages available in PATH.",
522        arg_required_else_help = true,
523        group(clap::ArgGroup::new("shell_action").required(true).args(["shell", "hook", "packages"]).multiple(true))
524    )]
525    Shell {
526        /// The shell to set up completions for
527        #[arg(value_enum)]
528        shell: Option<Shell>,
529        /// Generate a shell hook for automatic environment activation
530        #[arg(long)]
531        hook: bool,
532        /// The scope to apply the setup to (user or system-wide)
533        #[arg(long, value_enum, default_value = "user")]
534        scope: SetupScope,
535        /// Packages to include in the ephemeral environment
536        #[arg(short, long = "package", value_name = "ALL_PACKAGES")]
537        packages: Vec<String>,
538        /// Command to run in the ephemeral environment instead of an
539        /// interactive shell
540        #[arg(short, long)]
541        run: Option<String>,
542        /// Show additional details (resolution, installation progress, etc.)
543        #[arg(long, short)]
544        verbose: bool
545    },
546
547    /// Execute a package binary directly with its dependencies resolved
548    #[command(
549        alias = "x",
550        long_about = "Resolves a package and its dependencies, installs them \
551                      if needed, then runs the requested binary directly. By \
552                      default runs the first binary the package provides. \
553                      Uses bwrap for sandboxed packages."
554    )]
555    Exec {
556        /// The package source identifier.
557        #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
558        source: String,
559
560        /// Specific binary to run (required if package provides multiple
561        /// binaries)
562        #[arg(long)]
563        bin: Option<String>,
564
565        /// Show additional execution details
566        #[arg(long, short)]
567        verbose: bool,
568
569        /// Arguments to pass to the executed binary
570        #[arg(value_name = "ARGS")]
571        args: Vec<String>
572    },
573
574    /// Clears the cache of downloaded package binaries
575    Clean {
576        /// Do not actually clear the cache, just show what would be done
577        #[arg(long)]
578        dry_run: bool
579    },
580
581    /// Clones the git repository of a package
582    Clone {
583        /// The package identifier (e.g. @repo/name, path, or URL)
584        #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
585        package: String,
586        /// The location to clone the repository to
587        #[arg(value_name = "LOCATION")]
588        location: Option<String>
589    },
590
591    /// Manage Zoi's local cache
592    Cache {
593        /// The cache subcommand to execute.
594        #[command(subcommand)]
595        command: CacheCommands
596    },
597
598    /// Inspect recorded transactions
599    #[command(alias = "tx")]
600    Transaction {
601        /// The transaction subcommand to execute.
602        #[command(subcommand)]
603        command: TransactionCommands
604    },
605
606    /// Manage and author Zoi registries
607    #[command(alias = "reg")]
608    Registry(cmd::registry::RegistryCommand),
609
610    /// Manage declarative user environments (`ZoiOS` only)
611    Home(cmd::home::HomeCommand),
612
613    /// Manage the underlying `ZoiOS` system (`ZoiOS` only)
614    System(cmd::system::SystemCommand),
615
616    /// Manage package repositories
617    #[command(
618        aliases = ["repositories"],
619        long_about = "Manages the list of package repositories used by Zoi.\n\nCommands:\n- add (alias: a): Add an official repo by name or clone from a git URL.\n- remove|rm: Remove a repo from active list (repo rm <name>).\n- list|ls: Show active repositories by default; use 'list all' to show all available repositories.\n- git: Manage cloned git repositories (git ls, git rm <repo-name>)."
620    )]
621    Repo(cmd::repo::RepoCommand),
622
623    /// Manage telemetry settings (opt-in analytics)
624    #[command(long_about = "Manage opt-in anonymous telemetry used to \
625                            understand package popularity. Default is \
626                            disabled.")]
627    Telemetry {
628        /// The telemetry action to perform.
629        #[arg(value_enum)]
630        action: TelemetryAction
631    },
632
633    /// Create an application using a package template
634    Create {
635        /// The package source identifier.
636        #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
637        source: String,
638        /// The application name to substitute into template commands
639        app_name: Option<String>
640    },
641
642    /// Downgrade a package to a specific version from local cache or store
643    #[command(
644        alias = "dg",
645        long_about = "Interactively choose and install an older version of a \
646                      package from the local store or archive cache. This is \
647                      useful if a recent update has introduced bugs or \
648                      compatibility issues."
649    )]
650    Downgrade {
651        /// The package identifier.
652        #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
653        package: String
654    },
655
656    /// Manage Zoi extensions
657    #[command(alias = "ext")]
658    Extension(ExtensionCommand),
659
660    /// Rollback a package to the previously installed version
661    Rollback {
662        /// The package identifier.
663        #[arg(value_name = "INST_PACKAGES", required_unless_present = "last_transaction", help = PKG_SOURCE_HELP)]
664        package: Option<String>,
665
666        /// Rollback the last transaction
667        #[arg(long, conflicts_with = "package")]
668        last_transaction: bool
669    },
670
671    /// Shows a package's manual
672    Man {
673        /// The package identifier.
674        #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
675        package_name: String,
676        /// Always look at the upstream manual even if it's downloaded
677        #[arg(long)]
678        upstream: bool,
679        /// Print the manual to the terminal raw
680        #[arg(long)]
681        raw: bool,
682        /// Do not use the TUI, use the system pager instead
683        #[arg(long)]
684        no_tui: bool
685    },
686
687    /// Build, create, and manage Zoi packages
688    #[command(alias = "pkg")]
689    Package(cmd::package::PackageCommand),
690
691    /// Manage PGP keys for package signature verification
692    Pgp(cmd::pgp::PgpCommand),
693
694    /// Helper commands for various tasks
695    Helper(cmd::helper::HelperCommand),
696
697    /// Checks for common issues and provides actionable suggestions
698    Doctor,
699
700    /// Audit installed or all packages for security vulnerabilities
701    Audit {
702        /// Show all vulnerabilities from the database, not just for installed
703        /// packages
704        #[arg(short, long)]
705        all: bool,
706        /// Filter by registry handle
707        #[arg(long)]
708        registry: Option<String>,
709        /// Filter by repository
710        #[arg(long)]
711        repo: Option<String>
712    },
713
714    /// Execute an external subcommand.
715    #[command(external_subcommand)]
716    External(Vec<String>)
717}
718
719/// The extension management command.
720#[derive(clap::Parser, Debug)]
721pub struct ExtensionCommand {
722    /// The extension subcommand to execute.
723    #[command(subcommand)]
724    pub command: ExtensionCommands
725}
726
727/// The available extension subcommands.
728#[derive(clap::Subcommand, Debug)]
729pub enum ExtensionCommands {
730    /// Add an extension
731    Add {
732        /// The name of the extension to add
733        #[arg(required = true)]
734        name: String
735    },
736    /// Remove an extension
737    Remove {
738        /// The name of the extension to remove
739        #[arg(required = true)]
740        name: String
741    }
742}
743
744/// The available sync subcommands.
745#[derive(clap::Subcommand, Clone)]
746pub enum SyncCommands {
747    /// Add a new registry
748    Add {
749        /// URL of the registry to add
750        url: String
751    },
752    /// Remove a configured registry by its handle
753    Remove {
754        /// Handle of the registry to remove
755        handle: String
756    },
757    /// List configured registries
758    #[command(alias = "ls")]
759    List,
760    /// Set the default registry URL
761    Set {
762        /// URL or keyword (default, github, gitlab, codeberg)
763        url: String
764    }
765}
766
767/// The available cache management subcommands.
768#[derive(clap::Subcommand)]
769pub enum CacheCommands {
770    /// Add package archive(s) to the local cache
771    Add {
772        /// Path to the .zpa archive(s)
773        #[arg(required = true)]
774        files: Vec<std::path::PathBuf>
775    },
776    /// Clear the local cache
777    #[command(alias = "clean")]
778    Clear {
779        /// Do not actually clear the cache, just show what would be done
780        #[arg(long)]
781        dry_run: bool
782    },
783    /// List all archives currently in the cache
784    #[command(alias = "ls")]
785    List,
786    /// Manage cache mirrors used for archive downloads
787    Mirror {
788        /// The cache mirror subcommand to execute.
789        #[command(subcommand)]
790        command: CacheMirrorCommands
791    }
792}
793
794/// The available cache mirror management subcommands.
795#[derive(clap::Subcommand)]
796pub enum CacheMirrorCommands {
797    /// Add a cache mirror base URL
798    Add {
799        /// Mirror base URL
800        url: String
801    },
802    /// Remove a cache mirror base URL
803    Remove {
804        /// Mirror base URL
805        url: String
806    },
807    /// List configured cache mirrors
808    #[command(alias = "ls")]
809    List
810}
811
812/// The available transaction management subcommands.
813#[derive(clap::Subcommand)]
814pub enum TransactionCommands {
815    /// List known transaction logs
816    #[command(alias = "ls")]
817    List,
818    /// Show details for a transaction
819    Show {
820        /// Transaction ID
821        id: String
822    },
823    /// List modified files for a transaction
824    Files {
825        /// Transaction ID
826        id: String
827    }
828}
829
830/// The available actions for telemetry.
831#[derive(clap::ValueEnum, Clone)]
832enum TelemetryAction {
833    /// Show the current telemetry status.
834    Status,
835    /// Enable anonymous telemetry.
836    Enable,
837    /// Disable anonymous telemetry.
838    Disable
839}
840
841/// The main entry point for the Zoi CLI.
842///
843/// # Errors
844///
845/// Returns an error if argument parsing fails, plugin loading fails, or if any
846/// subcommand fails.
847pub fn run() -> anyhow::Result<()> {
848    let styles = styling::Styles::styled()
849        .header(
850            styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD
851        )
852        .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
853        .literal(styling::AnsiColor::Green.on_default())
854        .placeholder(styling::AnsiColor::Cyan.on_default());
855
856    let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
857    let cmd = Cli::command().styles(styles.clone());
858    let matches = cmd.clone().get_matches();
859    let cli = match Cli::from_arg_matches(&matches) {
860        Ok(cli) => cli,
861        Err(err) => {
862            err.print()?;
863            return Err(anyhow::anyhow!("Failed to parse arguments"));
864        }
865    };
866
867    if let Some(root) = cli.root {
868        crate::pkg::sysroot::set_sysroot(root);
869    }
870
871    let config = crate::pkg::config::read_config().unwrap_or_default();
872
873    let is_offline = cli.offline || config.offline_mode;
874    crate::pkg::offline::set_offline(is_offline);
875
876    let mut all_pkg_dirs = cli.pkg_dirs;
877    for dir in config.pkg_dirs {
878        let path = std::path::PathBuf::from(dir);
879        if !all_pkg_dirs.contains(&path) {
880            all_pkg_dirs.push(path);
881        }
882    }
883    crate::pkg::pkgdir::set_pkg_dirs(all_pkg_dirs);
884
885    utils::check_path();
886
887    if let Err(e) = crate::pkg::pgp::ensure_builtin_keys() {
888        eprintln!(
889            "{}: Failed to ensure builtin PGP keys: {}",
890            "Warning".yellow(),
891            e
892        );
893    }
894
895    let plugin_manager = crate::pkg::plugin::PluginManager::new()?;
896    if let Err(e) = plugin_manager.load_all(cli.yes) {
897        eprintln!("{}: Failed to load plugins: {}", "Warning".yellow(), e);
898    }
899
900    if cli.version_flag {
901        cmd::version::run(BRANCH, STATUS, NUMBER, commit);
902        return Ok(());
903    }
904
905    if let Some(command) = cli.command {
906        let needs_lock = matches!(
907            command,
908            Commands::Install { .. }
909                | Commands::Uninstall { .. }
910                | Commands::Update { .. }
911                | Commands::Autoremove { .. }
912                | Commands::Rollback { .. }
913                | Commands::Package(_)
914        );
915
916        let _lock_guard = if needs_lock {
917            Some(lock::acquire_lock()?)
918        } else {
919            None
920        };
921
922        let result = match command {
923            Commands::GenerateCompletions { shell } => {
924                let mut cmd = Cli::command();
925                let bin_name = cmd.get_name().to_string();
926                generate(shell, &mut cmd, bin_name, &mut io::stdout());
927                Ok(())
928            }
929            Commands::Complete {
930                shell,
931                index,
932                words
933            } => cmd::complete::run(shell, index, &words),
934            Commands::GenerateManual => cmd::gen_man::run().map_err(Into::into),
935            Commands::Version => {
936                cmd::version::run(BRANCH, STATUS, NUMBER, commit);
937                Ok(())
938            }
939            Commands::About => {
940                cmd::about::run(BRANCH, STATUS, NUMBER, commit);
941                Ok(())
942            }
943            Commands::Info => cmd::info::run(BRANCH, STATUS, NUMBER, commit),
944            Commands::Sync {
945                command,
946                verbose,
947                fallback,
948                no_package_managers,
949                force,
950                local,
951                frozen,
952                scope
953            } => {
954                if let Some(cmd) = command {
955                    match cmd {
956                        SyncCommands::Add { url } => {
957                            cmd::sync::add_registry(&url)
958                        }
959                        SyncCommands::Remove { handle } => {
960                            cmd::sync::remove_registry(&handle)
961                        }
962                        SyncCommands::List => cmd::sync::list_registries(),
963                        SyncCommands::Set { url } => {
964                            cmd::sync::set_registry(&url)
965                        }
966                    }
967                } else if local {
968                    plugin_manager.trigger_hook("on_pre_sync", None)?;
969                    let res =
970                        cmd::sync::run_local(verbose, fallback, force, frozen);
971                    plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
972                    res
973                } else {
974                    plugin_manager.trigger_hook("on_pre_sync", None)?;
975                    let res = cmd::sync::run(
976                        verbose,
977                        fallback,
978                        no_package_managers,
979                        force,
980                        scope
981                    );
982                    plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
983                    res
984                }
985            }
986            Commands::Migrate(args) => cmd::migrate::run(args),
987            Commands::List {
988                all,
989                outdated,
990                registry,
991                repo,
992                package_type,
993                foreign,
994                names,
995                completion
996            } => cmd::list::run(
997                all,
998                outdated,
999                registry.as_deref(),
1000                repo.as_deref(),
1001                package_type.as_deref(),
1002                foreign,
1003                names,
1004                completion
1005            ),
1006            Commands::Show {
1007                package_name,
1008                raw,
1009                purl
1010            } => cmd::show::run(&package_name, raw, purl),
1011            Commands::Pin { package, version } => {
1012                cmd::pin::run(&package, &version)
1013            }
1014            Commands::Provides { term } => cmd::provides::run(&term),
1015            Commands::Tree { packages } => cmd::tree::run(&packages),
1016            Commands::Unpin { package } => cmd::unpin::run(&package),
1017            Commands::Mark {
1018                packages,
1019                as_dependency,
1020                as_explicit
1021            } => cmd::mark::run(&packages, as_dependency, as_explicit),
1022            Commands::Update {
1023                package_names,
1024                all,
1025                dry_run,
1026                explain,
1027                plan_json,
1028                interactive
1029            } => cmd::update::run(
1030                all,
1031                &package_names,
1032                cli.yes,
1033                dry_run,
1034                explain,
1035                plan_json,
1036                interactive
1037            )
1038            .map_err(|e| cmd::ux::with_failure_hint("update", e)),
1039            Commands::Install(args) => args
1040                .run(cli.yes)
1041                .map_err(|e| cmd::ux::with_failure_hint("install", e)),
1042            Commands::Use { packages, global } => {
1043                cmd::use_cmd::run(&packages, global)
1044            }
1045            Commands::Uninstall(args) => args
1046                .run(cli.yes)
1047                .map_err(|e| cmd::ux::with_failure_hint("uninstall", e)),
1048            Commands::Run { cmd_alias, args } => {
1049                cmd::run::run(cmd_alias.as_deref(), &args)
1050            }
1051            Commands::Env {
1052                env_alias,
1053                export_shell
1054            } => cmd::env::run(env_alias.as_deref(), export_shell),
1055            Commands::Dev { run, repo } => cmd::dev::run(run, repo),
1056            Commands::Upgrade { force, tag, branch } => {
1057                match cmd::upgrade::run(
1058                    BRANCH, STATUS, NUMBER, force, tag, branch
1059                ) {
1060                    Ok(()) => {
1061                        println!(
1062                            "\n{}",
1063                            "Zoi upgraded successfully! Please restart your \
1064                             shell for changes to take effect."
1065                                .green()
1066                        );
1067                        println!(
1068                            "\n{}: https://github.com/zillowe/zoi/blob/main/CHANGELOG.md",
1069                            "Changelog".cyan().bold()
1070                        );
1071                        println!(
1072                            "\n{}: To update shell completions, run 'zoi \
1073                             shell <your-shell>'.",
1074                            "Hint".cyan().bold()
1075                        );
1076                    }
1077                    Err(e) if e.to_string() == "already_on_latest" => {}
1078                    Err(e) if e.to_string() == "managed_by_package_manager" => {
1079                    }
1080                    Err(e) => return Err(e)
1081                }
1082                Ok(())
1083            }
1084            Commands::Autoremove { dry_run } => {
1085                cmd::autoremove::run(cli.yes, dry_run)
1086            }
1087            Commands::Why { package_name } => cmd::why::run(&package_name),
1088            Commands::Owner { path } => cmd::owner::run(&path),
1089            Commands::Files { package } => cmd::files::run(&package),
1090            Commands::History {
1091                verify,
1092                export,
1093                ndjson
1094            } => cmd::history::run(verify, export, ndjson),
1095            Commands::Search {
1096                search_term,
1097                registry,
1098                repo,
1099                package_type,
1100                tags,
1101                sort,
1102                files,
1103                interactive
1104            } => cmd::search::run(
1105                &search_term,
1106                registry.as_deref(),
1107                repo.as_deref(),
1108                package_type.as_deref(),
1109                tags,
1110                &sort,
1111                files,
1112                interactive
1113            ),
1114            Commands::Service(args) => cmd::service::run(args),
1115            Commands::Shell {
1116                shell,
1117                hook,
1118                scope,
1119                packages,
1120                run,
1121                verbose
1122            } => {
1123                let target_shell = shell
1124                    .or_else(crate::pkg::utils::get_current_shell)
1125                    .unwrap_or(Shell::Bash);
1126                if hook {
1127                    cmd::shell::print_hook(target_shell)
1128                } else if !packages.is_empty() {
1129                    cmd::shell::enter_ephemeral_shell(
1130                        &packages,
1131                        run,
1132                        verbose,
1133                        Some(&plugin_manager)
1134                    )
1135                } else {
1136                    cmd::shell::run(target_shell, scope)
1137                }
1138            }
1139            Commands::Exec {
1140                source,
1141                bin,
1142                verbose,
1143                args
1144            } => cmd::exec::run(&source, bin, &args, verbose),
1145            Commands::Download {
1146                package,
1147                archive: _,
1148                source,
1149                output_dir
1150            } => {
1151                let download_type = if source {
1152                    cmd::download::DownloadType::Source
1153                } else {
1154                    cmd::download::DownloadType::Archive
1155                };
1156                cmd::download::run(&package, download_type, output_dir)
1157            }
1158            Commands::Clean { dry_run } => cmd::clean::run(dry_run),
1159            Commands::Clone { package, location } => {
1160                cmd::clone::run(&package, location, cli.yes)
1161            }
1162            Commands::Cache { command } => match command {
1163                CacheCommands::Add { files } => cmd::cache::add(&files),
1164                CacheCommands::Clear { dry_run } => cmd::cache::clear(dry_run),
1165                CacheCommands::List => cmd::cache::list(),
1166                CacheCommands::Mirror { command } => match command {
1167                    CacheMirrorCommands::Add { url } => {
1168                        cmd::cache::add_mirror(&url)
1169                    }
1170                    CacheMirrorCommands::Remove { url } => {
1171                        cmd::cache::remove_mirror(&url)
1172                    }
1173                    CacheMirrorCommands::List => cmd::cache::list_mirrors()
1174                }
1175            },
1176            Commands::Transaction { command } => match command {
1177                TransactionCommands::List => cmd::transaction::list(),
1178                TransactionCommands::Show { id } => cmd::transaction::show(&id),
1179                TransactionCommands::Files { id } => {
1180                    cmd::transaction::files(&id)
1181                }
1182            },
1183            Commands::Repo(args) => cmd::repo::run(args),
1184            Commands::Registry(args) => cmd::registry::run(args),
1185            Commands::Home(args) => cmd::home::run(args),
1186            Commands::System(args) => cmd::system::run(args, cli.yes),
1187            Commands::Telemetry { action } => {
1188                use cmd::telemetry::{TelemetryCommand, run};
1189                let cmd = match action {
1190                    TelemetryAction::Status => TelemetryCommand::Status,
1191                    TelemetryAction::Enable => TelemetryCommand::Enable,
1192                    TelemetryAction::Disable => TelemetryCommand::Disable
1193                };
1194                run(cmd)
1195            }
1196            Commands::Create { source, app_name } => cmd::create::run(
1197                cmd::create::CreateCommand { source, app_name },
1198                cli.yes,
1199                Some(&plugin_manager)
1200            ),
1201            Commands::Downgrade { package } => {
1202                cmd::downgrade::run(&package, cli.yes, Some(&plugin_manager))
1203            }
1204            Commands::Extension(args) => {
1205                cmd::extension::run(args, cli.yes, Some(&plugin_manager))
1206            }
1207            Commands::Rollback {
1208                package,
1209                last_transaction
1210            } => {
1211                if last_transaction {
1212                    cmd::rollback::run_transaction_rollback(
1213                        cli.yes,
1214                        Some(&plugin_manager)
1215                    )
1216                } else if let Some(pkg) = package {
1217                    cmd::rollback::run(&pkg, cli.yes, Some(&plugin_manager))
1218                } else {
1219                    Ok(())
1220                }
1221            }
1222            Commands::Man {
1223                package_name,
1224                upstream,
1225                raw,
1226                no_tui
1227            } => cmd::man::run(&package_name, upstream, raw, no_tui),
1228            Commands::Package(args) => cmd::package::run(args),
1229            Commands::Pgp(args) => cmd::pgp::run(args),
1230            Commands::Helper(args) => cmd::helper::run(args),
1231            Commands::Doctor => cmd::doctor::run(),
1232            Commands::Audit {
1233                all,
1234                registry,
1235                repo
1236            } => cmd::audit::run(all, registry, repo.as_deref()),
1237            Commands::External(args) => {
1238                let (cmd_name, cmd_args) =
1239                    if let Some((first, rest)) = args.split_first() {
1240                        (first, rest.to_vec())
1241                    } else {
1242                        return Err(anyhow::anyhow!("No command specified"));
1243                    };
1244
1245                match plugin_manager.run_command(cmd_name, cmd_args) {
1246                    Ok(true) => Ok(()),
1247                    Ok(false) => {
1248                        let mut shadow_cmd = Cli::command().styles(styles);
1249                        shadow_cmd =
1250                            shadow_cmd.allow_external_subcommands(false);
1251
1252                        let err = shadow_cmd
1253                            .clone()
1254                            .try_get_matches_from(std::env::args())
1255                            .err()
1256                            .unwrap_or_else(|| {
1257                                shadow_cmd.error(
1258                                    clap::error::ErrorKind::InvalidSubcommand,
1259                                    format!(
1260                                        "unrecognized subcommand '{cmd_name}'"
1261                                    )
1262                                )
1263                            });
1264
1265                        let plugin_cmds = plugin_manager.list_commands()?;
1266                        if !plugin_cmds.is_empty() {
1267                            eprintln!(
1268                                "{}:",
1269                                "Available Plugin Commands".cyan().bold()
1270                            );
1271                            for (pcmd, pdesc) in plugin_cmds {
1272                                if pdesc.is_empty() {
1273                                    eprintln!("  {pcmd}");
1274                                } else {
1275                                    eprintln!(
1276                                        "  {:<12} {}",
1277                                        pcmd,
1278                                        pdesc.dimmed()
1279                                    );
1280                                }
1281                            }
1282                            eprintln!();
1283                        }
1284
1285                        err.exit();
1286                    }
1287                    Err(e) => Err(e)
1288                }
1289            }
1290        };
1291
1292        if let Err(e) = result {
1293            eprintln!("Error: {e}");
1294            std::process::exit(1);
1295        }
1296    }
1297    Ok(())
1298}