Skip to main content

frm/
cli.rs

1// Copyright (c) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use clap::{Arg, ArgAction, Command};
10
11pub use bel7_cli::CompletionShell;
12
13use crate::commands::{CONFIG_FILES, EtcFile};
14use crate::common::cli_tools::RABBITMQ_CLI_TOOLS;
15use crate::shell::Shell;
16
17pub fn build_cli() -> Command {
18    Command::new("frm")
19        .version(env!("CARGO_PKG_VERSION"))
20        .disable_version_flag(true)
21        .author("Michael S. Klishin")
22        .about("Frakking RabbitMQ version Manager")
23        .help_template("{name} {version}\n{about}\n\n{usage-heading} {usage}\n\n{all-args}")
24        .arg_required_else_help(true)
25        .subcommand(status_command())
26        .subcommand(releases_command())
27        .subcommand(alphas_command())
28        .subcommand(tanzu_command())
29        .subcommand(conf_command())
30        .subcommand(default_command())
31        .subcommand(cli_command())
32        .subcommand(fg_command())
33        .subcommand(bg_command())
34        .subcommand(inspect_command())
35        .subcommand(shell_command())
36}
37
38fn status_command() -> Command {
39    Command::new("status")
40        .about("Show frm status: active version, default, installed versions")
41        .long_about(
42            "Show frm status: active version, default, installed versions.\n\n\
43            🟢 active in current shell (via 'frm releases use', 'frm alphas use', or 'frm tanzu use')\n\
44            ⚪ default version",
45        )
46}
47
48fn releases_command() -> Command {
49    Command::new("releases")
50        .about("Install or manage RabbitMQ releases (GA, RCs, betas); for alphas, see the 'alphas' command group")
51        .arg_required_else_help(true)
52        .subcommand(releases_list_command())
53        .subcommand(releases_path_command())
54        .subcommand(releases_logs_command())
55        .subcommand(releases_install_command())
56        .subcommand(releases_reinstall_command())
57        .subcommand(releases_uninstall_command())
58        .subcommand(releases_use_command())
59        .subcommand(releases_cp_etc_file_command())
60        .subcommand(releases_check_signature_command())
61        .subcommand(releases_completions_command())
62}
63
64fn releases_use_command() -> Command {
65    const HELP: &str = "Version to use (e.g., 4.2.3 or 'latest')";
66    Command::new("use")
67        .about("Output shell commands to use a specific release version")
68        .long_about(
69            "Output shell commands to use a specific release version.\n\n\
70            Use 'latest' to select the most recent installed GA version.\n\n\
71            bash/zsh: eval \"$(frm releases use [version])\"\n\
72            nushell:  Use 'frm shell env nu' init script, then call 'frm-use [version]'",
73        )
74        .arg(positional_version_arg(HELP))
75        .arg(version_opt_arg(HELP))
76        .arg(
77            Arg::new("shell")
78                .long("shell")
79                .short('s')
80                .help("Shell type (bash, zsh, nu)")
81                .value_parser(clap::value_parser!(Shell)),
82        )
83}
84
85fn releases_completions_command() -> Command {
86    Command::new("completions")
87        .about("Output installed release versions for shell completion")
88        .hide(true)
89        .arg(
90            Arg::new("shell")
91                .long("shell")
92                .short('s')
93                .help("Shell type (bash, zsh, nu)")
94                .value_parser(clap::value_parser!(Shell)),
95        )
96}
97
98fn releases_list_command() -> Command {
99    Command::new("list")
100        .visible_alias("ls")
101        .about("List installed stable RabbitMQ releases")
102}
103
104fn releases_path_command() -> Command {
105    Command::new("path")
106        .about("Show the local path of an installed release")
107        .arg(version_arg())
108}
109
110fn releases_logs_command() -> Command {
111    Command::new("logs")
112        .about("Show RabbitMQ log file information for a release")
113        .arg_required_else_help(true)
114        .subcommand(
115            Command::new("path")
116                .about("Show the path to the log file")
117                .arg(version_arg()),
118        )
119        .subcommand(
120            Command::new("tail")
121                .about("Show the last lines of the log file")
122                .arg(version_arg())
123                .arg(
124                    Arg::new("lines")
125                        .long("lines")
126                        .short('n')
127                        .help("Number of lines to show")
128                        .default_value("10")
129                        .value_parser(clap::value_parser!(usize)),
130                ),
131        )
132}
133
134fn releases_install_command() -> Command {
135    const HELP: &str = "Version to install (e.g., 4.2.3 or 4.2.0-rc.1)";
136    Command::new("install")
137        .visible_alias("i")
138        .about("Install a stable RabbitMQ release")
139        .long_about(
140            "Install a stable RabbitMQ release (beta, rc, or GA).\n\n\
141            Alpha versions are not allowed; use 'frm alphas install' instead.",
142        )
143        .arg(positional_version_arg(HELP))
144        .arg(version_opt_arg(HELP))
145        .arg(
146            Arg::new("force")
147                .long("force")
148                .short('f')
149                .help("Force reinstallation if version exists")
150                .action(ArgAction::SetTrue),
151        )
152}
153
154fn releases_reinstall_command() -> Command {
155    const HELP: &str = "Version to reinstall (e.g., 4.2.3)";
156    Command::new("reinstall")
157        .about("Reinstall a stable RabbitMQ release")
158        .long_about(
159            "Reinstall a stable RabbitMQ release.\n\n\
160            Removes the existing installation and downloads a fresh copy.",
161        )
162        .arg(positional_version_arg(HELP))
163        .arg(version_opt_arg(HELP))
164}
165
166fn releases_uninstall_command() -> Command {
167    const HELP: &str = "Version to uninstall (e.g., 4.2.3 or 'latest')";
168    Command::new("uninstall")
169        .visible_alias("rm")
170        .about("Uninstall a stable RabbitMQ release")
171        .long_about(
172            "Uninstall a stable RabbitMQ release.\n\n\
173            Use 'latest' to uninstall the most recent installed GA version.",
174        )
175        .arg(positional_version_arg(HELP))
176        .arg(version_opt_arg(HELP))
177}
178
179fn releases_cp_etc_file_command() -> Command {
180    cp_etc_file_command("Copy a configuration file to a stable release's etc/rabbitmq directory")
181}
182
183fn releases_check_signature_command() -> Command {
184    Command::new("check-signature")
185        .about("Verify the GPG signature of an installed release")
186        .long_about(
187            "Verify the GPG signature of an installed release.\n\n\
188            Downloads the signature file from GitHub and verifies it using GPG.\n\
189            Requires gpg to be installed and available in PATH.\n\n\
190            Note: Alpha versions are not signed and cannot be verified.",
191        )
192        .arg(version_arg())
193}
194
195fn alphas_cp_etc_file_command() -> Command {
196    cp_etc_file_command("Copy a configuration file to an alpha release's etc/rabbitmq directory")
197}
198
199fn cp_etc_file_command(about: &'static str) -> Command {
200    Command::new("cp-etc-file")
201        .about(about)
202        .long_about(format!(
203            "{}\n\n\
204            Copies a local file to the version's etc/rabbitmq directory.\n\n\
205            Supported files: {}",
206            about,
207            EtcFile::all_names().join(", ")
208        ))
209        .arg(
210            Arg::new("local_file_path")
211                .long("local-file-path")
212                .help("Path to the local file to copy")
213                .required(true)
214                .value_name("PATH"),
215        )
216        .arg(
217            Arg::new("etc_file")
218                .long("etc-file")
219                .help("Target configuration file name")
220                .required(true)
221                .value_name("FILE")
222                .value_parser(EtcFile::all_names()),
223        )
224        .arg(version_arg())
225}
226
227fn alphas_command() -> Command {
228    Command::new("alphas")
229        .about("Install, manage, rotate alpha RabbitMQ releases")
230        .arg_required_else_help(true)
231        .subcommand(alphas_list_command())
232        .subcommand(alphas_path_command())
233        .subcommand(alphas_logs_command())
234        .subcommand(alphas_install_command())
235        .subcommand(alphas_reinstall_command())
236        .subcommand(alphas_uninstall_command())
237        .subcommand(alphas_use_command())
238        .subcommand(alphas_cp_etc_file_command())
239        .subcommand(alphas_prune_command())
240        .subcommand(alphas_clean_command())
241        .subcommand(alphas_completions_command())
242}
243
244fn alphas_use_command() -> Command {
245    const HELP: &str = "Alpha version to use (e.g., 4.3.0-alpha.132057c7 or 'latest')";
246    Command::new("use")
247        .about("Output shell commands to use a specific alpha version")
248        .long_about(
249            "Output shell commands to use a specific alpha version.\n\n\
250            Use 'latest' to select the most recent installed alpha version.\n\n\
251            bash/zsh: eval \"$(frm alphas use [version])\"\n\
252            nushell:  Use 'frm shell env nu' init script, then call 'frm-use [version]'",
253        )
254        .arg(positional_version_arg(HELP))
255        .arg(version_opt_arg(HELP))
256        .arg(
257            Arg::new("shell")
258                .long("shell")
259                .short('s')
260                .help("Shell type (bash, zsh, nu)")
261                .value_parser(clap::value_parser!(Shell)),
262        )
263}
264
265fn alphas_completions_command() -> Command {
266    Command::new("completions")
267        .about("Output installed alpha versions for shell completion")
268        .hide(true)
269        .arg(
270            Arg::new("shell")
271                .long("shell")
272                .short('s')
273                .help("Shell type (bash, zsh, nu)")
274                .value_parser(clap::value_parser!(Shell)),
275        )
276}
277
278fn alphas_list_command() -> Command {
279    Command::new("list")
280        .visible_alias("ls")
281        .about("List installed alpha RabbitMQ releases")
282}
283
284fn alphas_path_command() -> Command {
285    Command::new("path")
286        .about("Show the local path of an installed alpha release")
287        .arg(version_arg())
288}
289
290fn alphas_logs_command() -> Command {
291    Command::new("logs")
292        .about("Show RabbitMQ log file information for an alpha release")
293        .arg_required_else_help(true)
294        .subcommand(
295            Command::new("path")
296                .about("Show the path to the log file")
297                .arg(version_arg()),
298        )
299        .subcommand(
300            Command::new("tail")
301                .about("Show the last lines of the log file")
302                .arg(version_arg())
303                .arg(
304                    Arg::new("lines")
305                        .long("lines")
306                        .short('n')
307                        .help("Number of lines to show")
308                        .default_value("10")
309                        .value_parser(clap::value_parser!(usize)),
310                ),
311        )
312}
313
314fn alphas_install_command() -> Command {
315    const HELP: &str = "Alpha version to install (e.g., 4.3.0-alpha.132057c7 or 'latest')";
316    Command::new("install")
317        .visible_alias("i")
318        .about("Install an alpha RabbitMQ release")
319        .long_about(
320            "Install an alpha RabbitMQ release from rabbitmq/server-packages.\n\n\
321            Use 'latest' to automatically install the most recent alpha release.",
322        )
323        .arg(positional_version_arg(HELP))
324        .arg(version_opt_arg(HELP))
325        .arg(
326            Arg::new("force")
327                .long("force")
328                .short('f')
329                .help("Force reinstallation if version exists")
330                .action(ArgAction::SetTrue),
331        )
332}
333
334fn alphas_reinstall_command() -> Command {
335    const HELP: &str = "Alpha version to reinstall (e.g., 4.3.0-alpha.132057c7 or 'latest')";
336    Command::new("reinstall")
337        .about("Reinstall an alpha RabbitMQ release")
338        .long_about(
339            "Reinstall an alpha RabbitMQ release.\n\n\
340            Removes the existing installation and downloads a fresh copy.\n\n\
341            Use 'latest' to reinstall the most recent installed alpha version.",
342        )
343        .arg(positional_version_arg(HELP))
344        .arg(version_opt_arg(HELP))
345}
346
347fn alphas_uninstall_command() -> Command {
348    const HELP: &str = "Alpha version to uninstall (e.g., 4.3.0-alpha.132057c7 or 'latest')";
349    Command::new("uninstall")
350        .visible_alias("rm")
351        .about("Uninstall an alpha RabbitMQ release")
352        .long_about(
353            "Uninstall an alpha RabbitMQ release.\n\n\
354            Use 'latest' to uninstall the most recent installed alpha version.",
355        )
356        .arg(positional_version_arg(HELP))
357        .arg(version_opt_arg(HELP))
358}
359
360fn alphas_prune_command() -> Command {
361    Command::new("prune")
362        .about("Remove all installed alpha releases")
363        .long_about("Remove all installed alpha releases to free up disk space.")
364}
365
366fn alphas_clean_command() -> Command {
367    Command::new("clean")
368        .about("Remove alpha releases older than a specified time")
369        .long_about(
370            "Remove alpha releases older than a specified time.\n\n\
371            The --older-than flag accepts human-readable time strings like:\n\
372            - \"2 weeks ago\"\n\
373            - \"1 month ago\"\n\
374            - \"yesterday\"\n\
375            - \"2025-01-01\" (absolute date)",
376        )
377        .arg(
378            Arg::new("older_than")
379                .long("older-than")
380                .help("Remove alphas installed before this time (e.g., \"2 weeks ago\")")
381                .required(true)
382                .value_name("TIME"),
383        )
384}
385
386fn tanzu_command() -> Command {
387    Command::new("tanzu")
388        .about("Install and manage Tanzu RabbitMQ from local tarballs")
389        .arg_required_else_help(true)
390        .subcommand(tanzu_install_command())
391        .subcommand(tanzu_use_command())
392}
393
394fn tanzu_use_command() -> Command {
395    const HELP: &str = "Version to use (e.g., 4.2.3 or 'latest')";
396    Command::new("use")
397        .about("Output shell commands to use a specific Tanzu RabbitMQ version")
398        .long_about(
399            "Output shell commands to use a specific Tanzu RabbitMQ version.\n\n\
400            Use 'latest' to select the most recent installed GA version.\n\n\
401            bash/zsh: eval \"$(frm tanzu use [version])\"\n\
402            nushell:  Use 'frm shell env nu' init script, then call 'frm-use [version]'",
403        )
404        .arg(positional_version_arg(HELP))
405        .arg(version_opt_arg(HELP))
406        .arg(
407            Arg::new("shell")
408                .long("shell")
409                .short('s')
410                .help("Shell type (bash, zsh, nu)")
411                .value_parser(clap::value_parser!(Shell)),
412        )
413}
414
415fn tanzu_install_command() -> Command {
416    Command::new("install")
417        .visible_alias("i")
418        .about("Install Tanzu RabbitMQ from a local tarball")
419        .long_about(
420            "Install Tanzu RabbitMQ from a local tarball.\n\n\
421            Requires both the tarball path and the expected version.\n\
422            The version in the tarball filename must match the specified version.\n\n\
423            Supported formats: .tar.xz, .tar.gz, .tgz",
424        )
425        .arg(
426            Arg::new("tarball_path")
427                .long("local-tanzu-rabbitmq-tarball-path")
428                .help("Path to the local Tanzu RabbitMQ tarball")
429                .required(true)
430                .value_name("PATH"),
431        )
432        .arg(
433            Arg::new("version")
434                .long("version")
435                .short('V')
436                .help("Expected RabbitMQ version (e.g., 4.2.3 or 4.2.3-rc.1)")
437                .required(true)
438                .value_name("VERSION"),
439        )
440        .arg(
441            Arg::new("force")
442                .long("force")
443                .short('f')
444                .help("Force reinstallation if version exists")
445                .action(ArgAction::SetTrue),
446        )
447}
448
449fn conf_command() -> Command {
450    Command::new("conf")
451        .about("Manage RabbitMQ configuration files")
452        .arg_required_else_help(true)
453        .subcommand(conf_get_key_command())
454        .subcommand(conf_set_key_command())
455}
456
457fn conf_get_key_command() -> Command {
458    Command::new("get-key")
459        .about("Get a configuration key value from rabbitmq.conf")
460        .long_about(
461            "Get a configuration key value from rabbitmq.conf.\n\n\
462            Supports pattern matching with * as a wildcard for a single segment:\n\n \
463            * `listeners.tcp.*` matches `listeners.tcp.default`, `listeners.tcp.amqp`, etc.\n \
464            * `log.*.level` matches `log.console.level`, `log.file.level`, etc.",
465        )
466        .arg(
467            Arg::new("key")
468                .help("Configuration key or pattern (e.g., listeners.tcp.* or heartbeat)")
469                .required(true)
470                .index(1),
471        )
472        .arg(version_arg())
473}
474
475fn conf_set_key_command() -> Command {
476    Command::new("set-key")
477        .about("Set a configuration key value in rabbitmq.conf")
478        .long_about(
479            "Set a configuration key value in rabbitmq.conf.\n\n\
480            Keys are validated against the known RabbitMQ configuration schema.\n\
481            Use --force to set unknown keys.",
482        )
483        .arg(
484            Arg::new("key")
485                .help("Configuration key (e.g., listeners.tcp.default)")
486                .required(true)
487                .index(1),
488        )
489        .arg(
490            Arg::new("value")
491                .help("Value to set")
492                .required(true)
493                .index(2),
494        )
495        .arg(version_arg())
496        .arg(
497            Arg::new("force")
498                .long("force")
499                .short('f')
500                .help("Set the key even if it's not recognized")
501                .action(ArgAction::SetTrue),
502        )
503}
504
505fn default_command() -> Command {
506    const HELP: &str = "Version to set as default (e.g., 4.2.3 or 'latest')";
507    Command::new("default")
508        .about("Set the default RabbitMQ version")
509        .long_about(
510            "Set the default RabbitMQ version.\n\n\
511            Use 'latest' to select the most recent installed GA version.",
512        )
513        .arg(positional_version_arg(HELP))
514        .arg(version_opt_arg(HELP))
515}
516
517fn shell_command() -> Command {
518    Command::new("shell")
519        .about("Shell-related operations")
520        .arg_required_else_help(true)
521        .subcommand(shell_completions_command())
522        .subcommand(shell_env_command())
523}
524
525fn shell_env_command() -> Command {
526    Command::new("env")
527        .about("Output shell initialization script")
528        .long_about(
529            "Output shell initialization script.\n\n\
530            Add to your shell profile:\n\
531            - bash: eval \"$(frm shell env bash)\" in ~/.bashrc\n\
532            - zsh: eval \"$(frm shell env zsh)\" in ~/.zshrc\n\
533            - nu: frm shell env nu | save -f ~/.local/frm/env.nu, then source in config.nu\n\n\
534            After setup, use 'frm-use <version>' to switch versions.",
535        )
536        .arg(
537            Arg::new("shell")
538                .help("Shell type (bash, zsh, nu)")
539                .required(true)
540                .index(1)
541                .value_parser(clap::value_parser!(Shell)),
542        )
543}
544
545fn shell_completions_command() -> Command {
546    Command::new("completions")
547        .about("Generate shell completions")
548        .long_about(
549            "Generate shell completions.\n\n\
550            If no shell is specified, attempts to detect the current shell from \
551            environment variables (SHELL, NU_VERSION, etc.).",
552        )
553        .arg(
554            Arg::new("shell")
555                .help("Target shell (bash, elvish, fish, nushell, powershell, zsh); auto-detected if omitted")
556                .index(1)
557                .value_parser(clap::value_parser!(CompletionShell)),
558        )
559}
560
561fn cli_command() -> Command {
562    Command::new("cli")
563        .about("Run a RabbitMQ CLI tool")
564        .long_about(format!(
565            "Run a RabbitMQ CLI tool from the specified version.\n\n\
566            Available tools: {}\n\n\
567            Use -- to separate tool arguments from frm options:\n\
568            frm cli rabbitmqctl -V 4.2.3 -- status",
569            RABBITMQ_CLI_TOOLS.join(", ")
570        ))
571        .trailing_var_arg(true)
572        .arg(Arg::new("tool").help("Tool to run").required(true).index(1))
573        .arg(version_arg())
574        .arg(
575            Arg::new("args")
576                .help("Arguments to pass to the tool (after --)")
577                .num_args(1..)
578                .index(2),
579        )
580}
581
582fn fg_command() -> Command {
583    Command::new("fg")
584        .about("Run RabbitMQ nodes in foreground")
585        .arg_required_else_help(true)
586        .subcommand(
587            Command::new("node")
588                .about("Start RabbitMQ server in foreground")
589                .arg(version_arg()),
590        )
591}
592
593fn bg_command() -> Command {
594    Command::new("bg")
595        .about("Start and stop RabbitMQ nodes in background")
596        .arg_required_else_help(true)
597        .subcommand(
598            Command::new("start")
599                .about("Start RabbitMQ server in background (detached)")
600                .arg(version_arg()),
601        )
602        .subcommand(
603            Command::new("stop")
604                .about("Stop a running RabbitMQ node")
605                .arg(version_arg()),
606        )
607}
608
609fn inspect_command() -> Command {
610    Command::new("inspect")
611        .about("Inspect a RabbitMQ configuration file")
612        .long_about(format!(
613            "Inspect a RabbitMQ configuration file from the specified version.\n\n\
614            Available files: {}",
615            CONFIG_FILES.join(", ")
616        ))
617        .arg(
618            Arg::new("file")
619                .help("Configuration file to inspect")
620                .required(true)
621                .index(1),
622        )
623        .arg(version_arg())
624}
625
626fn version_arg() -> Arg {
627    Arg::new("version")
628        .long("version")
629        .short('V')
630        .help("RabbitMQ version to use")
631        .value_name("VERSION")
632}
633
634fn positional_version_arg(help: &'static str) -> Arg {
635    Arg::new("version").help(help).index(1).required(false)
636}
637
638fn version_opt_arg(help: &'static str) -> Arg {
639    Arg::new("version_opt")
640        .long("version")
641        .short('V')
642        .help(format!(
643            "{}; takes precedence over the positional argument",
644            help
645        ))
646        .value_name("VERSION")
647}
648
649pub fn get_version_arg(matches: &clap::ArgMatches) -> Option<&String> {
650    matches
651        .get_one::<String>("version_opt")
652        .or_else(|| matches.get_one::<String>("version"))
653}