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",
"on-demand-cluster",
"privatelink",
"stage",
];
const DATA_OPERATIONS: &[&str] = &[
"collection",
"vector",
"database",
"index",
"partition",
"user",
"role",
"alias",
"external-collection",
];
const LOCAL_DEVELOPMENT: &[&str] = &["milvus"];
struct ConfigCmd {
name: &'static str,
description: &'static str,
}
const GETTING_STARTED: &[ConfigCmd] = &[
ConfigCmd {
name: "quickstart",
description: "Guided onboarding for first-time users.",
},
ConfigCmd {
name: "login",
description: "Log in to Zilliz Cloud (use --cn for the CN cloud).",
},
];
const CONFIGURATION: &[ConfigCmd] = &[
ConfigCmd {
name: "configure",
description: "Configure API key and default settings.",
},
ConfigCmd {
name: "context",
description: "Manage current cluster context.",
},
ConfigCmd {
name: "logout",
description: "Log out and clear stored credentials.",
},
ConfigCmd {
name: "whoami",
description: "Show current authentication status.",
},
ConfigCmd {
name: "switch",
description: "Switch to a different organization.",
},
ConfigCmd {
name: "completion",
description: "Shell completion management.",
},
ConfigCmd {
name: "version",
description: "Show CLI version.",
},
ConfigCmd {
name: "upgrade",
description: "Upgrade the CLI to the latest released 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("Getting Started:\n");
for cmd in GETTING_STARTED {
out.push_str(&format!(" {:16}{}\n", cmd.name, cmd.description));
}
out.push('\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!(" {:19}{}\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!(" {:21}{}\n", name, desc));
}
}
pub fn subcommand_name(cmd: &Commands) -> &'static str {
subcommand_path(cmd)[0]
}
pub fn subcommand_path(cmd: &Commands) -> Vec<&'static str> {
use crate::cli::args::{
AuthCommands, CompletionCommands, ConfigureCommands, ContextCommands, HistoryCommands,
};
match cmd {
Commands::Configure { subcmd } => {
let mut v = vec!["configure"];
if let Some(s) = subcmd {
v.push(match s {
ConfigureCommands::Set { .. } => "set",
ConfigureCommands::Get { .. } => "get",
ConfigureCommands::List => "list",
ConfigureCommands::Clear => "clear",
});
}
v
}
Commands::Context { subcmd } => {
let mut v = vec!["context"];
if let Some(s) = subcmd {
v.push(match s {
ContextCommands::Set { .. } => "set",
ContextCommands::Current { .. } => "current",
ContextCommands::Clear => "clear",
});
}
v
}
Commands::Auth { subcmd } => {
let mut v = vec!["auth"];
if let Some(s) = subcmd {
v.push(match s {
AuthCommands::Status => "status",
AuthCommands::Switch { .. } => "switch",
});
}
v
}
Commands::Completion { subcmd } => {
let mut v = vec!["completion"];
if let Some(s) = subcmd {
v.push(match s {
CompletionCommands::Install { .. } => "install",
CompletionCommands::Uninstall { .. } => "uninstall",
CompletionCommands::Status { .. } => "status",
CompletionCommands::Show { .. } => "show",
});
}
v
}
Commands::History { subcmd } => {
let mut v = vec!["history"];
if let Some(s) = subcmd {
v.push(match s {
HistoryCommands::List { .. } => "list",
HistoryCommands::Search { .. } => "search",
HistoryCommands::Clear { .. } => "clear",
});
}
v
}
Commands::Version => vec!["version"],
Commands::Login { .. } => vec!["login"],
Commands::Logout => vec!["logout"],
Commands::Whoami => vec!["whoami"],
Commands::Switch { .. } => vec!["switch"],
Commands::Quickstart { .. } => vec!["quickstart"],
Commands::Upgrade { .. } => vec!["upgrade"],
Commands::External(_) => vec!["external"],
}
}
pub fn print_subcommand_help(name: &str) -> Result<()> {
print_subcommand_help_path(&[name])
}
pub fn print_subcommand_help_path(path: &[&str]) -> Result<()> {
fn descend(cmd: &mut clap::Command, path: &[&str]) -> Result<()> {
match path.split_first() {
None => {
cmd.print_help()?;
Ok(())
}
Some((head, tail)) => match cmd.find_subcommand_mut(*head) {
Some(sub) => descend(sub, tail),
None => {
cmd.print_help()?;
Ok(())
}
},
}
}
let mut root = Cli::command();
descend(&mut root, path)
}
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", "create-vectorlake", "Create a standalone VectorLake instance."),
("cluster", "metrics", "Show cluster metrics."),
("collection", "metrics", "Show collection metrics."),
("on-demand-cluster", "create", "Create an on-demand cluster."),
("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).",
),
(
"external-collection",
"refresh",
"Manage refresh jobs (trigger / describe / list).",
),
];
const HAND_WRITTEN_RESOURCES: &[(&str, &str)] = &[
("alert", "Manage alert rules."),
("milvus", "Manage local Milvus deployments."),
(
"external-collection",
"Manage external collections (refresh jobs).",
),
];
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));
}
if let Some(ref body_flag) = operation.body_param {
out.push_str(&format!(
" {:24}{:30}{}\n",
body_flag,
"<json|file://path>",
"raw JSON request body (object merged with other flags)"
));
}
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
}