1use crate::cmd;
2use crate::pkg::lock;
3use crate::utils;
4use clap::{
5 ColorChoice, CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint,
6 builder::PossibleValue, builder::TypedValueParser, builder::styling,
7};
8use clap_complete::Shell;
9use clap_complete::generate;
10use std::io::{self};
11
12const BRANCH: &str = "Production";
14const STATUS: &str = "Release";
15const NUMBER: &str = "1.0.0";
16
17#[derive(Parser)]
22#[command(name = "zoi", author, about, long_about = None, disable_version_flag = true,
23 trailing_var_arg = true,
24 color = ColorChoice::Auto,
25)]
26pub struct Cli {
27 #[command(subcommand)]
28 command: Option<Commands>,
29
30 #[arg(
31 short = 'v',
32 long = "version",
33 help = "Print detailed version information"
34 )]
35 version_flag: bool,
36
37 #[arg(
38 short = 'y',
39 long,
40 help = "Automatically answer yes to all prompts",
41 global = true
42 )]
43 yes: bool,
44}
45
46#[derive(Clone, Debug)]
47struct PackageValueParser;
48
49impl TypedValueParser for PackageValueParser {
50 type Value = String;
51
52 fn parse_ref(
53 &self,
54 _cmd: &clap::Command,
55 _arg: Option<&clap::Arg>,
56 value: &std::ffi::OsStr,
57 ) -> Result<Self::Value, clap::Error> {
58 Ok(value.to_string_lossy().into_owned())
59 }
60
61 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
62 Some(Box::new(
63 utils::get_all_packages_for_completion()
64 .into_iter()
65 .map(|pkg| {
66 let help = if pkg.description.is_empty() {
67 pkg.repo
68 } else {
69 format!("[{}] {}", pkg.repo, pkg.description)
70 };
71 PossibleValue::new(Box::leak(pkg.display.into_boxed_str()) as &'static str)
72 .help(Box::leak(help.into_boxed_str()) as &'static str)
73 }),
74 ))
75 }
76}
77
78#[derive(Clone, Debug)]
79struct PkgOrPathParser;
80
81impl TypedValueParser for PkgOrPathParser {
82 type Value = String;
83
84 fn parse_ref(
85 &self,
86 _cmd: &clap::Command,
87 _arg: Option<&clap::Arg>,
88 value: &std::ffi::OsStr,
89 ) -> Result<Self::Value, clap::Error> {
90 Ok(value.to_string_lossy().into_owned())
91 }
92
93 fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
94 Some(Box::new(
95 utils::get_all_packages_for_completion()
96 .into_iter()
97 .map(|pkg| {
98 let help = if pkg.description.is_empty() {
99 pkg.repo
100 } else {
101 format!("[{}] {}", pkg.repo, pkg.description)
102 };
103 PossibleValue::new(Box::leak(pkg.display.into_boxed_str()) as &'static str)
104 .help(Box::leak(help.into_boxed_str()) as &'static str)
105 }),
106 ))
107 }
108}
109
110#[derive(clap::ValueEnum, Clone, Debug, Copy)]
111pub enum SetupScope {
112 User,
113 System,
114}
115
116#[derive(clap::ValueEnum, Clone, Debug, Copy)]
117pub enum InstallScope {
118 User,
119 System,
120 Project,
121}
122
123#[derive(Subcommand)]
124enum Commands {
125 #[command(hide = true)]
127 GenerateCompletions {
128 #[arg(value_enum)]
130 shell: Shell,
131 },
132
133 #[command(hide = true)]
135 GenerateManual,
136
137 #[command(
139 alias = "v",
140 long_about = "Displays the version number, build status, branch, and commit hash. This is the same output provided by the -v and --version flags."
141 )]
142 Version,
143
144 #[command(
146 long_about = "Displays the full application name, description, author, license, and homepage information."
147 )]
148 About,
149
150 #[command(
152 long_about = "Detects and displays key system details, including the OS, CPU architecture, Linux distribution (if applicable), and available package managers."
153 )]
154 Info,
155
156 #[command(
158 long_about = "Verifies that all required dependencies (like git) are installed and available in the system's PATH. This is useful for diagnostics."
159 )]
160 Check,
161
162 #[command(
164 alias = "sy",
165 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."
166 )]
167 Sync {
168 #[command(subcommand)]
169 command: Option<SyncCommands>,
170
171 #[arg(short, long)]
173 verbose: bool,
174
175 #[arg(long)]
177 fallback: bool,
178
179 #[arg(long = "no-pm")]
181 no_package_managers: bool,
182
183 #[arg(long)]
185 no_shell_setup: bool,
186 },
187
188 #[command(alias = "ls")]
190 List {
191 #[arg(short, long)]
193 all: bool,
194 #[arg(long)]
196 repo: Option<String>,
197 #[arg(short = 't', long = "type")]
199 package_type: Option<String>,
200 },
201
202 Show {
204 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
206 package_name: String,
207 #[arg(long)]
209 raw: bool,
210 },
211
212 Pin {
214 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
216 package: String,
217 version: String,
219 },
220
221 Unpin {
223 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
225 package: String,
226 },
227
228 #[command(alias = "up")]
230 Update {
231 #[arg(value_name = "PACKAGES", value_parser = PackageValueParser, hide_possible_values = true)]
233 package_names: Vec<String>,
234
235 #[arg(long, conflicts_with = "package_names")]
237 all: bool,
238 },
239
240 #[command(alias = "i")]
242 Install {
243 #[arg(value_name = "SOURCES", value_hint = ValueHint::FilePath, value_parser = PkgOrPathParser, hide_possible_values = true)]
245 sources: Vec<String>,
246 #[arg(long, value_name = "REPO", conflicts_with = "sources")]
248 repo: Option<String>,
249 #[arg(long)]
251 force: bool,
252 #[arg(long)]
254 all_optional: bool,
255 #[arg(long, value_enum, conflicts_with_all = &["local", "global"])]
257 scope: Option<InstallScope>,
258 #[arg(long, conflicts_with = "global")]
260 local: bool,
261 #[arg(long)]
263 global: bool,
264 #[arg(long)]
266 save: bool,
267 },
268
269 #[command(
271 aliases = ["un", "rm", "remove"],
272 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."
273 )]
274 Uninstall {
275 #[arg(value_name = "PACKAGES", required = true, value_parser = PackageValueParser, hide_possible_values = true)]
277 packages: Vec<String>,
278 #[arg(long, value_enum, conflicts_with_all = &["local", "global"])]
280 scope: Option<InstallScope>,
281 #[arg(long, conflicts_with = "global")]
283 local: bool,
284 #[arg(long)]
286 global: bool,
287 },
288
289 #[command(
291 long_about = "Execute a command from zoi.yaml. If no command is specified, it will launch an interactive prompt to choose one."
292 )]
293 Run {
294 cmd_alias: Option<String>,
296 args: Vec<String>,
298 },
299
300 #[command(
302 long_about = "Checks for required packages and runs setup commands for a defined environment. If no environment is specified, it launches an interactive prompt."
303 )]
304 Env {
305 env_alias: Option<String>,
307 },
308
309 #[command(
311 alias = "ug",
312 long_about = "Downloads the latest release from GitLab, verifies its checksum, and replaces the current executable."
313 )]
314 Upgrade {
315 #[arg(long)]
317 force: bool,
318
319 #[arg(long)]
321 tag: Option<String>,
322
323 #[arg(long)]
325 branch: Option<String>,
326 },
327
328 Autoremove,
330
331 Why {
333 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
335 package_name: String,
336 },
337
338 #[command(alias = "owns")]
340 Owner {
341 #[arg(value_hint = ValueHint::FilePath)]
343 path: std::path::PathBuf,
344 },
345
346 Files {
348 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
350 package: String,
351 },
352
353 #[command(
355 alias = "s",
356 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."
357 )]
358 Search {
359 search_term: String,
361 #[arg(long)]
363 repo: Option<String>,
364 #[arg(long = "type")]
366 package_type: Option<String>,
367 #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
369 tags: Option<Vec<String>>,
370 },
371
372 Shell {
374 #[arg(value_enum)]
376 shell: Shell,
377 },
378
379 #[command(
381 long_about = "Adds the Zoi binary directory to your shell's PATH to make Zoi packages' executables available as commands."
382 )]
383 Setup {
384 #[arg(long, value_enum, default_value = "user")]
386 scope: SetupScope,
387 },
388
389 #[command(
391 alias = "x",
392 long_about = "Downloads a binary to a temporary cache and executes it in a shell. All arguments after the package name are passed as arguments to the shell command."
393 )]
394 Exec {
395 #[arg(value_name = "SOURCE", value_parser = PkgOrPathParser, value_hint = ValueHint::FilePath, hide_possible_values = true)]
397 source: String,
398
399 #[arg(long)]
401 upstream: bool,
402
403 #[arg(long)]
405 cache: bool,
406
407 #[arg(long)]
409 local: bool,
410
411 #[arg(value_name = "ARGS")]
413 args: Vec<String>,
414 },
415
416 Clean,
418
419 #[command(
421 aliases = ["repositories"],
422 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>)."
423 )]
424 Repo(cmd::repo::RepoCommand),
425
426 #[command(
428 long_about = "Manage opt-in anonymous telemetry used to understand package popularity. Default is disabled."
429 )]
430 Telemetry {
431 #[arg(value_enum)]
432 action: TelemetryAction,
433 },
434
435 Create {
437 source: String,
439 app_name: Option<String>,
441 },
442
443 #[command(alias = "ext")]
445 Extension(ExtensionCommand),
446
447 Rollback {
449 #[arg(value_name = "PACKAGE", value_parser = PackageValueParser, hide_possible_values = true, required_unless_present = "last_transaction")]
451 package: Option<String>,
452
453 #[arg(long, conflicts_with = "package")]
455 last_transaction: bool,
456 },
457
458 Man {
460 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
462 package_name: String,
463 #[arg(long)]
465 upstream: bool,
466 #[arg(long)]
468 raw: bool,
469 },
470
471 #[command(alias = "pkg")]
473 Package(cmd::package::PackageCommand),
474
475 Pgp(cmd::pgp::PgpCommand),
477
478 Helper(cmd::helper::HelperCommand),
480}
481
482#[derive(clap::Parser, Debug)]
483pub struct ExtensionCommand {
484 #[command(subcommand)]
485 pub command: ExtensionCommands,
486}
487
488#[derive(clap::Subcommand, Debug)]
489pub enum ExtensionCommands {
490 Add {
492 #[arg(required = true)]
494 name: String,
495 },
496 Remove {
498 #[arg(required = true)]
500 name: String,
501 },
502}
503
504#[derive(clap::Subcommand, Clone)]
505pub enum SyncCommands {
506 Add {
508 url: String,
510 },
511 Remove {
513 handle: String,
515 },
516 #[command(alias = "ls")]
518 List,
519 Set {
521 url: String,
523 },
524}
525
526#[derive(clap::ValueEnum, Clone)]
527enum TelemetryAction {
528 Status,
529 Enable,
530 Disable,
531}
532
533pub fn run() {
534 let styles = styling::Styles::styled()
535 .header(styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD)
536 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
537 .literal(styling::AnsiColor::Green.on_default())
538 .placeholder(styling::AnsiColor::Cyan.on_default());
539
540 let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
541 let mut cmd = Cli::command().styles(styles);
542 let matches = cmd.clone().get_matches();
543 let cli = match Cli::from_arg_matches(&matches) {
544 Ok(cli) => cli,
545 Err(err) => {
546 err.print().unwrap();
547 std::process::exit(1);
548 }
549 };
550
551 utils::check_path();
552
553 if cli.version_flag {
554 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
555 return;
556 }
557
558 if let Some(command) = cli.command {
559 let needs_lock = matches!(
560 command,
561 Commands::Install { .. }
562 | Commands::Uninstall { .. }
563 | Commands::Update { .. }
564 | Commands::Autoremove
565 | Commands::Rollback { .. }
566 | Commands::Package(_)
567 );
568
569 let _lock_guard = if needs_lock {
570 match lock::acquire_lock() {
571 Ok(guard) => Some(guard),
572 Err(e) => {
573 eprintln!("Error: {}", e);
574 std::process::exit(1);
575 }
576 }
577 } else {
578 None
579 };
580
581 let result = match command {
582 Commands::GenerateCompletions { shell } => {
583 let mut cmd = Cli::command();
584 let bin_name = cmd.get_name().to_string();
585 generate(shell, &mut cmd, bin_name, &mut io::stdout());
586 Ok(())
587 }
588 Commands::GenerateManual => cmd::gen_man::run().map_err(Into::into),
589 Commands::Version => {
590 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
591 Ok(())
592 }
593 Commands::About => {
594 cmd::about::run(BRANCH, STATUS, NUMBER, commit);
595 Ok(())
596 }
597 Commands::Info => {
598 cmd::info::run(BRANCH, STATUS, NUMBER, commit);
599 Ok(())
600 }
601 Commands::Check => {
602 cmd::check::run();
603 Ok(())
604 }
605 Commands::Sync {
606 command,
607 verbose,
608 fallback,
609 no_package_managers,
610 no_shell_setup,
611 } => {
612 if let Some(cmd) = command {
613 match cmd {
614 SyncCommands::Add { url } => cmd::sync::add_registry(&url),
615 SyncCommands::Remove { handle } => cmd::sync::remove_registry(&handle),
616 SyncCommands::List => cmd::sync::list_registries(),
617 SyncCommands::Set { url } => cmd::sync::set_registry(&url),
618 }
619 } else {
620 cmd::sync::run(verbose, fallback, no_package_managers, no_shell_setup);
621 }
622 Ok(())
623 }
624 Commands::List {
625 all,
626 repo,
627 package_type,
628 } => {
629 let _ = cmd::list::run(all, repo, package_type);
630 Ok(())
631 }
632 Commands::Show { package_name, raw } => {
633 cmd::show::run(&package_name, raw);
634 Ok(())
635 }
636 Commands::Pin { package, version } => {
637 cmd::pin::run(&package, &version);
638 Ok(())
639 }
640 Commands::Unpin { package } => {
641 cmd::unpin::run(&package);
642 Ok(())
643 }
644 Commands::Update { package_names, all } => {
645 if !all && package_names.is_empty() {
646 let mut cmd = Cli::command();
647 if let Some(subcmd) = cmd.find_subcommand_mut("update") {
648 subcmd.print_help().unwrap();
649 }
650 } else {
651 cmd::update::run(all, &package_names, cli.yes);
652 }
653 Ok(())
654 }
655 Commands::Install {
656 sources,
657 repo,
658 force,
659 all_optional,
660 scope,
661 local,
662 global,
663 save,
664 } => {
665 cmd::install::run(
666 &sources,
667 repo,
668 force,
669 all_optional,
670 cli.yes,
671 scope,
672 local,
673 global,
674 save,
675 );
676 Ok(())
677 }
678 Commands::Uninstall {
679 packages,
680 scope,
681 local,
682 global,
683 } => {
684 cmd::uninstall::run(&packages, scope, local, global);
685 Ok(())
686 }
687 Commands::Run { cmd_alias, args } => {
688 cmd::run::run(cmd_alias, args);
689 Ok(())
690 }
691 Commands::Env { env_alias } => {
692 cmd::env::run(env_alias);
693 Ok(())
694 }
695 Commands::Upgrade { force, tag, branch } => {
696 cmd::upgrade::run(BRANCH, STATUS, NUMBER, force, tag, branch);
697 Ok(())
698 }
699 Commands::Autoremove => {
700 cmd::autoremove::run(cli.yes);
701 Ok(())
702 }
703 Commands::Why { package_name } => cmd::why::run(&package_name),
704 Commands::Owner { path } => {
705 cmd::owner::run(&path);
706 Ok(())
707 }
708 Commands::Files { package } => {
709 cmd::files::run(&package);
710 Ok(())
711 }
712 Commands::Search {
713 search_term,
714 repo,
715 package_type,
716 tags,
717 } => cmd::search::run(search_term, repo, package_type, tags),
718 Commands::Shell { shell } => {
719 cmd::shell::run(shell);
720 Ok(())
721 }
722 Commands::Setup { scope } => {
723 cmd::setup::run(scope);
724 Ok(())
725 }
726 Commands::Exec {
727 source,
728 upstream,
729 cache,
730 local,
731 args,
732 } => {
733 cmd::exec::run(source, args, upstream, cache, local);
734 Ok(())
735 }
736 Commands::Clean => {
737 cmd::clean::run();
738 Ok(())
739 }
740 Commands::Repo(args) => {
741 cmd::repo::run(args);
742 Ok(())
743 }
744 Commands::Telemetry { action } => {
745 use cmd::telemetry::{TelemetryCommand, run};
746 let cmd = match action {
747 TelemetryAction::Status => TelemetryCommand::Status,
748 TelemetryAction::Enable => TelemetryCommand::Enable,
749 TelemetryAction::Disable => TelemetryCommand::Disable,
750 };
751 run(cmd);
752 Ok(())
753 }
754 Commands::Create { source, app_name } => {
755 cmd::create::run(cmd::create::CreateCommand { source, app_name }, cli.yes);
756 Ok(())
757 }
758 Commands::Extension(args) => cmd::extension::run(args, cli.yes),
759 Commands::Rollback {
760 package,
761 last_transaction,
762 } => {
763 if last_transaction {
764 cmd::rollback::run_transaction_rollback(cli.yes)
765 } else if let Some(pkg) = package {
766 cmd::rollback::run(&pkg, cli.yes)
767 } else {
768 Ok(())
769 }
770 }
771 Commands::Man {
772 package_name,
773 upstream,
774 raw,
775 } => cmd::man::run(&package_name, upstream, raw),
776 Commands::Package(args) => {
777 cmd::package::run(args);
778 Ok(())
779 }
780 Commands::Pgp(args) => cmd::pgp::run(args),
781 Commands::Helper(args) => cmd::helper::run(args),
782 };
783
784 if let Err(e) = result {
785 eprintln!("Error: {}", e);
786 std::process::exit(1);
787 }
788 } else {
789 cmd.print_help().unwrap();
790 }
791}