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.2.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 #[arg(long)]
269 r#type: Option<String>,
270 },
271
272 #[command(
274 aliases = ["un", "rm", "remove"],
275 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."
276 )]
277 Uninstall {
278 #[arg(value_name = "PACKAGES", required = true, value_parser = PackageValueParser, hide_possible_values = true)]
280 packages: Vec<String>,
281 #[arg(long, value_enum, conflicts_with_all = &["local", "global"])]
283 scope: Option<InstallScope>,
284 #[arg(long, conflicts_with = "global")]
286 local: bool,
287 #[arg(long)]
289 global: bool,
290 #[arg(long)]
292 save: bool,
293 },
294
295 #[command(
297 long_about = "Execute a command from zoi.yaml. If no command is specified, it will launch an interactive prompt to choose one."
298 )]
299 Run {
300 cmd_alias: Option<String>,
302 args: Vec<String>,
304 },
305
306 #[command(
308 long_about = "Checks for required packages and runs setup commands for a defined environment. If no environment is specified, it launches an interactive prompt."
309 )]
310 Env {
311 env_alias: Option<String>,
313 },
314
315 #[command(
317 alias = "ug",
318 long_about = "Downloads the latest release from GitLab, verifies its checksum, and replaces the current executable."
319 )]
320 Upgrade {
321 #[arg(long)]
323 force: bool,
324
325 #[arg(long)]
327 tag: Option<String>,
328
329 #[arg(long)]
331 branch: Option<String>,
332 },
333
334 Autoremove,
336
337 Why {
339 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
341 package_name: String,
342 },
343
344 #[command(alias = "owns")]
346 Owner {
347 #[arg(value_hint = ValueHint::FilePath)]
349 path: std::path::PathBuf,
350 },
351
352 Files {
354 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
356 package: String,
357 },
358
359 #[command(
361 alias = "s",
362 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."
363 )]
364 Search {
365 search_term: String,
367 #[arg(long)]
369 repo: Option<String>,
370 #[arg(long = "type")]
372 package_type: Option<String>,
373 #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
375 tags: Option<Vec<String>>,
376 },
377
378 Shell {
380 #[arg(value_enum)]
382 shell: Shell,
383 },
384
385 #[command(
387 long_about = "Adds the Zoi binary directory to your shell's PATH to make Zoi packages' executables available as commands."
388 )]
389 Setup {
390 #[arg(long, value_enum, default_value = "user")]
392 scope: SetupScope,
393 },
394
395 #[command(
397 alias = "x",
398 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."
399 )]
400 Exec {
401 #[arg(value_name = "SOURCE", value_parser = PkgOrPathParser, value_hint = ValueHint::FilePath, hide_possible_values = true)]
403 source: String,
404
405 #[arg(long)]
407 upstream: bool,
408
409 #[arg(long)]
411 cache: bool,
412
413 #[arg(long)]
415 local: bool,
416
417 #[arg(value_name = "ARGS")]
419 args: Vec<String>,
420 },
421
422 Clean,
424
425 #[command(
427 aliases = ["repositories"],
428 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>)."
429 )]
430 Repo(cmd::repo::RepoCommand),
431
432 #[command(
434 long_about = "Manage opt-in anonymous telemetry used to understand package popularity. Default is disabled."
435 )]
436 Telemetry {
437 #[arg(value_enum)]
438 action: TelemetryAction,
439 },
440
441 Create {
443 source: String,
445 app_name: Option<String>,
447 },
448
449 #[command(alias = "ext")]
451 Extension(ExtensionCommand),
452
453 Rollback {
455 #[arg(value_name = "PACKAGE", value_parser = PackageValueParser, hide_possible_values = true, required_unless_present = "last_transaction")]
457 package: Option<String>,
458
459 #[arg(long, conflicts_with = "package")]
461 last_transaction: bool,
462 },
463
464 Man {
466 #[arg(value_parser = PackageValueParser, hide_possible_values = true)]
468 package_name: String,
469 #[arg(long)]
471 upstream: bool,
472 #[arg(long)]
474 raw: bool,
475 },
476
477 #[command(alias = "pkg")]
479 Package(cmd::package::PackageCommand),
480
481 Pgp(cmd::pgp::PgpCommand),
483
484 Helper(cmd::helper::HelperCommand),
486
487 Doctor,
489}
490
491#[derive(clap::Parser, Debug)]
492pub struct ExtensionCommand {
493 #[command(subcommand)]
494 pub command: ExtensionCommands,
495}
496
497#[derive(clap::Subcommand, Debug)]
498pub enum ExtensionCommands {
499 Add {
501 #[arg(required = true)]
503 name: String,
504 },
505 Remove {
507 #[arg(required = true)]
509 name: String,
510 },
511}
512
513#[derive(clap::Subcommand, Clone)]
514pub enum SyncCommands {
515 Add {
517 url: String,
519 },
520 Remove {
522 handle: String,
524 },
525 #[command(alias = "ls")]
527 List,
528 Set {
530 url: String,
532 },
533}
534
535#[derive(clap::ValueEnum, Clone)]
536enum TelemetryAction {
537 Status,
538 Enable,
539 Disable,
540}
541
542pub fn run() {
543 let styles = styling::Styles::styled()
544 .header(styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD)
545 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
546 .literal(styling::AnsiColor::Green.on_default())
547 .placeholder(styling::AnsiColor::Cyan.on_default());
548
549 let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
550 let mut cmd = Cli::command().styles(styles);
551 let matches = cmd.clone().get_matches();
552 let cli = match Cli::from_arg_matches(&matches) {
553 Ok(cli) => cli,
554 Err(err) => {
555 err.print().unwrap();
556 std::process::exit(1);
557 }
558 };
559
560 utils::check_path();
561
562 if cli.version_flag {
563 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
564 return;
565 }
566
567 if let Some(command) = cli.command {
568 let needs_lock = matches!(
569 command,
570 Commands::Install { .. }
571 | Commands::Uninstall { .. }
572 | Commands::Update { .. }
573 | Commands::Autoremove
574 | Commands::Rollback { .. }
575 | Commands::Package(_)
576 );
577
578 let _lock_guard = if needs_lock {
579 match lock::acquire_lock() {
580 Ok(guard) => Some(guard),
581 Err(e) => {
582 eprintln!("Error: {}", e);
583 std::process::exit(1);
584 }
585 }
586 } else {
587 None
588 };
589
590 let result = match command {
591 Commands::GenerateCompletions { shell } => {
592 let mut cmd = Cli::command();
593 let bin_name = cmd.get_name().to_string();
594 generate(shell, &mut cmd, bin_name, &mut io::stdout());
595 Ok(())
596 }
597 Commands::GenerateManual => cmd::gen_man::run().map_err(Into::into),
598 Commands::Version => {
599 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
600 Ok(())
601 }
602 Commands::About => {
603 cmd::about::run(BRANCH, STATUS, NUMBER, commit);
604 Ok(())
605 }
606 Commands::Info => {
607 cmd::info::run(BRANCH, STATUS, NUMBER, commit);
608 Ok(())
609 }
610 Commands::Check => {
611 cmd::check::run();
612 Ok(())
613 }
614 Commands::Sync {
615 command,
616 verbose,
617 fallback,
618 no_package_managers,
619 no_shell_setup,
620 } => {
621 if let Some(cmd) = command {
622 match cmd {
623 SyncCommands::Add { url } => cmd::sync::add_registry(&url),
624 SyncCommands::Remove { handle } => cmd::sync::remove_registry(&handle),
625 SyncCommands::List => cmd::sync::list_registries(),
626 SyncCommands::Set { url } => cmd::sync::set_registry(&url),
627 }
628 } else {
629 cmd::sync::run(verbose, fallback, no_package_managers, no_shell_setup);
630 }
631 Ok(())
632 }
633 Commands::List {
634 all,
635 repo,
636 package_type,
637 } => {
638 let _ = cmd::list::run(all, repo, package_type);
639 Ok(())
640 }
641 Commands::Show { package_name, raw } => {
642 cmd::show::run(&package_name, raw);
643 Ok(())
644 }
645 Commands::Pin { package, version } => {
646 cmd::pin::run(&package, &version);
647 Ok(())
648 }
649 Commands::Unpin { package } => {
650 cmd::unpin::run(&package);
651 Ok(())
652 }
653 Commands::Update { package_names, all } => {
654 if !all && package_names.is_empty() {
655 let mut cmd = Cli::command();
656 if let Some(subcmd) = cmd.find_subcommand_mut("update") {
657 subcmd.print_help().unwrap();
658 }
659 } else {
660 let _ = cmd::update::run(all, &package_names, cli.yes);
661 }
662 Ok(())
663 }
664 Commands::Install {
665 sources,
666 repo,
667 force,
668 all_optional,
669 scope,
670 local,
671 global,
672 save,
673 r#type,
674 } => {
675 cmd::install::run(
676 &sources,
677 repo,
678 force,
679 all_optional,
680 cli.yes,
681 scope,
682 local,
683 global,
684 save,
685 r#type,
686 );
687 Ok(())
688 }
689 Commands::Uninstall {
690 packages,
691 scope,
692 local,
693 global,
694 save,
695 } => {
696 cmd::uninstall::run(&packages, scope, local, global, save);
697 Ok(())
698 }
699 Commands::Run { cmd_alias, args } => {
700 cmd::run::run(cmd_alias, args);
701 Ok(())
702 }
703 Commands::Env { env_alias } => {
704 cmd::env::run(env_alias);
705 Ok(())
706 }
707 Commands::Upgrade { force, tag, branch } => {
708 cmd::upgrade::run(BRANCH, STATUS, NUMBER, force, tag, branch);
709 Ok(())
710 }
711 Commands::Autoremove => {
712 cmd::autoremove::run(cli.yes);
713 Ok(())
714 }
715 Commands::Why { package_name } => cmd::why::run(&package_name),
716 Commands::Owner { path } => {
717 cmd::owner::run(&path);
718 Ok(())
719 }
720 Commands::Files { package } => {
721 cmd::files::run(&package);
722 Ok(())
723 }
724 Commands::Search {
725 search_term,
726 repo,
727 package_type,
728 tags,
729 } => cmd::search::run(search_term, repo, package_type, tags),
730 Commands::Shell { shell } => {
731 cmd::shell::run(shell);
732 Ok(())
733 }
734 Commands::Setup { scope } => {
735 cmd::setup::run(scope);
736 Ok(())
737 }
738 Commands::Exec {
739 source,
740 upstream,
741 cache,
742 local,
743 args,
744 } => {
745 cmd::exec::run(source, args, upstream, cache, local);
746 Ok(())
747 }
748 Commands::Clean => {
749 cmd::clean::run();
750 Ok(())
751 }
752 Commands::Repo(args) => {
753 cmd::repo::run(args);
754 Ok(())
755 }
756 Commands::Telemetry { action } => {
757 use cmd::telemetry::{TelemetryCommand, run};
758 let cmd = match action {
759 TelemetryAction::Status => TelemetryCommand::Status,
760 TelemetryAction::Enable => TelemetryCommand::Enable,
761 TelemetryAction::Disable => TelemetryCommand::Disable,
762 };
763 run(cmd);
764 Ok(())
765 }
766 Commands::Create { source, app_name } => {
767 cmd::create::run(cmd::create::CreateCommand { source, app_name }, cli.yes);
768 Ok(())
769 }
770 Commands::Extension(args) => cmd::extension::run(args, cli.yes),
771 Commands::Rollback {
772 package,
773 last_transaction,
774 } => {
775 if last_transaction {
776 cmd::rollback::run_transaction_rollback(cli.yes)
777 } else if let Some(pkg) = package {
778 cmd::rollback::run(&pkg, cli.yes)
779 } else {
780 Ok(())
781 }
782 }
783 Commands::Man {
784 package_name,
785 upstream,
786 raw,
787 } => cmd::man::run(&package_name, upstream, raw),
788 Commands::Package(args) => {
789 cmd::package::run(args);
790 Ok(())
791 }
792 Commands::Pgp(args) => cmd::pgp::run(args),
793 Commands::Helper(args) => cmd::helper::run(args),
794 Commands::Doctor => cmd::doctor::run(),
795 };
796
797 if let Err(e) = result {
798 eprintln!("Error: {}", e);
799 std::process::exit(1);
800 }
801 } else {
802 cmd.print_help().unwrap();
803 }
804}