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