use anyhow::Result;
use clap::CommandFactory;
use crate::cli::args::{Cli, Commands};
use crate::model::loader::Models;
use crate::model::types::{Operation, Resource};
const CLOUD_MANAGEMENT: &[&str] = &[
"cluster", "project", "backup", "import", "volume", "job", "billing",
];
const DATA_OPERATIONS: &[&str] = &[
"collection",
"vector",
"database",
"index",
"partition",
"user",
"role",
"alias",
];
const LOCAL_DEVELOPMENT: &[&str] = &["milvus"];
struct ConfigCmd {
name: &'static str,
description: &'static str,
}
const CONFIGURATION: &[ConfigCmd] = &[
ConfigCmd {
name: "configure",
description: "Configure API key and default settings.",
},
ConfigCmd {
name: "context",
description: "Manage current cluster context.",
},
ConfigCmd {
name: "auth",
description: "Authentication commands.",
},
ConfigCmd {
name: "login",
description: "Log in to Zilliz Cloud (use --cn for the CN cloud).",
},
ConfigCmd {
name: "logout",
description: "Log out and clear stored credentials.",
},
ConfigCmd {
name: "completion",
description: "Shell completion management.",
},
ConfigCmd {
name: "version",
description: "Show CLI version.",
},
];
pub fn render_help(models: &Models) -> String {
let version = env!("CARGO_PKG_VERSION");
let mut out = String::new();
out.push_str(&format!("CLI and TUI for Zilliz Cloud {}\n\n", version));
out.push_str("Usage: zilliz [OPTIONS] <COMMAND> [ARGS]\n\n");
out.push_str("Cloud Management:\n");
append_resource_group(&mut out, CLOUD_MANAGEMENT, models);
out.push('\n');
out.push_str("Data Operations:\n");
append_resource_group(&mut out, DATA_OPERATIONS, models);
out.push('\n');
out.push_str("Local Development:\n");
append_resource_group(&mut out, LOCAL_DEVELOPMENT, models);
out.push('\n');
out.push_str("Configuration:\n");
for cmd in CONFIGURATION {
out.push_str(&format!(" {:16}{}\n", cmd.name, cmd.description));
}
out.push('\n');
out.push_str(
"Options:\n\
\x20 -o, --output <FORMAT> Output format: json, table, text, yaml, csv [default: table]\n\
\x20 --query <QUERY> JMESPath query to filter output\n\
\x20 --no-header Suppress table/CSV header row\n\
\x20 -h, --help Print help\n\
\x20 -V, --version Print version\n\
\n\
Per-command Options (resource operations only):\n\
\x20 --api-key <KEY> API key (overrides env/config) [env: ZILLIZ_API_KEY]\n\
\x20 -a, --all Fetch all pages for paginated results\n\
\x20 --wait Wait for async jobs to complete\n",
);
out
}
fn append_resource_group(out: &mut String, names: &[&str], models: &Models) {
for &name in names {
let desc = lookup_description(name, models);
out.push_str(&format!(" {:16}{}\n", name, desc));
}
}
pub fn subcommand_name(cmd: &Commands) -> &'static str {
match cmd {
Commands::Configure { .. } => "configure",
Commands::Context { .. } => "context",
Commands::Version => "version",
Commands::Login { .. } => "login",
Commands::Logout => "logout",
Commands::Auth { .. } => "auth",
Commands::Completion { .. } => "completion",
Commands::History { .. } => "history",
Commands::External(_) => "external",
}
}
pub fn print_subcommand_help(name: &str) -> Result<()> {
let mut root = Cli::command();
if let Some(sub) = root.find_subcommand_mut(name) {
sub.print_help()?;
}
Ok(())
}
fn lookup_description<'a>(name: &'a str, models: &'a Models) -> &'a str {
if let Some(r) = models.control_plane.resources.get(name) {
if let Some(ref d) = r.description {
return d.as_str();
}
}
if let Some(r) = models.data_plane.resources.get(name) {
if let Some(ref d) = r.description {
return d.as_str();
}
}
if let Some(&(_, desc)) = HAND_WRITTEN_RESOURCES.iter().find(|&&(r, _)| r == name) {
return desc;
}
""
}
const HAND_WRITTEN_OPS: &[(&str, &str, &str)] = &[
("alert", "list", "List alert rules."),
("alert", "create", "Create a new alert rule."),
("alert", "update", "Update an existing alert rule."),
("alert", "delete", "Delete an alert rule."),
("alert", "enable", "Enable an alert rule."),
("alert", "disable", "Disable an alert rule."),
("cluster", "create", "Create a new cluster."),
("cluster", "metrics", "Show cluster metrics."),
("collection", "metrics", "Show collection metrics."),
("billing", "usage", "Show billing usage summary."),
("billing", "invoices", "List invoices."),
("billing", "download-invoice", "Download an invoice PDF."),
(
"milvus",
"standalone",
"Manage a local Milvus standalone Docker deployment (install/start/stop/restart/delete/upgrade).",
),
];
const HAND_WRITTEN_RESOURCES: &[(&str, &str)] = &[
("alert", "Manage alert rules."),
("milvus", "Manage local Milvus deployments."),
];
pub fn render_hand_written_resource_help(name: &str) -> Option<String> {
let desc = HAND_WRITTEN_RESOURCES
.iter()
.find(|&&(r, _)| r == name)
.map(|&(_, d)| d)?;
let mut out = String::new();
out.push_str(&format!("{}\n\n", desc));
out.push_str(&format!("Usage: zilliz {} <OPERATION> [OPTIONS]\n\n", name));
out.push_str("Operations:\n");
for &(res, op, op_desc) in HAND_WRITTEN_OPS {
if res == name {
out.push_str(&format!(" {:24}{}\n", op, op_desc));
}
}
Some(out)
}
pub fn is_hand_written_resource(name: &str) -> bool {
HAND_WRITTEN_RESOURCES.iter().any(|&(r, _)| r == name)
}
pub fn is_hand_written_op(resource: &str, op: &str) -> bool {
HAND_WRITTEN_OPS
.iter()
.any(|&(r, o, _)| r == resource && o == op)
}
pub fn hand_written_ops() -> &'static [(&'static str, &'static str, &'static str)] {
HAND_WRITTEN_OPS
}
pub fn hand_written_op_description(resource: &str, op: &str) -> Option<&'static str> {
HAND_WRITTEN_OPS
.iter()
.find(|&&(r, o, _)| r == resource && o == op)
.map(|&(_, _, desc)| desc)
}
pub fn find_resource<'a>(models: &'a Models, name: &str) -> Option<&'a Resource> {
models
.control_plane
.resources
.get(name)
.or_else(|| models.data_plane.resources.get(name))
}
pub fn render_resource_help(resource_name: &str, resource: &Resource) -> String {
let mut out = String::new();
let desc = resource.description.as_deref().unwrap_or("No description.");
out.push_str(&format!("{}\n\n", desc));
out.push_str(&format!(
"Usage: zilliz {} <OPERATION> [OPTIONS]\n\n",
resource_name
));
out.push_str("Operations:\n");
for &(res, op, desc) in HAND_WRITTEN_OPS {
if res == resource_name && !resource.operations.contains_key(op) {
out.push_str(&format!(" {:24}{}\n", op, desc));
}
}
for (op_name, op) in &resource.operations {
let op_desc = op.description.as_deref().unwrap_or("");
out.push_str(&format!(" {:24}{}\n", op_name, op_desc));
}
out
}
pub fn render_operation_help(resource_name: &str, op_name: &str, operation: &Operation) -> String {
let mut out = String::new();
let desc = operation
.description
.as_deref()
.unwrap_or("No description.");
out.push_str(&format!("{}\n\n", desc));
out.push_str(&format!(
"Usage: zilliz {} {} [OPTIONS]\n\n",
resource_name, op_name
));
out.push_str("Options:\n");
for p in &operation.params {
let flag = p.cli_flag();
let mut meta = format!("<{}>", p.param_type);
if p.required {
meta.push_str(" (required)");
}
if let Some(ref def) = p.default {
meta.push_str(&format!(" [default: {}]", def));
}
if let Some(ref choices) = p.choices {
meta.push_str(&format!(" [{}]", choices.join(", ")));
}
let desc = p.description.as_deref().unwrap_or("");
out.push_str(&format!(" {:24}{:30}{}\n", flag, meta, desc));
}
out.push_str(&format!(
" {:24}{:30}{}\n",
"--api-key", "<string>", "API key (overrides env/config) [env: ZILLIZ_API_KEY]"
));
out.push('\n');
if !operation.examples.is_empty() {
out.push_str("Examples:\n");
for ex in &operation.examples {
if ex.is_empty() {
out.push('\n');
} else {
out.push_str(&format!(" {}\n", ex));
}
}
}
out
}
pub fn available_resources(models: &Models) -> Vec<&str> {
let mut names: Vec<&str> = Vec::new();
for name in models.control_plane.resources.keys() {
names.push(name.as_str());
}
for name in models.data_plane.resources.keys() {
names.push(name.as_str());
}
names
}