Skip to main content

zoi_cli/
cli.rs

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