zilliz 1.2.0

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
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",
];

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.",
    },
    ConfigCmd {
        name: "logout",
        description: "Log out and clear stored credentials.",
    },
    ConfigCmd {
        name: "completion",
        description: "Shell completion management.",
    },
    ConfigCmd {
        name: "version",
        description: "Show CLI version.",
    },
];

/// Render the full grouped help text.
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");

    // Cloud Management
    out.push_str("Cloud Management:\n");
    append_resource_group(&mut out, CLOUD_MANAGEMENT, models);
    out.push('\n');

    // Data Operations
    out.push_str("Data Operations:\n");
    append_resource_group(&mut out, DATA_OPERATIONS, models);
    out.push('\n');

    // Configuration
    out.push_str("Configuration:\n");
    for cmd in CONFIGURATION {
        out.push_str(&format!("  {:16}{}\n", cmd.name, cmd.description));
    }
    out.push('\n');

    // Global options
    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));
    }
}

/// Return the clap subcommand name for a parsed `Commands` variant.
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::External(_) => "external",
    }
}

/// Print clap's built-in help for a named subcommand.
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;
    }
    ""
}

/// Extra operations that are hand-written (not in JSON models) per resource.
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."),
];

/// Resources that are entirely hand-written (not in JSON models).
const HAND_WRITTEN_RESOURCES: &[(&str, &str)] = &[("alert", "Manage alert rules.")];

/// Render help for a resource that exists only in hand-written ops (no JSON model).
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)
}

/// Check if a resource is entirely hand-written (not in JSON models).
pub fn is_hand_written_resource(name: &str) -> bool {
    HAND_WRITTEN_RESOURCES.iter().any(|&(r, _)| r == name)
}

/// Check if an operation is hand-written (not in JSON models).
pub fn is_hand_written_op(resource: &str, op: &str) -> bool {
    HAND_WRITTEN_OPS
        .iter()
        .any(|&(r, o, _)| r == resource && o == op)
}

/// Return the list of hand-written operations.
pub fn hand_written_ops() -> &'static [(&'static str, &'static str, &'static str)] {
    HAND_WRITTEN_OPS
}

/// Return the description of a hand-written operation, if any.
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)
}

/// Find a resource across both model files.
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))
}

/// Render help for a resource: description + operations list.
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");

    // Hand-written ops for this resource (listed first if not already in model)
    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
}

/// Render help for a specific operation: description, flags, examples.
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));
    }
    // Common flags available on all resource operations
    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
}

/// List all available resource names.
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
}