1use crate::cmd;
2use crate::pkg::lock;
3use crate::utils;
4use clap::{
5 ColorChoice, CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint, builder::styling,
6};
7use clap_complete::Shell;
8use clap_complete::generate;
9use std::io::{self};
10
11const BRANCH: &str = "Production";
13const STATUS: &str = "Release";
14const NUMBER: &str = "1.4.0";
15const PKG_SOURCE_HELP: &str = "Package identifier (e.g. @repo/name, path, or URL)";
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(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
47pub enum SetupScope {
48 User,
49 System,
50}
51
52#[derive(clap::ValueEnum, Clone, Debug, Copy)]
53pub enum InstallScope {
54 User,
55 System,
56 Project,
57}
58
59#[derive(Subcommand)]
60enum Commands {
61 #[command(hide = true)]
63 GenerateCompletions {
64 #[arg(value_enum)]
66 shell: Shell,
67 },
68
69 #[command(hide = true)]
71 GenerateManual,
72
73 #[command(
75 alias = "v",
76 long_about = "Displays the version number, build status, branch, and commit hash. This is the same output provided by the -v and --version flags."
77 )]
78 Version,
79
80 #[command(
82 long_about = "Displays the full application name, description, author, license, and homepage information."
83 )]
84 About,
85
86 #[command(
88 long_about = "Detects and displays key system details, including the OS, CPU architecture, Linux distribution (if applicable), and available package managers."
89 )]
90 Info,
91
92 #[command(
94 long_about = "Verifies that all required dependencies (like git) are installed and available in the system's PATH. This is useful for diagnostics."
95 )]
96 Check,
97
98 #[command(
100 alias = "sy",
101 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."
102 )]
103 Sync {
104 #[command(subcommand)]
105 command: Option<SyncCommands>,
106
107 #[arg(short, long)]
109 verbose: bool,
110
111 #[arg(long)]
113 fallback: bool,
114
115 #[arg(long = "no-pm")]
117 no_package_managers: bool,
118
119 #[arg(long)]
121 no_shell_setup: bool,
122 },
123
124 #[command(alias = "ls")]
126 List {
127 #[arg(short, long)]
129 all: bool,
130 #[arg(long)]
132 repo: Option<String>,
133 #[arg(short = 't', long = "type")]
135 package_type: Option<String>,
136 },
137
138 Show {
140 #[arg(help = PKG_SOURCE_HELP)]
141 package_name: String,
142 #[arg(long)]
144 raw: bool,
145 },
146
147 Pin {
149 #[arg(help = PKG_SOURCE_HELP)]
150 package: String,
151 version: String,
153 },
154
155 Unpin {
157 #[arg(help = PKG_SOURCE_HELP)]
158 package: String,
159 },
160
161 #[command(alias = "up")]
163 Update {
164 #[arg(value_name = "PACKAGES", help = PKG_SOURCE_HELP)]
165 package_names: Vec<String>,
166
167 #[arg(long, conflicts_with = "package_names")]
169 all: bool,
170 },
171
172 #[command(alias = "i")]
174 Install {
175 #[arg(value_name = "SOURCES", value_hint = ValueHint::FilePath, help = PKG_SOURCE_HELP)]
176 sources: Vec<String>,
177 #[arg(long, value_name = "REPO", conflicts_with = "sources")]
179 repo: Option<String>,
180 #[arg(long)]
182 force: bool,
183 #[arg(long)]
185 all_optional: bool,
186 #[arg(long, value_enum, conflicts_with_all = &["local", "global"])]
188 scope: Option<InstallScope>,
189 #[arg(long, conflicts_with = "global")]
191 local: bool,
192 #[arg(long)]
194 global: bool,
195 #[arg(long)]
197 save: bool,
198 #[arg(long)]
200 r#type: Option<String>,
201 },
202
203 #[command(
205 aliases = ["un", "rm", "remove"],
206 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."
207 )]
208 Uninstall {
209 #[arg(value_name = "PACKAGES", required = true, help = PKG_SOURCE_HELP)]
210 packages: Vec<String>,
211 #[arg(long, value_enum, conflicts_with_all = &["local", "global"])]
213 scope: Option<InstallScope>,
214 #[arg(long, conflicts_with = "global")]
216 local: bool,
217 #[arg(long)]
219 global: bool,
220 #[arg(long)]
222 save: bool,
223 },
224
225 #[command(
227 long_about = "Execute a command from zoi.yaml. If no command is specified, it will launch an interactive prompt to choose one."
228 )]
229 Run {
230 cmd_alias: Option<String>,
232 args: Vec<String>,
234 },
235
236 #[command(
238 long_about = "Checks for required packages and runs setup commands for a defined environment. If no environment is specified, it launches an interactive prompt."
239 )]
240 Env {
241 env_alias: Option<String>,
243 },
244
245 #[command(
247 alias = "ug",
248 long_about = "Downloads the latest release from GitLab, verifies its checksum, and replaces the current executable."
249 )]
250 Upgrade {
251 #[arg(long)]
253 force: bool,
254
255 #[arg(long)]
257 tag: Option<String>,
258
259 #[arg(long)]
261 branch: Option<String>,
262 },
263
264 Autoremove,
266
267 Why {
269 #[arg(help = PKG_SOURCE_HELP)]
270 package_name: String,
271 },
272
273 #[command(alias = "owns")]
275 Owner {
276 #[arg(value_hint = ValueHint::FilePath)]
278 path: std::path::PathBuf,
279 },
280
281 Files {
283 #[arg(help = PKG_SOURCE_HELP)]
284 package: String,
285 },
286
287 #[command(
289 alias = "s",
290 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."
291 )]
292 Search {
293 search_term: String,
295 #[arg(long)]
297 repo: Option<String>,
298 #[arg(long = "type")]
300 package_type: Option<String>,
301 #[arg(short = 't', long = "tag", value_delimiter = ',', num_args = 1..)]
303 tags: Option<Vec<String>>,
304 },
305
306 #[command(
308 long_about = "Installs completion scripts for a given shell and adds the Zoi binary directory to your shell's PATH."
309 )]
310 Shell {
311 #[arg(value_enum)]
313 shell: Shell,
314 #[arg(long, value_enum, default_value = "user")]
316 scope: SetupScope,
317 },
318
319 #[command(
321 alias = "x",
322 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."
323 )]
324 Exec {
325 #[arg(value_name = "SOURCE", value_hint = ValueHint::FilePath, help = PKG_SOURCE_HELP)]
326 source: String,
327
328 #[arg(long)]
330 upstream: bool,
331
332 #[arg(long)]
334 cache: bool,
335
336 #[arg(long)]
338 local: bool,
339
340 #[arg(value_name = "ARGS")]
342 args: Vec<String>,
343 },
344
345 Clean,
347
348 #[command(
350 aliases = ["repositories"],
351 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>)."
352 )]
353 Repo(cmd::repo::RepoCommand),
354
355 #[command(
357 long_about = "Manage opt-in anonymous telemetry used to understand package popularity. Default is disabled."
358 )]
359 Telemetry {
360 #[arg(value_enum)]
361 action: TelemetryAction,
362 },
363
364 Create {
366 #[arg(help = PKG_SOURCE_HELP)]
367 source: String,
368 app_name: Option<String>,
370 },
371
372 #[command(alias = "ext")]
374 Extension(ExtensionCommand),
375
376 Rollback {
378 #[arg(value_name = "PACKAGE", required_unless_present = "last_transaction", help = PKG_SOURCE_HELP)]
379 package: Option<String>,
380
381 #[arg(long, conflicts_with = "package")]
383 last_transaction: bool,
384 },
385
386 Man {
388 #[arg(help = PKG_SOURCE_HELP)]
389 package_name: String,
390 #[arg(long)]
392 upstream: bool,
393 #[arg(long)]
395 raw: bool,
396 },
397
398 #[command(alias = "pkg")]
400 Package(cmd::package::PackageCommand),
401
402 Pgp(cmd::pgp::PgpCommand),
404
405 Helper(cmd::helper::HelperCommand),
407
408 Doctor,
410}
411
412#[derive(clap::Parser, Debug)]
413pub struct ExtensionCommand {
414 #[command(subcommand)]
415 pub command: ExtensionCommands,
416}
417
418#[derive(clap::Subcommand, Debug)]
419pub enum ExtensionCommands {
420 Add {
422 #[arg(required = true)]
424 name: String,
425 },
426 Remove {
428 #[arg(required = true)]
430 name: String,
431 },
432}
433
434#[derive(clap::Subcommand, Clone)]
435pub enum SyncCommands {
436 Add {
438 url: String,
440 },
441 Remove {
443 handle: String,
445 },
446 #[command(alias = "ls")]
448 List,
449 Set {
451 url: String,
453 },
454}
455
456#[derive(clap::ValueEnum, Clone)]
457enum TelemetryAction {
458 Status,
459 Enable,
460 Disable,
461}
462
463pub fn run() -> anyhow::Result<()> {
464 let styles = styling::Styles::styled()
465 .header(styling::AnsiColor::Yellow.on_default() | styling::Effects::BOLD)
466 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
467 .literal(styling::AnsiColor::Green.on_default())
468 .placeholder(styling::AnsiColor::Cyan.on_default());
469
470 let commit: &str = option_env!("ZOI_COMMIT_HASH").unwrap_or("dev");
471 let mut cmd = Cli::command().styles(styles);
472 let matches = cmd.clone().get_matches();
473 let cli = match Cli::from_arg_matches(&matches) {
474 Ok(cli) => cli,
475 Err(err) => {
476 err.print().unwrap();
477 return Err(anyhow::anyhow!("Failed to parse arguments"));
478 }
479 };
480
481 utils::check_path();
482
483 if cli.version_flag {
484 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
485 return Ok(());
486 }
487
488 if let Some(command) = cli.command {
489 let needs_lock = matches!(
490 command,
491 Commands::Install { .. }
492 | Commands::Uninstall { .. }
493 | Commands::Update { .. }
494 | Commands::Autoremove
495 | Commands::Rollback { .. }
496 | Commands::Package(_)
497 );
498
499 let _lock_guard = if needs_lock {
500 Some(lock::acquire_lock()?)
501 } else {
502 None
503 };
504
505 let result = match command {
506 Commands::GenerateCompletions { shell } => {
507 let mut cmd = Cli::command();
508 let bin_name = cmd.get_name().to_string();
509 generate(shell, &mut cmd, bin_name, &mut io::stdout());
510 Ok(())
511 }
512 Commands::GenerateManual => cmd::gen_man::run().map_err(Into::into),
513 Commands::Version => {
514 cmd::version::run(BRANCH, STATUS, NUMBER, commit);
515 Ok(())
516 }
517 Commands::About => {
518 cmd::about::run(BRANCH, STATUS, NUMBER, commit);
519 Ok(())
520 }
521 Commands::Info => cmd::info::run(BRANCH, STATUS, NUMBER, commit),
522 Commands::Check => cmd::check::run(),
523 Commands::Sync {
524 command,
525 verbose,
526 fallback,
527 no_package_managers,
528 no_shell_setup,
529 } => {
530 if let Some(cmd) = command {
531 match cmd {
532 SyncCommands::Add { url } => cmd::sync::add_registry(&url),
533 SyncCommands::Remove { handle } => cmd::sync::remove_registry(&handle),
534 SyncCommands::List => cmd::sync::list_registries(),
535 SyncCommands::Set { url } => cmd::sync::set_registry(&url),
536 }
537 } else {
538 cmd::sync::run(verbose, fallback, no_package_managers, no_shell_setup)
539 }
540 }
541 Commands::List {
542 all,
543 repo,
544 package_type,
545 } => cmd::list::run(all, repo, package_type),
546 Commands::Show { package_name, raw } => cmd::show::run(&package_name, raw),
547 Commands::Pin { package, version } => cmd::pin::run(&package, &version),
548 Commands::Unpin { package } => cmd::unpin::run(&package),
549 Commands::Update { package_names, all } => {
550 if !all && package_names.is_empty() {
551 let mut cmd = Cli::command();
552 if let Some(subcmd) = cmd.find_subcommand_mut("update") {
553 subcmd.print_help().unwrap();
554 }
555 Ok(())
556 } else {
557 cmd::update::run(all, &package_names, cli.yes)
558 }
559 }
560 Commands::Install {
561 sources,
562 repo,
563 force,
564 all_optional,
565 scope,
566 local,
567 global,
568 save,
569 r#type,
570 } => cmd::install::run(
571 &sources,
572 repo,
573 force,
574 all_optional,
575 cli.yes,
576 scope,
577 local,
578 global,
579 save,
580 r#type,
581 ),
582 Commands::Uninstall {
583 packages,
584 scope,
585 local,
586 global,
587 save,
588 } => cmd::uninstall::run(&packages, scope, local, global, save, cli.yes),
589 Commands::Run { cmd_alias, args } => cmd::run::run(cmd_alias, args),
590 Commands::Env { env_alias } => cmd::env::run(env_alias),
591 Commands::Upgrade { force, tag, branch } => {
592 cmd::upgrade::run(BRANCH, STATUS, NUMBER, force, tag, branch)
593 }
594 Commands::Autoremove => cmd::autoremove::run(cli.yes),
595 Commands::Why { package_name } => cmd::why::run(&package_name),
596 Commands::Owner { path } => cmd::owner::run(&path),
597 Commands::Files { package } => cmd::files::run(&package),
598 Commands::Search {
599 search_term,
600 repo,
601 package_type,
602 tags,
603 } => cmd::search::run(search_term, repo, package_type, tags),
604 Commands::Shell { shell, scope } => cmd::shell::run(shell, scope),
605 Commands::Exec {
606 source,
607 upstream,
608 cache,
609 local,
610 args,
611 } => match cmd::exec::run(source, args, upstream, cache, local) {
612 Ok(0) => Ok(()),
613 Ok(exit_code) => Err(anyhow::anyhow!("process exited with code {}", exit_code)),
614 Err(e) => Err(e),
615 },
616 Commands::Clean => cmd::clean::run(),
617 Commands::Repo(args) => cmd::repo::run(args),
618 Commands::Telemetry { action } => {
619 use cmd::telemetry::{TelemetryCommand, run};
620 let cmd = match action {
621 TelemetryAction::Status => TelemetryCommand::Status,
622 TelemetryAction::Enable => TelemetryCommand::Enable,
623 TelemetryAction::Disable => TelemetryCommand::Disable,
624 };
625 run(cmd)
626 }
627 Commands::Create { source, app_name } => {
628 cmd::create::run(cmd::create::CreateCommand { source, app_name }, cli.yes)
629 }
630 Commands::Extension(args) => cmd::extension::run(args, cli.yes),
631 Commands::Rollback {
632 package,
633 last_transaction,
634 } => {
635 if last_transaction {
636 cmd::rollback::run_transaction_rollback(cli.yes)
637 } else if let Some(pkg) = package {
638 cmd::rollback::run(&pkg, cli.yes)
639 } else {
640 Ok(())
641 }
642 }
643 Commands::Man {
644 package_name,
645 upstream,
646 raw,
647 } => cmd::man::run(&package_name, upstream, raw),
648 Commands::Package(args) => cmd::package::run(args),
649 Commands::Pgp(args) => cmd::pgp::run(args),
650 Commands::Helper(args) => cmd::helper::run(args),
651 Commands::Doctor => cmd::doctor::run(),
652 };
653
654 if let Err(e) = result {
655 eprintln!("Error: {}", e);
656 std::process::exit(1);
657 }
658 } else {
659 cmd.print_help().unwrap();
660 }
661 Ok(())
662}