zilliz 0.1.1

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
pub mod api;
pub mod cli;
pub mod config;
pub mod model;
pub mod service;
pub mod tui;

use anyhow::{bail, Result};
use clap::Parser;

use crate::cli::args::{Cli, Commands};
use crate::config::manager::ConfigManager;
use crate::model::loader::ModelLoader;

pub fn main_impl() {
    // Extract output format early so we can use it in error display
    let output_format = std::env::args()
        .collect::<Vec<_>>()
        .windows(2)
        .find_map(|w| {
            if w[0] == "-o" || w[0] == "--output" {
                Some(w[1].clone())
            } else {
                None
            }
        })
        .unwrap_or_else(|| "table".to_string());

    let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
    if let Err(e) = rt.block_on(async_main()) {
        // Walk the error chain to find ApiError (may be wrapped by .context())
        let api_err = e
            .chain()
            .find_map(|cause| cause.downcast_ref::<api::error::ApiError>());
        let msg = if let Some(api_err) = api_err {
            match api_err {
                api::error::ApiError::Api { code, message } => {
                    cli::formatter::format_error(&output_format, *code, message)
                }
                other => cli::formatter::format_error_simple(&output_format, &format!("{}", other)),
            }
        } else {
            cli::formatter::format_error_simple(&output_format, &format!("{:#}", e))
        };
        eprintln!("{}", msg);
        std::process::exit(1);
    }
}

