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