Skip to main content

mz_deploy/cli/
extended_help.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Extended help system for mz-deploy.
11//!
12//! Provides Layer 3 help: detailed usage guides with behavior notes, examples,
13//! error recovery guidance, and related commands. Content is loaded at compile
14//! time from markdown files in the `help/` directory.
15
16use std::collections::BTreeMap;
17
18/// All known commands with their canonical names and help text.
19const COMMANDS: &[(&str, &str)] = &[
20    ("abort", include_str!("help/abort.md")),
21    ("apply", include_str!("help/apply.md")),
22    ("apply-clusters", include_str!("help/apply-clusters.md")),
23    (
24        "apply-connections",
25        include_str!("help/apply-connections.md"),
26    ),
27    (
28        "apply-network-policies",
29        include_str!("help/apply-network-policies.md"),
30    ),
31    ("apply-roles", include_str!("help/apply-roles.md")),
32    ("apply-secrets", include_str!("help/apply-secrets.md")),
33    ("apply-sources", include_str!("help/apply-sources.md")),
34    ("apply-tables", include_str!("help/apply-tables.md")),
35    ("clean", include_str!("help/clean.md")),
36    ("compile", include_str!("help/compile.md")),
37    ("explain", include_str!("help/explain.md")),
38    ("init", include_str!("help/init.md")),
39    ("debug", include_str!("help/debug.md")),
40    ("delete", include_str!("help/delete.md")),
41    ("dev", include_str!("help/dev.md")),
42    ("promote", include_str!("help/promote.md")),
43    ("describe", include_str!("help/describe.md")),
44    ("list", include_str!("help/list.md")),
45    ("lock", include_str!("help/lock.md")),
46    ("lsp", include_str!("help/lsp.md")),
47    ("log", include_str!("help/log.md")),
48    ("mcp", include_str!("help/mcp.md")),
49    ("new", include_str!("help/new.md")),
50    ("profiles", include_str!("help/profiles.md")),
51    ("setup", include_str!("help/setup.md")),
52    ("sql", include_str!("help/sql.md")),
53    ("wait", include_str!("help/wait.md")),
54    ("stage", include_str!("help/stage.md")),
55    ("test", include_str!("help/test.md")),
56];
57
58/// Aliases that map alternative names to canonical command names.
59const ALIASES: &[(&str, &str)] = &[
60    ("branches", "list"),
61    ("build", "compile"),
62    ("clusters", "apply-clusters"),
63    ("connections", "apply-connections"),
64    ("deployments", "list"),
65    ("gen-data-contracts", "lock"),
66    ("history", "log"),
67    ("network-policies", "apply-network-policies"),
68    ("deploy", "promote"),
69    ("profile", "profiles"),
70    ("ready", "wait"),
71    ("roles", "apply-roles"),
72    ("secrets", "apply-secrets"),
73    ("show", "describe"),
74    ("sources", "apply-sources"),
75    ("tables", "apply-tables"),
76];
77
78/// Look up extended help by command name or alias.
79///
80/// Returns the help text if the name matches a canonical command or a known
81/// alias. Returns `None` for unknown commands.
82pub fn help_for(name: &str) -> Option<&'static str> {
83    let canonical = resolve_alias(name);
84    COMMANDS
85        .iter()
86        .find(|(cmd, _)| *cmd == canonical)
87        .map(|(_, text)| *text)
88}
89
90/// Concatenate all help texts with delimiters for bulk ingestion.
91///
92/// Each command's help is preceded by a `--- mz-deploy help <cmd> ---` header,
93/// making it easy to parse programmatically.
94pub fn all_help() -> String {
95    let mut out = String::new();
96    for (i, (name, text)) in COMMANDS.iter().enumerate() {
97        if i > 0 {
98            out.push('\n');
99        }
100        out.push_str(&format!("--- mz-deploy help {name} ---\n\n"));
101        out.push_str(text);
102        if !text.ends_with('\n') {
103            out.push('\n');
104        }
105    }
106    out
107}
108
109/// Print an error message for an unknown command, listing valid commands.
110#[allow(clippy::print_stderr)]
111pub fn print_unknown_command(name: &str) {
112    use owo_colors::{OwoColorize, Stream, Style};
113
114    let error_style = Style::new().bright_red().bold();
115    eprintln!(
116        "{}: no help entry for '{}'",
117        "error".if_supports_color(Stream::Stderr, |t| error_style.style(t)),
118        name
119    );
120    eprintln!();
121
122    let mut commands: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
123    for (cmd, _) in COMMANDS {
124        commands.insert(cmd, vec![]);
125    }
126    for (alias, canonical) in ALIASES {
127        if let Some(aliases) = commands.get_mut(canonical) {
128            aliases.push(alias);
129        }
130    }
131
132    eprintln!("Available commands:");
133    for (cmd, aliases) in &commands {
134        if aliases.is_empty() {
135            eprintln!("  {cmd}");
136        } else {
137            eprintln!("  {cmd} ({})", aliases.join(", "));
138        }
139    }
140}
141
142fn resolve_alias(name: &str) -> &str {
143    ALIASES
144        .iter()
145        .find(|(alias, _)| *alias == name)
146        .map(|(_, canonical)| *canonical)
147        .unwrap_or(name)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[mz_ore::test]
155    fn help_for_canonical_name() {
156        assert!(help_for("compile").is_some());
157        assert!(help_for("stage").is_some());
158        assert!(help_for("apply").is_some());
159        assert!(help_for("promote").is_some());
160    }
161
162    #[mz_ore::test]
163    fn help_for_alias() {
164        assert_eq!(help_for("build"), help_for("compile"));
165        assert_eq!(help_for("show"), help_for("describe"));
166        assert_eq!(help_for("history"), help_for("log"));
167        assert_eq!(help_for("deployments"), help_for("list"));
168        assert_eq!(help_for("branches"), help_for("list"));
169        assert_eq!(help_for("gen-data-contracts"), help_for("lock"));
170        assert_eq!(help_for("ready"), help_for("wait"));
171        assert_eq!(help_for("clusters"), help_for("apply-clusters"));
172        assert_eq!(help_for("roles"), help_for("apply-roles"));
173        assert_eq!(help_for("deploy"), help_for("promote"));
174    }
175
176    #[mz_ore::test]
177    fn help_for_unknown() {
178        assert!(help_for("nonexistent").is_none());
179    }
180
181    #[mz_ore::test]
182    fn all_help_contains_all_commands() {
183        let all = all_help();
184        for (name, _) in COMMANDS {
185            assert!(
186                all.contains(&format!("--- mz-deploy help {name} ---")),
187                "missing delimiter for {name}"
188            );
189        }
190    }
191}