async fn async_main() -> Result<()> {
    let cli_args = Cli::parse();

    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("zilliz=info".parse()?),
        )
        .with_writer(std::io::stderr)
        .init();

    let config_mgr = ConfigManager::new(None)?;
    let models = ModelLoader::load_builtin()?;

    // Custom grouped help: intercept --help
    if cli_args.help {
        match &cli_args.command {
            None => {
                // Top-level: show custom grouped help
                print!("{}", cli::help::render_help(&models));
            }
            Some(Commands::External(args)) => {
                // Resource/operation help from JSON models
                let resource_name = args.first().map(|s| s.as_str()).unwrap_or("");
                let op_name = args.get(1).map(|s| s.as_str());

                if let Some(resource) = cli::help::find_resource(&models, resource_name) {
                    match op_name {
                        Some(op) if resource.operations.contains_key(op) => {
                            print!(
                                "{}",
                                cli::help::render_operation_help(
                                    resource_name,
                                    op,
                                    &resource.operations[op]
                                )
                            );
                        }
                        Some(op) => {
                            let ops: Vec<_> = resource.operations.keys().map(|s| s.as_str()).collect();
                            bail!(
                                "Unknown operation '{}' for resource '{}'. Available operations: {}",
                                op, resource_name, ops.join(", ")
                            );
                        }
                        None => {
                            print!(
                                "{}",
                                cli::help::render_resource_help(resource_name, resource)
                            );
                        }
                    }
                } else {
                    let names = cli::help::available_resources(&models);
                    bail!(
                        "Unknown resource '{}'. Available resources: {}",
                        resource_name, names.join(", ")
                    );
                }
            }
            Some(cmd) => {
                // Clap-native subcommand: delegate to clap's built-in help
                use clap::CommandFactory;
                let name = cli::help::subcommand_name(cmd);
                let mut root = Cli::command();
                if let Some(sub) = root.find_subcommand_mut(name) {
                    sub.print_help()?;
                }
            }
        }
        return Ok(());
    }

    let output_format = cli_args.output.as_deref().unwrap_or("table");
    let fetch_all = cli_args.all;
    let output_opts = cli::dispatch::OutputOpts {
        format: output_format,
        query: cli_args.query.as_deref(),
        no_header: cli_args.no_header,
        wait: cli_args.wait,
    };

    match cli_args.command {
        None => {
            tui::run(models, config_mgr).await?;
        }
        Some(Commands::Configure { subcmd }) => {
            cli::configure::run(&config_mgr, subcmd).await?;
        }
        Some(Commands::Context(ctx_cmd)) => {
            cli::context::run(&models, &config_mgr, ctx_cmd, output_format).await?;
        }
        Some(Commands::Version) => {
            cli::version::run(output_format);
        }
        Some(Commands::Login {
            no_browser,
            with_api_key: api_key_mode,
        }) => {
            let auth_config = models
                .control_plane
                .auth
                .as_ref()
                .expect("Auth config not found in control-plane.json");
            cli::auth::login(&config_mgr, auth_config, no_browser, api_key_mode).await?;
        }
        Some(Commands::Logout) => {
            cli::auth::logout(&config_mgr)?;
        }
        Some(Commands::Auth(auth_cmd)) => {
            cli::auth_cmd::run(&config_mgr, auth_cmd)?;
        }
        Some(Commands::Completion(comp_cmd)) => {
            cli::completion::run(comp_cmd)?;
        }
        Some(Commands::Alert { subcmd: Some(alert_cmd) }) => {
            cli::alert::run(&config_mgr, alert_cmd, &output_opts).await?;
        }
        Some(Commands::Alert { subcmd: None }) => {
            // No subcommand given: show help
            use clap::CommandFactory;
            let mut root = Cli::command();
            if let Some(sub) = root.find_subcommand_mut("alert") {
                sub.print_help()?;
            }
        }
        Some(Commands::External(args)) => {
            // Clap doesn't extract global flags from external subcommands, so we
            // must strip them manually before dispatching.
            let wants_help = args.iter().any(|a| a == "-h" || a == "--help");

            // Global flags that take a value (--flag value)
            const GLOBAL_VALUE_FLAGS: &[&str] = &[
                "-o", "--output", "--query", "--api-key",
            ];
            // Global flags that are boolean (--flag, no value)
            const GLOBAL_BOOL_FLAGS: &[&str] = &[
                "-h", "--help", "-a", "--all", "--no-header", "--wait",
            ];

            let mut filtered_args: Vec<&str> = Vec::new();
            let mut ext_output: Option<&str> = None;
            let mut ext_query: Option<&str> = None;
            let mut ext_all = false;
            let mut ext_no_header = false;
            let mut ext_wait = false;
            {
                let str_args: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
                let mut i = 0;
                while i < str_args.len() {
                    if GLOBAL_BOOL_FLAGS.contains(&str_args[i]) {
                        match str_args[i] {
                            "-a" | "--all" => ext_all = true,
                            "--no-header" => ext_no_header = true,
                            "--wait" => ext_wait = true,
                            _ => {}
                        }
                        i += 1;
                    } else if GLOBAL_VALUE_FLAGS.contains(&str_args[i]) {
                        if let Some(&val) = str_args.get(i + 1) {
                            match str_args[i] {
                                "-o" | "--output" => ext_output = Some(val),
                                "--query" => ext_query = Some(val),
                                _ => {}
                            }
                        }
                        i += 2;
                    } else {
                        filtered_args.push(str_args[i]);
                        i += 1;
                    }
                }
            }

            // Override output opts with any global flags found in external args
            let ext_format_owned = ext_output.map(|s| s.to_string());
            let ext_query_owned = ext_query.map(|s| s.to_string());
            let output_opts = cli::dispatch::OutputOpts {
                format: ext_format_owned.as_deref().unwrap_or(output_opts.format),
                query: ext_query_owned.as_deref().or(output_opts.query),
                no_header: output_opts.no_header || ext_no_header,
                wait: output_opts.wait || ext_wait,
            };
            let fetch_all = fetch_all || ext_all;

            if wants_help {
                let resource_name = filtered_args.first().copied().unwrap_or("");
                let op_name = filtered_args.get(1).copied();
                if let Some(resource) = cli::help::find_resource(&models, resource_name) {
                    match op_name {
                        Some(op) if resource.operations.contains_key(op) => {
                            print!(
                                "{}",
                                cli::help::render_operation_help(
                                    resource_name,
                                    op,
                                    &resource.operations[op]
                                )
                            );
                        }
                        Some(op) => {
                            let ops: Vec<_> = resource.operations.keys().map(|s| s.as_str()).collect();
                            bail!(
                                "Unknown operation '{}' for resource '{}'. Available operations: {}",
                                op, resource_name, ops.join(", ")
                            );
                        }
                        None => {
                            print!(
                                "{}",
                                cli::help::render_resource_help(resource_name, resource)
                            );
                        }
                    }
                } else {
                    let names = cli::help::available_resources(&models);
                    bail!(
                        "Unknown resource '{}'. Available resources: {}",
                        resource_name, names.join(", ")
                    );
                }
                return Ok(());
            }

            let resource = filtered_args.first().copied().unwrap_or("");
            let operation = filtered_args.get(1).copied().unwrap_or("");
            let rest: Vec<String> = if filtered_args.len() > 2 {
                filtered_args[2..].iter().map(|s| s.to_string()).collect()
            } else {
                vec![]
            };

            // No operation given: show resource help
            if operation.is_empty() {
                if let Some(res) = cli::help::find_resource(&models, resource) {
                    print!("{}", cli::help::render_resource_help(resource, res));
                } else {
                    let names = cli::help::available_resources(&models);
                    bail!(
                        "Unknown resource '{}'. Available resources: {}",
                        resource, names.join(", ")
                    );
                }
                return Ok(());
            }

            // Intercept hand-written commands before model dispatch
            match (resource, operation) {
                ("cluster", "create") => {
                    cli::cluster::create(&models, &config_mgr, &rest, &output_opts).await?;
                }
                ("cluster", "metrics") => {
                    cli::metrics::run_from_args(&config_mgr, &rest, &output_opts).await?;
                }
                ("billing", "usage") => {
                    cli::billing::usage(&models, &config_mgr, &rest, &output_opts).await?;
                }
                ("billing", "invoices") => {
                    cli::billing::invoices(&models, &config_mgr, &rest, &output_opts).await?;
                }
                _ => {
                    cli::dispatch::run(
                        &models,
                        &config_mgr,
                        resource,
                        operation,
                        &rest,
                        &output_opts,
                        fetch_all,
                    )
                    .await?;
                }
            }
        }
    }

    Ok(())
}