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
15const BRANCH: &str = "Prod";
23
24const STATUS: &str = "Release";
28
29const NUMBER: &str = "1.29.3";
33const PKG_SOURCE_HELP: &str =
35 "Package identifier (e.g. @repo/name, #git@repo/name, path, or URL)";
36
37#[derive(Parser)]
42#[command(name = "zoi", author, about, long_about = None, disable_version_flag = true,
43 trailing_var_arg = true,
44 color = ColorChoice::Auto,
45 arg_required_else_help = true,
46)]
47pub struct Cli {
48 #[command(subcommand)]
50 command: Option<Commands>,
51
52 #[arg(
54 short = 'v',
55 long = "version",
56 help = "Print detailed version information"
57 )]
58 version_flag: bool,
59
60 #[arg(
62 short = 'y',
63 long,
64 help = "Automatically answer yes to all prompts",
65 global = true
66 )]
67 yes: bool,
68
69 #[arg(
71 long = "root",
72 help = "Operate on a different root directory",
73 global = true,
74 value_hint = ValueHint::DirPath
75 )]
76 pub root: Option<std::path::PathBuf>,
77
78 #[arg(
80 long = "offline",
81 help = "Do not attempt to connect to the network",
82 global = true
83 )]
84 pub offline: bool,
85
86 #[arg(
88 long = "pkg-dir",
89 help = "Additional directory to search for .zpa archives",
90 global = true,
91 value_hint = ValueHint::DirPath
92 )]
93 pub pkg_dirs: Vec<std::path::PathBuf>
95}
96
97#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
99pub enum SetupScope {
100 User,
102 System
104}
105
106#[derive(clap::ValueEnum, Clone, Debug, Copy)]
108pub enum InstallScope {
109 User,
111 System,
113 Project
115}
116
117#[derive(Subcommand)]
122pub enum AltCommand {
123 #[command(alias = "ls")]
125 List {
126 #[arg(short, long)]
128 verbose: bool
129 },
130 Get {
132 name: String
134 },
135 Set {
137 name: String,
139 path: String
141 },
142 Auto {
144 name: String
146 },
147 Remove {
149 name: String,
151 path: String
153 }
154}
155
156#[derive(Subcommand)]
158enum Commands {
159 #[command(hide = true)]
161 GenerateCompletions {
162 #[arg(value_enum)]
164 shell: Shell
165 },
166
167 #[command(hide = true)]
169 Complete {
170 #[arg(value_enum)]
172 shell: Shell,
173 index: usize,
175 words: Vec<String>
177 },
178
179 #[command(
181 alias = "v",
182 long_about = "Displays the version number, build status, branch, and \
183 commit hash. This is the same output provided by the -v \
184 and --version flags."
185 )]
186 Version,
187
188 #[command(long_about = "Displays the full application name, description, \
190 author, license, and homepage information.")]
191 About,
192
193 #[command(long_about = "Detects and displays key system details, \
195 including the OS, CPU architecture, Linux \
196 distribution (if applicable), and available \
197 package managers.")]
198 Info,
199
200 #[command(
202 alias = "dl",
203 long_about = "Downloads the binary archive (.zpa) or source bundle \
204 (.zsa) for a package to the local cache or a specified \
205 directory."
206 )]
207 Download {
208 #[arg(value_name = "PACKAGE", required = true, help = PKG_SOURCE_HELP)]
210 package: String,
211
212 #[arg(long, group = "type")]
214 archive: bool,
215
216 #[arg(long, group = "type")]
218 source: bool,
219
220 #[arg(short, long)]
222 output_dir: Option<PathBuf>
223 },
224
225 #[command(
227 alias = "sy",
228 long_about = "Clones the official package database from GitLab to \
229 Zoi's user data directory. If the database already \
230 exists, it verifies the remote URL and pulls the latest \
231 changes."
232 )]
233 Sync {
234 #[command(subcommand)]
236 command: Option<SyncCommands>,
237
238 #[arg(short, long)]
240 verbose: bool,
241
242 #[arg(long)]
244 fallback: bool,
245
246 #[arg(long = "no-pm")]
248 no_package_managers: bool,
249
250 #[arg(long)]
253 force: bool,
254
255 #[arg(long)]
258 local: bool,
259
260 #[arg(long)]
263 frozen: bool,
264
265 #[arg(long, value_enum, conflicts_with = "local")]
267 scope: Option<SetupScope>
268 },
269
270 Migrate(cmd::migrate::MigrateCommand),
272
273 #[command(alias = "ls")]
275 List {
276 #[arg(short, long)]
278 all: bool,
279 #[arg(short, long)]
281 outdated: bool,
282 #[arg(long)]
284 registry: Option<String>,
285 #[arg(long)]
287 repo: Option<String>,
288 #[arg(short = 't', long = "type")]
290 package_type: Option<String>,
291 #[arg(short = 'm', long)]
293 foreign: bool,
294 #[arg(long, hide = true)]
296 names: bool,
297 #[arg(long, hide = true)]
299 completion: bool
300 },
301
302 Show {
304 #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
306 package_name: String,
307 #[arg(long)]
309 raw: bool,
310 #[arg(long)]
312 purl: bool
313 },
314
315 Pin {
317 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
319 package: String,
320 version: String
322 },
323
324 Provides {
326 term: String
328 },
329
330 Tree {
332 #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
334 packages: Vec<String>
335 },
336
337 Unpin {
339 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
341 package: String
342 },
343
344 #[command(
346 alias = "m",
347 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.",
348 group(clap::ArgGroup::new("mode").required(true).args(["as_dependency", "as_explicit"]))
349 )]
350 Mark {
351 #[arg(value_name = "INST_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
353 packages: Vec<String>,
354
355 #[arg(long, aliases = ["asdeps"])]
357 as_dependency: bool,
358
359 #[arg(long, aliases = ["asexpl"], conflicts_with = "as_dependency")]
361 as_explicit: bool
362 },
363
364 #[command(
366 alias = "up",
367 arg_required_else_help = true,
368 group(clap::ArgGroup::new("target").required(true).args(["package_names", "all"]))
369 )]
370 Update {
371 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
373 package_names: Vec<String>,
374
375 #[arg(long, conflicts_with = "package_names")]
377 all: bool,
378
379 #[arg(long)]
381 dry_run: bool,
382 #[arg(long)]
384 explain: bool,
385 #[arg(long, requires = "dry_run")]
387 plan_json: bool,
388 #[arg(long, short)]
390 verbose: bool,
391 #[arg(long, requires = "all")]
394 interactive: bool
395 },
396
397 #[command(aliases = ["i", "in", "add"])]
400 Install(cmd::install::args::InstallArgs),
401
402 #[command(
404 aliases = ["un", "rm", "remove"],
405 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."
406 )]
407 Uninstall(cmd::uninstall::args::UninstallArgs),
408
409 #[command(long_about = "Execute a command from zoi.lua. If no command is \
411 specified, it will launch an interactive prompt \
412 to choose one.")]
413 Run {
414 cmd_alias: Option<String>,
416 args: Vec<String>
418 },
419
420 #[command(long_about = "Checks for required packages and runs setup \
422 commands for a defined environment. If no \
423 environment is specified, it launches an \
424 interactive prompt.")]
425 Env {
426 env_alias: Option<String>,
428
429 #[arg(long, value_enum, hide = true)]
431 export_shell: Option<Shell>
432 },
433
434 #[command(
436 alias = "develop",
437 long_about = "Loads the project configuration from zoi.lua, ensures \
438 all required packages are installed locally, sets up \
439 environment variables (PATH, LD_LIBRARY_PATH, etc.), \
440 and drops you into a subshell."
441 )]
442 Dev {
443 #[arg(short, long)]
445 run: Option<String>,
446 #[arg(long)]
448 repo: Option<String>
449 },
450
451 #[command(
453 alias = "ug",
454 long_about = "Upgrades Zoi to the latest version. By default, it \
455 attempts a delta upgrade (bsdiff) to minimize download \
456 size. If the delta upgrade is unavailable or fails, it \
457 automatically falls back to a full download."
458 )]
459 Upgrade {
460 #[arg(long)]
462 force: bool,
463
464 #[arg(long)]
466 tag: Option<String>,
467
468 #[arg(long)]
470 branch: Option<String>
471 },
472
473 Autoremove {
476 #[arg(long)]
478 dry_run: bool
479 },
480
481 Why {
483 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
485 package_name: String
486 },
487
488 #[command(alias = "owns")]
490 Owner {
491 #[arg(value_hint = ValueHint::FilePath)]
493 path: std::path::PathBuf
494 },
495
496 Files {
498 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
500 package: String
501 },
502
503 #[command(subcommand, name = "alt")]
510 Alt(AltCommand),
511
512 History {
514 #[arg(long, conflicts_with = "export")]
517 verify: bool,
518 #[arg(long, value_hint = ValueHint::FilePath, conflicts_with = "verify")]
521 export: Option<std::path::PathBuf>,
522 #[arg(long, requires = "export")]
524 ndjson: bool
525 },
526
527 #[command(
529 alias = "s",
530 long_about = "Searches for a case-insensitive term in the name, \
531 description, and tags of all available packages in the \
532 database. Filter by repo, type, or tags."
533 )]
534 Search {
535 search_term: String,
537 #[arg(long)]
539 registry: Option<String>,
540 #[arg(long)]
542 repo: Option<String>,
543 #[arg(long = "type")]
545 package_type: Option<String>,
546 #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
548 tags: Option<Vec<String>>,
549 #[arg(long, default_value = "name")]
551 sort: String,
552 #[arg(short, long)]
554 files: bool,
555 #[arg(short = 'i', long)]
557 interactive: bool
558 },
559
560 #[command(alias = "svc")]
562 Service(cmd::service::ServiceCommand),
563
564 #[command(
567 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.",
568 arg_required_else_help = true,
569 group(clap::ArgGroup::new("shell_action").required(true).args(["shell", "hook", "packages"]).multiple(true))
570 )]
571 Shell {
572 #[arg(value_enum)]
574 shell: Option<Shell>,
575 #[arg(long)]
577 hook: bool,
578 #[arg(long, value_enum, default_value = "user")]
580 scope: SetupScope,
581 #[arg(short, long = "package", value_name = "ALL_PACKAGES")]
583 packages: Vec<String>,
584 #[arg(short, long)]
587 run: Option<String>,
588 #[arg(long, short)]
590 verbose: bool
591 },
592
593 #[command(
595 alias = "x",
596 long_about = "Resolves a package and its dependencies, installs them \
597 if needed, then runs the requested binary directly. By \
598 default runs the first binary the package provides. \
599 Uses bwrap for sandboxed packages."
600 )]
601 Exec {
602 #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
604 source: String,
605
606 #[arg(long)]
609 bin: Option<String>,
610
611 #[arg(long, short)]
613 verbose: bool,
614
615 #[arg(value_name = "ARGS")]
617 args: Vec<String>
618 },
619
620 Clone {
622 #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
624 package: String,
625 #[arg(value_name = "LOCATION")]
627 location: Option<String>
628 },
629
630 Cache {
632 #[command(subcommand)]
634 command: CacheCommands
635 },
636
637 #[command(alias = "tx")]
639 Transaction {
640 #[command(subcommand)]
642 command: TransactionCommands
643 },
644
645 #[command(alias = "reg")]
647 Registry(cmd::registry::RegistryCommand),
648
649 Home(cmd::home::HomeCommand),
651
652 System(cmd::system::SystemCommand),
654
655 #[command(
657 aliases = ["repositories"],
658 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>)."
659 )]
660 Repo(cmd::repo::RepoCommand),
661
662 #[command(long_about = "Manage opt-in anonymous telemetry used to \
664 understand package popularity. Default is \
665 disabled.")]
666 Telemetry {
667 #[command(subcommand)]
669 command: TelemetrySubcommand
670 },
671
672 Init {
674 #[arg(short, long)]
676 force: bool
677 },
678
679 Create {
681 #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
683 source: String,
684 app_name: Option<String>
686 },
687
688 #[command(
690 alias = "dg",
691 long_about = "Interactively choose and install an older version of a \
692 package from the local store or archive cache. This is \
693 useful if a recent update has introduced bugs or \
694 compatibility issues."
695 )]
696 Downgrade {
697 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
699 package: String
700 },
701
702 #[command(alias = "ext")]
704 Extension(ExtensionCommand),
705
706 Rollback {
708 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
710 package: String
711 },
712
713 Man {
715 #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
717 package_name: String,
718 #[arg(long)]
720 upstream: bool,
721 #[arg(long)]
723 raw: bool,
724 #[arg(long)]
727 page: Option<String>
728 },
729
730 #[command(alias = "pkg")]
732 Package(cmd::package::PackageCommand),
733
734 Pgp(cmd::pgp::PgpCommand),
736
737 Helper(cmd::helper::HelperCommand),
739
740 Doctor,
742
743 Audit {
745 #[arg(short, long)]
748 all: bool,
749 #[arg(long)]
751 registry: Option<String>,
752 #[arg(long)]
754 repo: Option<String>
755 },
756
757 #[command(external_subcommand)]
759 External(Vec<String>)
760}
761
762#[derive(clap::Parser, Debug)]
764pub struct ExtensionCommand {
765 #[command(subcommand)]
767 pub command: ExtensionCommands
768}
769
770#[derive(clap::Subcommand, Debug)]
772pub enum ExtensionCommands {
773 Add {
775 #[arg(required = true)]
777 name: String
778 },
779 Remove {
781 #[arg(required = true)]
783 name: String
784 }
785}
786
787#[derive(clap::Subcommand, Clone)]
789pub enum SyncCommands {
790 Add {
792 url: String
794 },
795 Remove {
797 handle: String
799 },
800 #[command(alias = "ls")]
802 List,
803 Set {
805 url: String
807 }
808}
809
810#[derive(clap::Subcommand)]
812pub enum CacheCommands {
813 Add {
815 #[arg(required = true)]
817 files: Vec<std::path::PathBuf>
818 },
819 #[command(alias = "clean")]
821 Clear {
822 #[arg(long)]
824 dry_run: bool
825 },
826 #[command(alias = "ls")]
828 List,
829 Mirror {
831 #[command(subcommand)]
833 command: CacheMirrorCommands
834 }
835}
836
837#[derive(clap::Subcommand)]
839pub enum CacheMirrorCommands {
840 Add {
842 url: String
844 },
845 Remove {
847 url: String
849 },
850 #[command(alias = "ls")]
852 List
853}
854
855#[derive(clap::Subcommand)]
857pub enum TransactionCommands {
858 #[command(alias = "ls")]
860 List,
861 Show {
863 id: String
865 },
866 Files {
868 id: String
870 },
871 #[command(alias = "rollback")]
874 Undo {
875 id: Option<String>,
877
878 #[arg(long)]
880 yes: bool
881 }
882}
883
884#[derive(clap::Subcommand, Clone)]
886pub enum TelemetrySubcommand {
887 Status,
889 Enable,
891 Disable,
893 Crash {
896 #[command(subcommand)]
898 command: CrashSubcommand
899 },
900 #[command(hide = true)]
903 Flush
904}
905
906#[derive(clap::Subcommand, Clone)]
908pub enum CrashSubcommand {
909 #[command(alias = "ls")]
911 List,
912 Show {
914 file: PathBuf
916 },
917 Send {
919 file: PathBuf,
921 #[arg(long)]
923 yes: bool
924 },
925 Clear {
927 #[arg(long)]
929 yes: bool
930 }
931}
932
933fn command_name(command: &Commands) -> &'static str {
935 match command {
936 Commands::GenerateCompletions { .. } => "generate-completions",
937 Commands::Complete { .. } => "complete",
938 Commands::Version => "version",
939 Commands::About => "about",
940 Commands::Info => "info",
941 Commands::Download { .. } => "download",
942 Commands::Sync { .. } => "sync",
943 Commands::Migrate(_) => "migrate",
944 Commands::List { .. } => "list",
945 Commands::Show { .. } => "show",
946 Commands::Pin { .. } => "pin",
947 Commands::Provides { .. } => "provides",
948 Commands::Tree { .. } => "tree",
949 Commands::Unpin { .. } => "unpin",
950 Commands::Mark { .. } => "mark",
951 Commands::Update { .. } => "update",
952 Commands::Install(_) => "install",
953 Commands::Uninstall(_) => "uninstall",
954 Commands::Run { .. } => "run",
955 Commands::Env { .. } => "env",
956 Commands::Dev { .. } => "dev",
957 Commands::Upgrade { .. } => "upgrade",
958 Commands::Autoremove { .. } => "autoremove",
959 Commands::Why { .. } => "why",
960 Commands::Owner { .. } => "owner",
961 Commands::Files { .. } => "files",
962 Commands::Alt(_) => "alt",
963 Commands::History { .. } => "history",
964 Commands::Search { .. } => "search",
965 Commands::Service(_) => "service",
966 Commands::Shell { .. } => "shell",
967 Commands::Exec { .. } => "exec",
968 Commands::Clone { .. } => "clone",
969 Commands::Cache { .. } => "cache",
970 Commands::Transaction { .. } => "transaction",
971 Commands::Repo(_) => "repo",
972 Commands::Registry(_) => "registry",
973 Commands::Home(_) => "home",
974 Commands::System { .. } => "system",
975 Commands::Telemetry { .. } => "telemetry",
976 Commands::Create { .. } => "create",
977 Commands::Init { .. } => "init",
978 Commands::Downgrade { .. } => "downgrade",
979 Commands::Extension(_) => "extension",
980 Commands::Rollback { .. } => "rollback",
981 Commands::Man { .. } => "man",
982 Commands::Package(_) => "package",
983 Commands::Pgp(_) => "pgp",
984 Commands::Helper(_) => "helper",
985 Commands::Doctor => "doctor",
986 Commands::Audit { .. } => "audit",
987 Commands::External(_) => "external"
988 }
989}
990
991pub fn run() -> anyhow::Result<()> {
998 let styles = styling::Styles::styled()
999 .header(
1000 styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD
1001 )
1002 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
1003 .literal(styling::AnsiColor::Green.on_default())
1004 .placeholder(styling::AnsiColor::Cyan.on_default());
1005
1006 let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
1007 let cmd = Cli::command().styles(styles.clone());
1008 let matches = cmd.clone().get_matches();
1009 let cli = match Cli::from_arg_matches(&matches) {
1010 Ok(cli) => cli,
1011 Err(err) => {
1012 err.print()?;
1013 return Err(anyhow::anyhow!("Failed to parse arguments"));
1014 }
1015 };
1016
1017 if let Some(root) = cli.root {
1018 crate::pkg::sysroot::set_sysroot(root);
1019 }
1020
1021 let config = crate::pkg::config::read_config().unwrap_or_default();
1022
1023 let is_offline = cli.offline || config.offline_mode;
1024 crate::pkg::offline::set_offline(is_offline);
1025
1026 let prompt_pending_crashes =
1034 !matches!(cli.command, Some(Commands::Telemetry { .. }));
1035 let _telemetry_guards =
1036 zoi_telemetry::init_telemetry(prompt_pending_crashes);
1037
1038 let mut all_pkg_dirs = cli.pkg_dirs;
1039 for dir in config.pkg_dirs {
1040 let path = std::path::PathBuf::from(dir);
1041 if !all_pkg_dirs.contains(&path) {
1042 all_pkg_dirs.push(path);
1043 }
1044 }
1045 crate::pkg::pkgdir::set_pkg_dirs(all_pkg_dirs);
1046
1047 utils::check_path();
1048
1049 if let Err(e) = crate::pkg::pgp::ensure_builtin_keys() {
1050 eprintln!(
1051 "{}: Failed to ensure builtin PGP keys: {}",
1052 "Warning".yellow(),
1053 e
1054 );
1055 }
1056
1057 let plugin_manager = crate::pkg::plugin::PluginManager::new()?;
1058 if let Err(e) = plugin_manager.load_all(cli.yes) {
1059 eprintln!("{}: Failed to load plugins: {}", "Warning".yellow(), e);
1060 }
1061
1062 if cli.version_flag {
1063 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
1064 return Ok(());
1065 }
1066
1067 if let Some(command) = cli.command {
1068 let command_name = command_name(&command);
1069 let is_flush_command = matches!(
1073 command,
1074 Commands::Telemetry {
1075 command: TelemetrySubcommand::Flush
1076 }
1077 );
1078 unsafe {
1082 std::env::set_var("ZOI_CRASH_COMMAND", command_name);
1083 }
1084
1085 let needs_lock = matches!(
1086 command,
1087 Commands::Install { .. }
1088 | Commands::Uninstall { .. }
1089 | Commands::Update { .. }
1090 | Commands::Autoremove { .. }
1091 | Commands::Rollback { .. }
1092 | Commands::Package(_)
1093 );
1094
1095 let _lock_guard = if needs_lock {
1096 Some(lock::acquire_lock()?)
1097 } else {
1098 None
1099 };
1100
1101 let result = match command {
1102 Commands::GenerateCompletions { shell } => {
1103 let mut cmd = Cli::command();
1104 let bin_name = cmd.get_name().to_string();
1105 generate(shell, &mut cmd, bin_name, &mut io::stdout());
1106 Ok(())
1107 }
1108 Commands::Complete {
1109 shell,
1110 index,
1111 words
1112 } => cmd::complete::run(shell, index, &words),
1113 Commands::Version => {
1114 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
1115 Ok(())
1116 }
1117 Commands::About => {
1118 cmd::about::run(BRANCH, STATUS, NUMBER, commit);
1119 Ok(())
1120 }
1121 Commands::Info => cmd::info::run(BRANCH, STATUS, NUMBER, commit),
1122 Commands::Sync {
1123 command,
1124 verbose,
1125 fallback,
1126 no_package_managers,
1127 force,
1128 local,
1129 frozen,
1130 scope
1131 } => {
1132 if let Some(cmd) = command {
1133 match cmd {
1134 SyncCommands::Add { url } => {
1135 cmd::sync::add_registry(&url)
1136 }
1137 SyncCommands::Remove { handle } => {
1138 cmd::sync::remove_registry(&handle)
1139 }
1140 SyncCommands::List => cmd::sync::list_registries(),
1141 SyncCommands::Set { url } => {
1142 cmd::sync::set_registry(&url)
1143 }
1144 }
1145 } else if local {
1146 plugin_manager.trigger_hook("on_pre_sync", None)?;
1147 let res =
1148 cmd::sync::run_local(verbose, fallback, force, frozen);
1149 plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
1150 res
1151 } else {
1152 plugin_manager.trigger_hook("on_pre_sync", None)?;
1153 let res = cmd::sync::run(
1154 verbose,
1155 fallback,
1156 no_package_managers,
1157 force,
1158 scope
1159 );
1160 plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
1161 res
1162 }
1163 }
1164 Commands::Migrate(args) => cmd::migrate::run(args),
1165 Commands::List {
1166 all,
1167 outdated,
1168 registry,
1169 repo,
1170 package_type,
1171 foreign,
1172 names,
1173 completion
1174 } => cmd::list::run(
1175 all,
1176 outdated,
1177 registry.as_deref(),
1178 repo.as_deref(),
1179 package_type.as_deref(),
1180 foreign,
1181 names,
1182 completion
1183 ),
1184 Commands::Show {
1185 package_name,
1186 raw,
1187 purl
1188 } => cmd::show::run(&package_name, raw, purl),
1189 Commands::Pin { package, version } => {
1190 cmd::pin::run(&package, &version, cli.yes)
1191 }
1192 Commands::Provides { term } => cmd::provides::run(&term),
1193 Commands::Tree { packages } => cmd::tree::run(&packages),
1194 Commands::Unpin { package } => cmd::unpin::run(&package, cli.yes),
1195 Commands::Mark {
1196 packages,
1197 as_dependency,
1198 as_explicit
1199 } => cmd::mark::run(&packages, as_dependency, as_explicit),
1200 Commands::Update {
1201 package_names,
1202 all,
1203 dry_run,
1204 explain,
1205 plan_json,
1206 verbose,
1207 interactive
1208 } => cmd::update::run(
1209 all,
1210 &package_names,
1211 cli.yes,
1212 dry_run,
1213 explain,
1214 plan_json,
1215 verbose,
1216 interactive
1217 )
1218 .map_err(|e| cmd::ux::with_failure_hint("update", e)),
1219 Commands::Install(args) => args
1220 .run(cli.yes)
1221 .map_err(|e| cmd::ux::with_failure_hint("install", e)),
1222 Commands::Uninstall(args) => args
1223 .run(cli.yes)
1224 .map_err(|e| cmd::ux::with_failure_hint("uninstall", e)),
1225 Commands::Run { cmd_alias, args } => {
1226 cmd::run::run(cmd_alias.as_deref(), &args)
1227 }
1228 Commands::Env {
1229 env_alias,
1230 export_shell
1231 } => cmd::env::run(env_alias.as_deref(), export_shell),
1232 Commands::Dev { run, repo } => cmd::dev::run(run, repo),
1233 Commands::Upgrade { force, tag, branch } => {
1234 match cmd::upgrade::run(
1235 BRANCH, STATUS, NUMBER, force, tag, branch
1236 ) {
1237 Ok(()) => {
1238 println!(
1239 "\n{}",
1240 "Zoi upgraded successfully! Please restart your \
1241 shell for changes to take effect."
1242 .green()
1243 );
1244 println!(
1245 "\n{}: https://github.com/zillowe/zoi/blob/main/CHANGELOG.md",
1246 "Changelog".cyan().bold()
1247 );
1248 println!(
1249 "\n{}: To update shell completions, run 'zoi \
1250 shell <your-shell>'.",
1251 "Hint".cyan().bold()
1252 );
1253 }
1254 Err(e) if e.to_string() == "already_on_latest" => {}
1255 Err(e) if e.to_string() == "managed_by_package_manager" => {
1256 }
1257 Err(e) => return Err(e)
1258 }
1259 Ok(())
1260 }
1261 Commands::Autoremove { dry_run } => {
1262 cmd::autoremove::run(cli.yes, dry_run)
1263 }
1264 Commands::Why { package_name } => cmd::why::run(&package_name),
1265 Commands::Owner { path } => cmd::owner::run(&path),
1266 Commands::Alt(ref args) => cmd::alt::run(args),
1267 Commands::Files { package } => cmd::files::run(&package),
1268 Commands::History {
1269 verify,
1270 export,
1271 ndjson
1272 } => cmd::history::run(verify, export, ndjson),
1273 Commands::Search {
1274 search_term,
1275 registry,
1276 repo,
1277 package_type,
1278 tags,
1279 sort,
1280 files,
1281 interactive
1282 } => cmd::search::run(
1283 &search_term,
1284 registry.as_deref(),
1285 repo.as_deref(),
1286 package_type.as_deref(),
1287 tags,
1288 &sort,
1289 files,
1290 interactive
1291 ),
1292 Commands::Service(args) => cmd::service::run(args),
1293 Commands::Shell {
1294 shell,
1295 hook,
1296 scope,
1297 packages,
1298 run,
1299 verbose
1300 } => {
1301 let target_shell = shell
1302 .or_else(crate::pkg::utils::get_current_shell)
1303 .unwrap_or(Shell::Bash);
1304 if hook {
1305 cmd::shell::print_hook(target_shell)
1306 } else if !packages.is_empty() {
1307 cmd::shell::enter_ephemeral_shell(
1308 &packages,
1309 run,
1310 verbose,
1311 Some(&plugin_manager)
1312 )
1313 } else {
1314 cmd::shell::run(target_shell, scope)
1315 }
1316 }
1317 Commands::Exec {
1318 source,
1319 bin,
1320 verbose,
1321 args
1322 } => cmd::exec::run(&source, bin, &args, verbose),
1323 Commands::Download {
1324 package,
1325 archive: _,
1326 source,
1327 output_dir
1328 } => {
1329 let download_type = if source {
1330 cmd::download::DownloadType::Source
1331 } else {
1332 cmd::download::DownloadType::Archive
1333 };
1334 cmd::download::run(&package, download_type, output_dir)
1335 }
1336 Commands::Clone { package, location } => {
1337 cmd::clone::run(&package, location, cli.yes)
1338 }
1339 Commands::Cache { command } => match command {
1340 CacheCommands::Add { files } => cmd::cache::add(&files),
1341 CacheCommands::Clear { dry_run } => cmd::cache::clear(dry_run),
1342 CacheCommands::List => cmd::cache::list(),
1343 CacheCommands::Mirror { command } => match command {
1344 CacheMirrorCommands::Add { url } => {
1345 cmd::cache::add_mirror(&url)
1346 }
1347 CacheMirrorCommands::Remove { url } => {
1348 cmd::cache::remove_mirror(&url)
1349 }
1350 CacheMirrorCommands::List => cmd::cache::list_mirrors()
1351 }
1352 },
1353 Commands::Transaction { command } => match command {
1354 TransactionCommands::List => cmd::transaction::list(),
1355 TransactionCommands::Show { id } => cmd::transaction::show(&id),
1356 TransactionCommands::Files { id } => {
1357 cmd::transaction::files(&id)
1358 }
1359 TransactionCommands::Undo { id, yes } => {
1360 cmd::transaction::undo(id.as_deref(), yes, cli.yes)
1361 }
1362 },
1363 Commands::Repo(args) => cmd::repo::run(args),
1364 Commands::Registry(args) => cmd::registry::run(args),
1365 Commands::Home(args) => cmd::home::run(args),
1366 Commands::System(args) => cmd::system::run(args, cli.yes),
1367 Commands::Telemetry { command } => {
1368 use cmd::telemetry::{CrashCommand, TelemetryCommand, run};
1369 let cmd = match command {
1370 TelemetrySubcommand::Status => TelemetryCommand::Status,
1371 TelemetrySubcommand::Enable => TelemetryCommand::Enable,
1372 TelemetrySubcommand::Disable => TelemetryCommand::Disable,
1373 TelemetrySubcommand::Flush => TelemetryCommand::Flush,
1374 TelemetrySubcommand::Crash { command } => {
1375 TelemetryCommand::Crash(match command {
1376 CrashSubcommand::List => CrashCommand::List,
1377 CrashSubcommand::Show { file } => {
1378 CrashCommand::Show { file }
1379 }
1380 CrashSubcommand::Send { file, yes } => {
1381 CrashCommand::Send {
1382 file,
1383 yes: yes || cli.yes
1384 }
1385 }
1386 CrashSubcommand::Clear { yes } => {
1387 CrashCommand::Clear {
1388 yes: yes || cli.yes
1389 }
1390 }
1391 })
1392 }
1393 };
1394 run(cmd)
1395 }
1396 Commands::Create { source, app_name } => cmd::create::run(
1397 cmd::create::CreateCommand { source, app_name },
1398 cli.yes,
1399 Some(&plugin_manager)
1400 ),
1401 Commands::Init { force } => cmd::init::run(cli.yes, force),
1402 Commands::Downgrade { package } => {
1403 cmd::downgrade::run(&package, cli.yes, Some(&plugin_manager))
1404 }
1405 Commands::Extension(args) => {
1406 cmd::extension::run(args, cli.yes, Some(&plugin_manager))
1407 }
1408 Commands::Rollback { package } => {
1409 cmd::rollback::run(&package, cli.yes, Some(&plugin_manager))
1410 }
1411 Commands::Man {
1412 package_name,
1413 upstream,
1414 raw,
1415 page
1416 } => cmd::man::run(&package_name, upstream, raw, page.as_deref()),
1417 Commands::Package(args) => cmd::package::run(args),
1418 Commands::Pgp(args) => cmd::pgp::run(args),
1419 Commands::Helper(args) => cmd::helper::run(args),
1420 Commands::Doctor => cmd::doctor::run(),
1421 Commands::Audit {
1422 all,
1423 registry,
1424 repo
1425 } => cmd::audit::run(all, registry, repo.as_deref()),
1426 Commands::External(args) => {
1427 let (cmd_name, cmd_args) =
1428 if let Some((first, rest)) = args.split_first() {
1429 (first, rest.to_vec())
1430 } else {
1431 return Err(anyhow::anyhow!("No command specified"));
1432 };
1433
1434 match plugin_manager.run_command(cmd_name, cmd_args) {
1435 Ok(true) => Ok(()),
1436 Ok(false) => {
1437 let mut shadow_cmd = Cli::command().styles(styles);
1438 shadow_cmd =
1439 shadow_cmd.allow_external_subcommands(false);
1440
1441 let err = shadow_cmd
1442 .clone()
1443 .try_get_matches_from(std::env::args())
1444 .err()
1445 .unwrap_or_else(|| {
1446 shadow_cmd.error(
1447 clap::error::ErrorKind::InvalidSubcommand,
1448 format!(
1449 "unrecognized subcommand '{cmd_name}'"
1450 )
1451 )
1452 });
1453
1454 let plugin_cmds = plugin_manager.list_commands()?;
1455 if !plugin_cmds.is_empty() {
1456 eprintln!(
1457 "{}:",
1458 "Available Plugin Commands".cyan().bold()
1459 );
1460 for (pcmd, pdesc) in plugin_cmds {
1461 if pdesc.is_empty() {
1462 eprintln!(" {pcmd}");
1463 } else {
1464 eprintln!(
1465 " {:<12} {}",
1466 pcmd,
1467 pdesc.dimmed()
1468 );
1469 }
1470 }
1471 eprintln!();
1472 }
1473
1474 err.exit();
1475 }
1476 Err(e) => Err(e)
1477 }
1478 }
1479 };
1480
1481 unsafe {
1484 std::env::remove_var("ZOI_CRASH_COMMAND");
1485 }
1486
1487 match &result {
1488 Ok(()) => {
1489 if !is_flush_command {
1496 let _ = zoi_telemetry::posthog_capture_dau_ping();
1497 }
1498 }
1499 Err(e) => {
1500 zoi_telemetry::sentry_capture_error(command_name, e.as_ref());
1501 }
1502 }
1503
1504 if let Err(e) = result {
1505 eprintln!("Error: {e}");
1506 zoi_telemetry::shutdown_sentry();
1512 std::process::exit(1);
1513 }
1514 }
1515 Ok(())
1516}