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