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 = "Production";
18const STATUS: &str = "Release";
20const NUMBER: &str = "1.28.0";
22const PKG_SOURCE_HELP: &str =
24 "Package identifier (e.g. @repo/name, #git@repo/name, path, or URL)";
25
26#[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 #[command(subcommand)]
39 command: Option<Commands>,
40
41 #[arg(
43 short = 'v',
44 long = "version",
45 help = "Print detailed version information"
46 )]
47 version_flag: bool,
48
49 #[arg(
51 short = 'y',
52 long,
53 help = "Automatically answer yes to all prompts",
54 global = true
55 )]
56 yes: bool,
57
58 #[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 #[arg(
69 long = "offline",
70 help = "Do not attempt to connect to the network",
71 global = true
72 )]
73 pub offline: bool,
74
75 #[arg(
77 long = "pkg-dir",
78 help = "Additional directory to search for .zpa archives",
79 global = true,
80 value_hint = ValueHint::DirPath
81 )]
82 pub pkg_dirs: Vec<std::path::PathBuf>
84}
85
86#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
88pub enum SetupScope {
89 User,
91 System
93}
94
95#[derive(clap::ValueEnum, Clone, Debug, Copy)]
97pub enum InstallScope {
98 User,
100 System,
102 Project
104}
105
106#[derive(Subcommand)]
108enum Commands {
109 #[command(hide = true)]
111 GenerateCompletions {
112 #[arg(value_enum)]
114 shell: Shell
115 },
116
117 #[command(hide = true)]
119 Complete {
120 #[arg(value_enum)]
122 shell: Shell,
123 index: usize,
125 words: Vec<String>
127 },
128
129 #[command(
131 alias = "v",
132 long_about = "Displays the version number, build status, branch, and \
133 commit hash. This is the same output provided by the -v \
134 and --version flags."
135 )]
136 Version,
137
138 #[command(long_about = "Displays the full application name, description, \
140 author, license, and homepage information.")]
141 About,
142
143 #[command(long_about = "Detects and displays key system details, \
145 including the OS, CPU architecture, Linux \
146 distribution (if applicable), and available \
147 package managers.")]
148 Info,
149
150 #[command(
152 alias = "dl",
153 long_about = "Downloads the binary archive (.zpa) or source bundle \
154 (.zsa) for a package to the local cache or a specified \
155 directory."
156 )]
157 Download {
158 #[arg(value_name = "PACKAGE", required = true, help = PKG_SOURCE_HELP)]
160 package: String,
161
162 #[arg(long, group = "type")]
164 archive: bool,
165
166 #[arg(long, group = "type")]
168 source: bool,
169
170 #[arg(short, long)]
172 output_dir: Option<PathBuf>
173 },
174
175 #[command(
177 alias = "sy",
178 long_about = "Clones the official package database from GitLab to \
179 Zoi's user data directory. If the database already \
180 exists, it verifies the remote URL and pulls the latest \
181 changes."
182 )]
183 Sync {
184 #[command(subcommand)]
186 command: Option<SyncCommands>,
187
188 #[arg(short, long)]
190 verbose: bool,
191
192 #[arg(long)]
194 fallback: bool,
195
196 #[arg(long = "no-pm")]
198 no_package_managers: bool,
199
200 #[arg(long)]
203 force: bool,
204
205 #[arg(long)]
208 local: bool,
209
210 #[arg(long)]
213 frozen: bool,
214
215 #[arg(long, value_enum, conflicts_with = "local")]
217 scope: Option<SetupScope>
218 },
219
220 Migrate(cmd::migrate::MigrateCommand),
222
223 #[command(alias = "ls")]
225 List {
226 #[arg(short, long)]
228 all: bool,
229 #[arg(short, long)]
231 outdated: bool,
232 #[arg(long)]
234 registry: Option<String>,
235 #[arg(long)]
237 repo: Option<String>,
238 #[arg(short = 't', long = "type")]
240 package_type: Option<String>,
241 #[arg(short = 'm', long)]
243 foreign: bool,
244 #[arg(long, hide = true)]
246 names: bool,
247 #[arg(long, hide = true)]
249 completion: bool
250 },
251
252 Show {
254 #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
256 package_name: String,
257 #[arg(long)]
259 raw: bool,
260 #[arg(long)]
262 purl: bool
263 },
264
265 Pin {
267 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
269 package: String,
270 version: String
272 },
273
274 Provides {
276 term: String
278 },
279
280 Tree {
282 #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
284 packages: Vec<String>
285 },
286
287 Unpin {
289 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
291 package: String
292 },
293
294 #[command(
296 alias = "m",
297 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.",
298 group(clap::ArgGroup::new("mode").required(true).args(["as_dependency", "as_explicit"]))
299 )]
300 Mark {
301 #[arg(value_name = "INST_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
303 packages: Vec<String>,
304
305 #[arg(long, aliases = ["asdeps"])]
307 as_dependency: bool,
308
309 #[arg(long, aliases = ["asexpl"], conflicts_with = "as_dependency")]
311 as_explicit: bool
312 },
313
314 #[command(
316 alias = "up",
317 arg_required_else_help = true,
318 group(clap::ArgGroup::new("target").required(true).args(["package_names", "all"]))
319 )]
320 Update {
321 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
323 package_names: Vec<String>,
324
325 #[arg(long, conflicts_with = "package_names")]
327 all: bool,
328
329 #[arg(long)]
331 dry_run: bool,
332 #[arg(long)]
334 explain: bool,
335 #[arg(long, requires = "dry_run")]
337 plan_json: bool,
338 #[arg(long, short)]
340 verbose: bool,
341 #[arg(long, requires = "all")]
344 interactive: bool
345 },
346
347 #[command(aliases = ["i", "in", "add"])]
350 Install(cmd::install::args::InstallArgs),
351
352 #[command(alias = "u")]
354 Use {
355 #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
357 packages: Vec<String>,
358
359 #[arg(short, long)]
361 global: bool
362 },
363
364 #[command(
366 aliases = ["un", "rm", "remove"],
367 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."
368 )]
369 Uninstall(cmd::uninstall::args::UninstallArgs),
370
371 #[command(long_about = "Execute a command from zoi.lua. If no command is \
373 specified, it will launch an interactive prompt \
374 to choose one.")]
375 Run {
376 cmd_alias: Option<String>,
378 args: Vec<String>
380 },
381
382 #[command(long_about = "Checks for required packages and runs setup \
384 commands for a defined environment. If no \
385 environment is specified, it launches an \
386 interactive prompt.")]
387 Env {
388 env_alias: Option<String>,
390
391 #[arg(long, value_enum, hide = true)]
393 export_shell: Option<Shell>
394 },
395
396 #[command(
398 alias = "develop",
399 long_about = "Loads the project configuration from zoi.lua, ensures \
400 all required packages are installed locally, sets up \
401 environment variables (PATH, LD_LIBRARY_PATH, etc.), \
402 and drops you into a subshell."
403 )]
404 Dev {
405 #[arg(short, long)]
407 run: Option<String>,
408 #[arg(long)]
410 repo: Option<String>
411 },
412
413 #[command(
415 alias = "ug",
416 long_about = "Upgrades Zoi to the latest version. By default, it \
417 attempts a delta upgrade (bsdiff) to minimize download \
418 size. If the delta upgrade is unavailable or fails, it \
419 automatically falls back to a full download."
420 )]
421 Upgrade {
422 #[arg(long)]
424 force: bool,
425
426 #[arg(long)]
428 tag: Option<String>,
429
430 #[arg(long)]
432 branch: Option<String>
433 },
434
435 Autoremove {
438 #[arg(long)]
440 dry_run: bool
441 },
442
443 Why {
445 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
447 package_name: String
448 },
449
450 #[command(alias = "owns")]
452 Owner {
453 #[arg(value_hint = ValueHint::FilePath)]
455 path: std::path::PathBuf
456 },
457
458 Files {
460 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
462 package: String
463 },
464
465 History {
467 #[arg(long, conflicts_with = "export")]
470 verify: bool,
471 #[arg(long, value_hint = ValueHint::FilePath, conflicts_with = "verify")]
474 export: Option<std::path::PathBuf>,
475 #[arg(long, requires = "export")]
477 ndjson: bool
478 },
479
480 #[command(
482 alias = "s",
483 long_about = "Searches for a case-insensitive term in the name, \
484 description, and tags of all available packages in the \
485 database. Filter by repo, type, or tags."
486 )]
487 Search {
488 search_term: String,
490 #[arg(long)]
492 registry: Option<String>,
493 #[arg(long)]
495 repo: Option<String>,
496 #[arg(long = "type")]
498 package_type: Option<String>,
499 #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
501 tags: Option<Vec<String>>,
502 #[arg(long, default_value = "name")]
504 sort: String,
505 #[arg(short, long)]
507 files: bool,
508 #[arg(short = 'i', long)]
510 interactive: bool
511 },
512
513 #[command(alias = "svc")]
515 Service(cmd::service::ServiceCommand),
516
517 #[command(
520 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.",
521 arg_required_else_help = true,
522 group(clap::ArgGroup::new("shell_action").required(true).args(["shell", "hook", "packages"]).multiple(true))
523 )]
524 Shell {
525 #[arg(value_enum)]
527 shell: Option<Shell>,
528 #[arg(long)]
530 hook: bool,
531 #[arg(long, value_enum, default_value = "user")]
533 scope: SetupScope,
534 #[arg(short, long = "package", value_name = "ALL_PACKAGES")]
536 packages: Vec<String>,
537 #[arg(short, long)]
540 run: Option<String>,
541 #[arg(long, short)]
543 verbose: bool
544 },
545
546 #[command(
548 alias = "x",
549 long_about = "Resolves a package and its dependencies, installs them \
550 if needed, then runs the requested binary directly. By \
551 default runs the first binary the package provides. \
552 Uses bwrap for sandboxed packages."
553 )]
554 Exec {
555 #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
557 source: String,
558
559 #[arg(long)]
562 bin: Option<String>,
563
564 #[arg(long, short)]
566 verbose: bool,
567
568 #[arg(value_name = "ARGS")]
570 args: Vec<String>
571 },
572
573 Clone {
575 #[arg(value_name = "ALL_PACKAGES", required = true, help = PKG_SOURCE_HELP)]
577 package: String,
578 #[arg(value_name = "LOCATION")]
580 location: Option<String>
581 },
582
583 Cache {
585 #[command(subcommand)]
587 command: CacheCommands
588 },
589
590 #[command(alias = "tx")]
592 Transaction {
593 #[command(subcommand)]
595 command: TransactionCommands
596 },
597
598 #[command(alias = "reg")]
600 Registry(cmd::registry::RegistryCommand),
601
602 Home(cmd::home::HomeCommand),
604
605 System(cmd::system::SystemCommand),
607
608 #[command(
610 aliases = ["repositories"],
611 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>)."
612 )]
613 Repo(cmd::repo::RepoCommand),
614
615 #[command(long_about = "Manage opt-in anonymous telemetry used to \
617 understand package popularity. Default is \
618 disabled.")]
619 Telemetry {
620 #[command(subcommand)]
622 command: TelemetrySubcommand
623 },
624
625 Create {
627 #[arg(value_name = "ALL_SOURCES", help = PKG_SOURCE_HELP)]
629 source: String,
630 app_name: Option<String>
632 },
633
634 #[command(
636 alias = "dg",
637 long_about = "Interactively choose and install an older version of a \
638 package from the local store or archive cache. This is \
639 useful if a recent update has introduced bugs or \
640 compatibility issues."
641 )]
642 Downgrade {
643 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
645 package: String
646 },
647
648 #[command(alias = "ext")]
650 Extension(ExtensionCommand),
651
652 Rollback {
654 #[arg(value_name = "INST_PACKAGES", help = PKG_SOURCE_HELP)]
656 package: String
657 },
658
659 Man {
661 #[arg(value_name = "ALL_PACKAGES", help = PKG_SOURCE_HELP)]
663 package_name: String,
664 #[arg(long)]
666 upstream: bool,
667 #[arg(long)]
669 raw: bool,
670 #[arg(long)]
673 page: Option<String>
674 },
675
676 #[command(alias = "pkg")]
678 Package(cmd::package::PackageCommand),
679
680 Pgp(cmd::pgp::PgpCommand),
682
683 Helper(cmd::helper::HelperCommand),
685
686 Doctor,
688
689 Audit {
691 #[arg(short, long)]
694 all: bool,
695 #[arg(long)]
697 registry: Option<String>,
698 #[arg(long)]
700 repo: Option<String>
701 },
702
703 #[command(external_subcommand)]
705 External(Vec<String>)
706}
707
708#[derive(clap::Parser, Debug)]
710pub struct ExtensionCommand {
711 #[command(subcommand)]
713 pub command: ExtensionCommands
714}
715
716#[derive(clap::Subcommand, Debug)]
718pub enum ExtensionCommands {
719 Add {
721 #[arg(required = true)]
723 name: String
724 },
725 Remove {
727 #[arg(required = true)]
729 name: String
730 }
731}
732
733#[derive(clap::Subcommand, Clone)]
735pub enum SyncCommands {
736 Add {
738 url: String
740 },
741 Remove {
743 handle: String
745 },
746 #[command(alias = "ls")]
748 List,
749 Set {
751 url: String
753 }
754}
755
756#[derive(clap::Subcommand)]
758pub enum CacheCommands {
759 Add {
761 #[arg(required = true)]
763 files: Vec<std::path::PathBuf>
764 },
765 #[command(alias = "clean")]
767 Clear {
768 #[arg(long)]
770 dry_run: bool
771 },
772 #[command(alias = "ls")]
774 List,
775 Mirror {
777 #[command(subcommand)]
779 command: CacheMirrorCommands
780 }
781}
782
783#[derive(clap::Subcommand)]
785pub enum CacheMirrorCommands {
786 Add {
788 url: String
790 },
791 Remove {
793 url: String
795 },
796 #[command(alias = "ls")]
798 List
799}
800
801#[derive(clap::Subcommand)]
803pub enum TransactionCommands {
804 #[command(alias = "ls")]
806 List,
807 Show {
809 id: String
811 },
812 Files {
814 id: String
816 },
817 #[command(alias = "rollback")]
820 Undo {
821 id: Option<String>,
823
824 #[arg(long)]
826 yes: bool
827 }
828}
829
830#[derive(clap::Subcommand, Clone)]
832pub enum TelemetrySubcommand {
833 Status,
835 Enable,
837 Disable,
839 Crash {
842 #[command(subcommand)]
844 command: CrashSubcommand
845 },
846 #[command(hide = true)]
849 Flush
850}
851
852#[derive(clap::Subcommand, Clone)]
854pub enum CrashSubcommand {
855 #[command(alias = "ls")]
857 List,
858 Show {
860 file: PathBuf
862 },
863 Send {
865 file: PathBuf,
867 #[arg(long)]
869 yes: bool
870 },
871 Clear {
873 #[arg(long)]
875 yes: bool
876 }
877}
878
879fn command_name(command: &Commands) -> &'static str {
881 match command {
882 Commands::GenerateCompletions { .. } => "generate-completions",
883 Commands::Complete { .. } => "complete",
884 Commands::Version => "version",
885 Commands::About => "about",
886 Commands::Info => "info",
887 Commands::Download { .. } => "download",
888 Commands::Sync { .. } => "sync",
889 Commands::Migrate(_) => "migrate",
890 Commands::List { .. } => "list",
891 Commands::Show { .. } => "show",
892 Commands::Pin { .. } => "pin",
893 Commands::Provides { .. } => "provides",
894 Commands::Tree { .. } => "tree",
895 Commands::Unpin { .. } => "unpin",
896 Commands::Mark { .. } => "mark",
897 Commands::Update { .. } => "update",
898 Commands::Install(_) => "install",
899 Commands::Use { .. } => "use",
900 Commands::Uninstall(_) => "uninstall",
901 Commands::Run { .. } => "run",
902 Commands::Env { .. } => "env",
903 Commands::Dev { .. } => "dev",
904 Commands::Upgrade { .. } => "upgrade",
905 Commands::Autoremove { .. } => "autoremove",
906 Commands::Why { .. } => "why",
907 Commands::Owner { .. } => "owner",
908 Commands::Files { .. } => "files",
909 Commands::History { .. } => "history",
910 Commands::Search { .. } => "search",
911 Commands::Service(_) => "service",
912 Commands::Shell { .. } => "shell",
913 Commands::Exec { .. } => "exec",
914 Commands::Clone { .. } => "clone",
915 Commands::Cache { .. } => "cache",
916 Commands::Transaction { .. } => "transaction",
917 Commands::Repo(_) => "repo",
918 Commands::Registry(_) => "registry",
919 Commands::Home(_) => "home",
920 Commands::System { .. } => "system",
921 Commands::Telemetry { .. } => "telemetry",
922 Commands::Create { .. } => "create",
923 Commands::Downgrade { .. } => "downgrade",
924 Commands::Extension(_) => "extension",
925 Commands::Rollback { .. } => "rollback",
926 Commands::Man { .. } => "man",
927 Commands::Package(_) => "package",
928 Commands::Pgp(_) => "pgp",
929 Commands::Helper(_) => "helper",
930 Commands::Doctor => "doctor",
931 Commands::Audit { .. } => "audit",
932 Commands::External(_) => "external"
933 }
934}
935
936pub fn run() -> anyhow::Result<()> {
943 let styles = styling::Styles::styled()
944 .header(
945 styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD
946 )
947 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
948 .literal(styling::AnsiColor::Green.on_default())
949 .placeholder(styling::AnsiColor::Cyan.on_default());
950
951 let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
952 let cmd = Cli::command().styles(styles.clone());
953 let matches = cmd.clone().get_matches();
954 let cli = match Cli::from_arg_matches(&matches) {
955 Ok(cli) => cli,
956 Err(err) => {
957 err.print()?;
958 return Err(anyhow::anyhow!("Failed to parse arguments"));
959 }
960 };
961
962 if let Some(root) = cli.root {
963 crate::pkg::sysroot::set_sysroot(root);
964 }
965
966 let config = crate::pkg::config::read_config().unwrap_or_default();
967
968 let is_offline = cli.offline || config.offline_mode;
969 crate::pkg::offline::set_offline(is_offline);
970
971 let prompt_pending_crashes =
979 !matches!(cli.command, Some(Commands::Telemetry { .. }));
980 let _telemetry_guards =
981 zoi_telemetry::init_telemetry(prompt_pending_crashes);
982
983 let mut all_pkg_dirs = cli.pkg_dirs;
984 for dir in config.pkg_dirs {
985 let path = std::path::PathBuf::from(dir);
986 if !all_pkg_dirs.contains(&path) {
987 all_pkg_dirs.push(path);
988 }
989 }
990 crate::pkg::pkgdir::set_pkg_dirs(all_pkg_dirs);
991
992 utils::check_path();
993
994 if let Err(e) = crate::pkg::pgp::ensure_builtin_keys() {
995 eprintln!(
996 "{}: Failed to ensure builtin PGP keys: {}",
997 "Warning".yellow(),
998 e
999 );
1000 }
1001
1002 let plugin_manager = crate::pkg::plugin::PluginManager::new()?;
1003 if let Err(e) = plugin_manager.load_all(cli.yes) {
1004 eprintln!("{}: Failed to load plugins: {}", "Warning".yellow(), e);
1005 }
1006
1007 if cli.version_flag {
1008 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
1009 return Ok(());
1010 }
1011
1012 if let Some(command) = cli.command {
1013 let command_name = command_name(&command);
1014 let is_flush_command = matches!(
1018 command,
1019 Commands::Telemetry {
1020 command: TelemetrySubcommand::Flush
1021 }
1022 );
1023 unsafe {
1027 std::env::set_var("ZOI_CRASH_COMMAND", command_name);
1028 }
1029
1030 let needs_lock = matches!(
1031 command,
1032 Commands::Install { .. }
1033 | Commands::Uninstall { .. }
1034 | Commands::Update { .. }
1035 | Commands::Autoremove { .. }
1036 | Commands::Rollback { .. }
1037 | Commands::Package(_)
1038 );
1039
1040 let _lock_guard = if needs_lock {
1041 Some(lock::acquire_lock()?)
1042 } else {
1043 None
1044 };
1045
1046 let result = match command {
1047 Commands::GenerateCompletions { shell } => {
1048 let mut cmd = Cli::command();
1049 let bin_name = cmd.get_name().to_string();
1050 generate(shell, &mut cmd, bin_name, &mut io::stdout());
1051 Ok(())
1052 }
1053 Commands::Complete {
1054 shell,
1055 index,
1056 words
1057 } => cmd::complete::run(shell, index, &words),
1058 Commands::Version => {
1059 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
1060 Ok(())
1061 }
1062 Commands::About => {
1063 cmd::about::run(BRANCH, STATUS, NUMBER, commit);
1064 Ok(())
1065 }
1066 Commands::Info => cmd::info::run(BRANCH, STATUS, NUMBER, commit),
1067 Commands::Sync {
1068 command,
1069 verbose,
1070 fallback,
1071 no_package_managers,
1072 force,
1073 local,
1074 frozen,
1075 scope
1076 } => {
1077 if let Some(cmd) = command {
1078 match cmd {
1079 SyncCommands::Add { url } => {
1080 cmd::sync::add_registry(&url)
1081 }
1082 SyncCommands::Remove { handle } => {
1083 cmd::sync::remove_registry(&handle)
1084 }
1085 SyncCommands::List => cmd::sync::list_registries(),
1086 SyncCommands::Set { url } => {
1087 cmd::sync::set_registry(&url)
1088 }
1089 }
1090 } else if local {
1091 plugin_manager.trigger_hook("on_pre_sync", None)?;
1092 let res =
1093 cmd::sync::run_local(verbose, fallback, force, frozen);
1094 plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
1095 res
1096 } else {
1097 plugin_manager.trigger_hook("on_pre_sync", None)?;
1098 let res = cmd::sync::run(
1099 verbose,
1100 fallback,
1101 no_package_managers,
1102 force,
1103 scope
1104 );
1105 plugin_manager.trigger_hook_nonfatal("on_post_sync", None);
1106 res
1107 }
1108 }
1109 Commands::Migrate(args) => cmd::migrate::run(args),
1110 Commands::List {
1111 all,
1112 outdated,
1113 registry,
1114 repo,
1115 package_type,
1116 foreign,
1117 names,
1118 completion
1119 } => cmd::list::run(
1120 all,
1121 outdated,
1122 registry.as_deref(),
1123 repo.as_deref(),
1124 package_type.as_deref(),
1125 foreign,
1126 names,
1127 completion
1128 ),
1129 Commands::Show {
1130 package_name,
1131 raw,
1132 purl
1133 } => cmd::show::run(&package_name, raw, purl),
1134 Commands::Pin { package, version } => {
1135 cmd::pin::run(&package, &version)
1136 }
1137 Commands::Provides { term } => cmd::provides::run(&term),
1138 Commands::Tree { packages } => cmd::tree::run(&packages),
1139 Commands::Unpin { package } => cmd::unpin::run(&package),
1140 Commands::Mark {
1141 packages,
1142 as_dependency,
1143 as_explicit
1144 } => cmd::mark::run(&packages, as_dependency, as_explicit),
1145 Commands::Update {
1146 package_names,
1147 all,
1148 dry_run,
1149 explain,
1150 plan_json,
1151 verbose,
1152 interactive
1153 } => cmd::update::run(
1154 all,
1155 &package_names,
1156 cli.yes,
1157 dry_run,
1158 explain,
1159 plan_json,
1160 verbose,
1161 interactive
1162 )
1163 .map_err(|e| cmd::ux::with_failure_hint("update", e)),
1164 Commands::Install(args) => args
1165 .run(cli.yes)
1166 .map_err(|e| cmd::ux::with_failure_hint("install", e)),
1167 Commands::Use { packages, global } => {
1168 cmd::use_cmd::run(&packages, global)
1169 }
1170 Commands::Uninstall(args) => args
1171 .run(cli.yes)
1172 .map_err(|e| cmd::ux::with_failure_hint("uninstall", e)),
1173 Commands::Run { cmd_alias, args } => {
1174 cmd::run::run(cmd_alias.as_deref(), &args)
1175 }
1176 Commands::Env {
1177 env_alias,
1178 export_shell
1179 } => cmd::env::run(env_alias.as_deref(), export_shell),
1180 Commands::Dev { run, repo } => cmd::dev::run(run, repo),
1181 Commands::Upgrade { force, tag, branch } => {
1182 match cmd::upgrade::run(
1183 BRANCH, STATUS, NUMBER, force, tag, branch
1184 ) {
1185 Ok(()) => {
1186 println!(
1187 "\n{}",
1188 "Zoi upgraded successfully! Please restart your \
1189 shell for changes to take effect."
1190 .green()
1191 );
1192 println!(
1193 "\n{}: https://github.com/zillowe/zoi/blob/main/CHANGELOG.md",
1194 "Changelog".cyan().bold()
1195 );
1196 println!(
1197 "\n{}: To update shell completions, run 'zoi \
1198 shell <your-shell>'.",
1199 "Hint".cyan().bold()
1200 );
1201 }
1202 Err(e) if e.to_string() == "already_on_latest" => {}
1203 Err(e) if e.to_string() == "managed_by_package_manager" => {
1204 }
1205 Err(e) => return Err(e)
1206 }
1207 Ok(())
1208 }
1209 Commands::Autoremove { dry_run } => {
1210 cmd::autoremove::run(cli.yes, dry_run)
1211 }
1212 Commands::Why { package_name } => cmd::why::run(&package_name),
1213 Commands::Owner { path } => cmd::owner::run(&path),
1214 Commands::Files { package } => cmd::files::run(&package),
1215 Commands::History {
1216 verify,
1217 export,
1218 ndjson
1219 } => cmd::history::run(verify, export, ndjson),
1220 Commands::Search {
1221 search_term,
1222 registry,
1223 repo,
1224 package_type,
1225 tags,
1226 sort,
1227 files,
1228 interactive
1229 } => cmd::search::run(
1230 &search_term,
1231 registry.as_deref(),
1232 repo.as_deref(),
1233 package_type.as_deref(),
1234 tags,
1235 &sort,
1236 files,
1237 interactive
1238 ),
1239 Commands::Service(args) => cmd::service::run(args),
1240 Commands::Shell {
1241 shell,
1242 hook,
1243 scope,
1244 packages,
1245 run,
1246 verbose
1247 } => {
1248 let target_shell = shell
1249 .or_else(crate::pkg::utils::get_current_shell)
1250 .unwrap_or(Shell::Bash);
1251 if hook {
1252 cmd::shell::print_hook(target_shell)
1253 } else if !packages.is_empty() {
1254 cmd::shell::enter_ephemeral_shell(
1255 &packages,
1256 run,
1257 verbose,
1258 Some(&plugin_manager)
1259 )
1260 } else {
1261 cmd::shell::run(target_shell, scope)
1262 }
1263 }
1264 Commands::Exec {
1265 source,
1266 bin,
1267 verbose,
1268 args
1269 } => cmd::exec::run(&source, bin, &args, verbose),
1270 Commands::Download {
1271 package,
1272 archive: _,
1273 source,
1274 output_dir
1275 } => {
1276 let download_type = if source {
1277 cmd::download::DownloadType::Source
1278 } else {
1279 cmd::download::DownloadType::Archive
1280 };
1281 cmd::download::run(&package, download_type, output_dir)
1282 }
1283 Commands::Clone { package, location } => {
1284 cmd::clone::run(&package, location, cli.yes)
1285 }
1286 Commands::Cache { command } => match command {
1287 CacheCommands::Add { files } => cmd::cache::add(&files),
1288 CacheCommands::Clear { dry_run } => cmd::cache::clear(dry_run),
1289 CacheCommands::List => cmd::cache::list(),
1290 CacheCommands::Mirror { command } => match command {
1291 CacheMirrorCommands::Add { url } => {
1292 cmd::cache::add_mirror(&url)
1293 }
1294 CacheMirrorCommands::Remove { url } => {
1295 cmd::cache::remove_mirror(&url)
1296 }
1297 CacheMirrorCommands::List => cmd::cache::list_mirrors()
1298 }
1299 },
1300 Commands::Transaction { command } => match command {
1301 TransactionCommands::List => cmd::transaction::list(),
1302 TransactionCommands::Show { id } => cmd::transaction::show(&id),
1303 TransactionCommands::Files { id } => {
1304 cmd::transaction::files(&id)
1305 }
1306 TransactionCommands::Undo { id, yes } => {
1307 cmd::transaction::undo(id.as_deref(), yes, cli.yes)
1308 }
1309 },
1310 Commands::Repo(args) => cmd::repo::run(args),
1311 Commands::Registry(args) => cmd::registry::run(args),
1312 Commands::Home(args) => cmd::home::run(args),
1313 Commands::System(args) => cmd::system::run(args, cli.yes),
1314 Commands::Telemetry { command } => {
1315 use cmd::telemetry::{CrashCommand, TelemetryCommand, run};
1316 let cmd = match command {
1317 TelemetrySubcommand::Status => TelemetryCommand::Status,
1318 TelemetrySubcommand::Enable => TelemetryCommand::Enable,
1319 TelemetrySubcommand::Disable => TelemetryCommand::Disable,
1320 TelemetrySubcommand::Flush => TelemetryCommand::Flush,
1321 TelemetrySubcommand::Crash { command } => {
1322 TelemetryCommand::Crash(match command {
1323 CrashSubcommand::List => CrashCommand::List,
1324 CrashSubcommand::Show { file } => {
1325 CrashCommand::Show { file }
1326 }
1327 CrashSubcommand::Send { file, yes } => {
1328 CrashCommand::Send {
1329 file,
1330 yes: yes || cli.yes
1331 }
1332 }
1333 CrashSubcommand::Clear { yes } => {
1334 CrashCommand::Clear {
1335 yes: yes || cli.yes
1336 }
1337 }
1338 })
1339 }
1340 };
1341 run(cmd)
1342 }
1343 Commands::Create { source, app_name } => cmd::create::run(
1344 cmd::create::CreateCommand { source, app_name },
1345 cli.yes,
1346 Some(&plugin_manager)
1347 ),
1348 Commands::Downgrade { package } => {
1349 cmd::downgrade::run(&package, cli.yes, Some(&plugin_manager))
1350 }
1351 Commands::Extension(args) => {
1352 cmd::extension::run(args, cli.yes, Some(&plugin_manager))
1353 }
1354 Commands::Rollback { package } => {
1355 cmd::rollback::run(&package, cli.yes, Some(&plugin_manager))
1356 }
1357 Commands::Man {
1358 package_name,
1359 upstream,
1360 raw,
1361 page
1362 } => cmd::man::run(&package_name, upstream, raw, page.as_deref()),
1363 Commands::Package(args) => cmd::package::run(args),
1364 Commands::Pgp(args) => cmd::pgp::run(args),
1365 Commands::Helper(args) => cmd::helper::run(args),
1366 Commands::Doctor => cmd::doctor::run(),
1367 Commands::Audit {
1368 all,
1369 registry,
1370 repo
1371 } => cmd::audit::run(all, registry, repo.as_deref()),
1372 Commands::External(args) => {
1373 let (cmd_name, cmd_args) =
1374 if let Some((first, rest)) = args.split_first() {
1375 (first, rest.to_vec())
1376 } else {
1377 return Err(anyhow::anyhow!("No command specified"));
1378 };
1379
1380 match plugin_manager.run_command(cmd_name, cmd_args) {
1381 Ok(true) => Ok(()),
1382 Ok(false) => {
1383 let mut shadow_cmd = Cli::command().styles(styles);
1384 shadow_cmd =
1385 shadow_cmd.allow_external_subcommands(false);
1386
1387 let err = shadow_cmd
1388 .clone()
1389 .try_get_matches_from(std::env::args())
1390 .err()
1391 .unwrap_or_else(|| {
1392 shadow_cmd.error(
1393 clap::error::ErrorKind::InvalidSubcommand,
1394 format!(
1395 "unrecognized subcommand '{cmd_name}'"
1396 )
1397 )
1398 });
1399
1400 let plugin_cmds = plugin_manager.list_commands()?;
1401 if !plugin_cmds.is_empty() {
1402 eprintln!(
1403 "{}:",
1404 "Available Plugin Commands".cyan().bold()
1405 );
1406 for (pcmd, pdesc) in plugin_cmds {
1407 if pdesc.is_empty() {
1408 eprintln!(" {pcmd}");
1409 } else {
1410 eprintln!(
1411 " {:<12} {}",
1412 pcmd,
1413 pdesc.dimmed()
1414 );
1415 }
1416 }
1417 eprintln!();
1418 }
1419
1420 err.exit();
1421 }
1422 Err(e) => Err(e)
1423 }
1424 }
1425 };
1426
1427 unsafe {
1430 std::env::remove_var("ZOI_CRASH_COMMAND");
1431 }
1432
1433 match &result {
1434 Ok(()) => {
1435 if !is_flush_command {
1442 let _ = zoi_telemetry::posthog_capture_dau_ping();
1443 }
1444 }
1445 Err(e) => {
1446 zoi_telemetry::sentry_capture_error(command_name, e.as_ref());
1447 }
1448 }
1449
1450 if let Err(e) = result {
1451 eprintln!("Error: {e}");
1452 zoi_telemetry::shutdown_sentry();
1458 std::process::exit(1);
1459 }
1460 }
1461 Ok(())
1462}