pub mod api;
pub mod cli;
pub mod config;
pub mod model;
pub mod service;
pub mod tui;
pub mod update_check;
use anyhow::{bail, Result};
use clap::Parser;
use crate::cli::args::{Cli, Commands};
use crate::config::history::{self, HistoryRecord};
use crate::config::manager::ConfigManager;
use crate::model::loader::ModelLoader;
const SENSITIVE_FLAGS: &[&str] = &[
"--api-key",
"--password",
"--token",
"--secret",
"--credentials",
];
const SENSITIVE_CONFIG_KEYS: &[&str] = &["api_key", "password", "token", "secret"];
fn build_safe_command() -> String {
let mut args: Vec<String> = std::env::args().collect();
let mut i = 0;
let mut seen_configure = false;
let mut seen_set = false;
while i < args.len() {
let arg_lower = args[i].to_lowercase();
if arg_lower == "configure" {
seen_configure = true;
} else if seen_configure && arg_lower == "set" {
seen_set = true;
}
if SENSITIVE_FLAGS.contains(&args[i].as_str()) {
if let Some(next) = args.get_mut(i + 1) {
*next = "<redacted>".to_string();
}
i += 2;
continue;
}
if let Some((flag, _)) = args[i].split_once('=') {
if SENSITIVE_FLAGS.contains(&flag) {
args[i] = format!("{}=<redacted>", flag);
i += 1;
continue;
}
}
if seen_set && SENSITIVE_CONFIG_KEYS.contains(&arg_lower.as_str()) {
if let Some(next) = args.get_mut(i + 1) {
if !next.starts_with('-') {
*next = "<redacted>".to_string();
}
}
}
i += 1;
}
args.join(" ")
}
fn sanitize_error(err: &anyhow::Error) -> String {
let msg = err.to_string();
let first_line = msg.lines().next().unwrap_or(&msg);
let char_count = first_line.chars().count();
if char_count > 200 {
let truncated: String = first_line.chars().take(200).collect();
format!("{}...", truncated)
} else {
first_line.to_string()
}
}
fn build_history_record(command: &Option<Commands>, config_mgr: &ConfigManager) -> HistoryRecord {
let safe_command = build_safe_command();
let user = match config_mgr.get_user_info() {
Some(info) if !info.email.is_empty() => info.email,
_ => "anonymous".to_string(),
};
let timestamp = chrono::Local::now().to_rfc3339();
let (command_type, resource, operation) = match command {
None => ("tui".to_string(), None, Some("tui".to_string())),
Some(Commands::Configure { .. }) => {
("builtin".to_string(), None, Some("configure".to_string()))
}
Some(Commands::Context { .. }) => {
("builtin".to_string(), None, Some("context".to_string()))
}
Some(Commands::Version) => ("builtin".to_string(), None, Some("version".to_string())),
Some(Commands::Login { .. }) => ("builtin".to_string(), None, Some("login".to_string())),
Some(Commands::Logout) => ("builtin".to_string(), None, Some("logout".to_string())),
Some(Commands::Whoami) => ("builtin".to_string(), None, Some("whoami".to_string())),
Some(Commands::Switch { .. }) => ("builtin".to_string(), None, Some("switch".to_string())),
Some(Commands::Auth { .. }) => ("builtin".to_string(), None, Some("auth".to_string())),
Some(Commands::Completion { .. }) => {
("builtin".to_string(), None, Some("completion".to_string()))
}
Some(Commands::History { .. }) => {
("builtin".to_string(), None, Some("history".to_string()))
}
Some(Commands::Quickstart { .. }) => {
("builtin".to_string(), None, Some("quickstart".to_string()))
}
Some(Commands::Upgrade { .. }) => {
("builtin".to_string(), None, Some("upgrade".to_string()))
}
Some(Commands::Uninstall { .. }) => {
("builtin".to_string(), None, Some("uninstall".to_string()))
}
Some(Commands::External(args)) => {
let res = args.first().cloned();
let op = args.get(1).cloned();
("resource".to_string(), res, op)
}
};
HistoryRecord {
command: safe_command,
user,
timestamp,
command_type,
resource,
operation,
success: true,
error_message: None,
}
}
fn record_history(history_path: &std::path::PathBuf, record: &HistoryRecord) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
history::run_cleanup(history_path);
history::append_record(history_path, record);
}));
}
pub fn main_impl() {
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()) {
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 update_handle = update_check::spawn(env!("CARGO_PKG_VERSION"));
let config_mgr = ConfigManager::new(None)?;
let models = ModelLoader::load_builtin()?;
let mut history_record = build_history_record(&cli_args.command, &config_mgr);
let history_path = history::resolve_history_path(&config_mgr);
if cli_args.help {
match &cli_args.command {
None => {
print!("{}", cli::help::render_help(&models));
}
Some(Commands::External(args)) => {
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 if let Some(help) =
cli::help::render_hand_written_resource_help(resource_name)
{
match op_name {
Some(op) if cli::help::is_hand_written_op(resource_name, op) => {
if let Some(desc) =
cli::help::hand_written_op_description(resource_name, op)
{
println!(
"{}\n\nUsage: zilliz {} {} [OPTIONS]",
desc, resource_name, op
);
}
}
Some(op) => {
bail!(
"Unknown operation '{}' for resource '{}'.",
op,
resource_name
);
}
None => {
print!("{}", help);
}
}
} else {
let names = cli::help::available_resources(&models);
bail!(
"Unknown resource '{}'. Available resources: {}",
resource_name,
names.join(", ")
);
}
}
Some(cmd) => {
let path = cli::help::subcommand_path(cmd);
cli::help::print_subcommand_help_path(&path)?;
}
}
update_check::emit_tip_if_due(&update_handle);
return Ok(());
}
let output_format = cli_args.output.as_deref().unwrap_or("table");
let output_opts = cli::dispatch::OutputOpts {
format: output_format,
query: cli_args.query.as_deref(),
no_header: cli_args.no_header,
wait: false,
api_key: None,
};
let skip_history = matches!(
&cli_args.command,
Some(Commands::History {
subcmd: Some(crate::cli::args::HistoryCommands::Clear { .. })
})
);
if cli_args.command.is_none() {
let result = tui::run(models, config_mgr).await;
if let Err(ref e) = result {
history_record.success = false;
history_record.error_message = Some(sanitize_error(e));
}
record_history(&history_path, &history_record);
update_check::emit_tip_if_due(&update_handle);
return result;
}
let result: Result<()> = match cli_args.command {
None => unreachable!(),
Some(Commands::Configure { subcmd }) => cli::configure::run(&config_mgr, subcmd).await,
Some(Commands::Context {
subcmd: Some(ctx_cmd),
}) => {
cli::context::run(
&models,
&config_mgr,
ctx_cmd,
output_format,
output_opts.api_key,
)
.await
}
Some(Commands::Context { subcmd: None }) => cli::help::print_subcommand_help("context"),
Some(Commands::Version) => {
cli::version::run(output_format);
Ok(())
}
Some(Commands::Login {
no_browser,
api_key,
cn,
dev,
}) => {
let auth_config = models.control_plane.auth.as_ref();
cli::auth::login(
&config_mgr,
auth_config,
no_browser,
api_key.as_deref(),
cn,
dev,
)
.await
}
Some(Commands::Logout) => cli::auth::logout(&config_mgr),
Some(Commands::Whoami) => cli::auth_cmd::run(&config_mgr, cli::args::AuthCommands::Status),
Some(Commands::Switch { org_id }) => {
cli::auth_cmd::run(&config_mgr, cli::args::AuthCommands::Switch { org_id })
}
Some(Commands::Auth {
subcmd: Some(auth_cmd),
}) => cli::auth_cmd::run(&config_mgr, auth_cmd),
Some(Commands::Auth { subcmd: None }) => cli::help::print_subcommand_help("auth"),
Some(Commands::Completion {
subcmd: Some(comp_cmd),
}) => cli::completion::run(comp_cmd),
Some(Commands::Completion { subcmd: None }) => {
cli::help::print_subcommand_help("completion")
}
Some(Commands::History { subcmd }) => {
cli::history::run(&config_mgr, subcmd, output_format, cli_args.no_header).await
}
Some(Commands::Upgrade { check, yes, force }) => cli::upgrade::run(check, yes, force).await,
Some(Commands::Uninstall { yes, purge }) => cli::uninstall::run(yes, purge).await,
Some(Commands::Quickstart {
non_interactive,
skip_login,
}) => {
cli::quickstart::run(
&models,
&config_mgr,
non_interactive,
skip_login,
&output_opts,
)
.await
}
Some(Commands::External(args)) => {
run_external_command(&models, &config_mgr, &args, &output_opts).await
}
};
if !skip_history {
if let Err(ref e) = result {
history_record.success = false;
history_record.error_message = Some(sanitize_error(e));
}
record_history(&history_path, &history_record);
}
update_check::emit_tip_if_due(&update_handle);
result
}
async fn run_external_command(
models: &crate::model::loader::Models,
config_mgr: &ConfigManager,
args: &[String],
base_output_opts: &cli::dispatch::OutputOpts<'_>,
) -> Result<()> {
let wants_help = args.iter().any(|a| a == "-h" || a == "--help");
const GLOBAL_VALUE_FLAGS: &[&str] = &["-o", "--output", "--query", "--api-key"];
const GLOBAL_BOOL_FLAGS: &[&str] = &["-h", "--help", "--no-header"];
const EXT_BOOL_FLAGS: &[&str] = &["-a", "--all", "--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_api_key: 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]) || EXT_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).filter(|v| !v.starts_with('-')) {
match str_args[i] {
"-o" | "--output" => ext_output = Some(val),
"--query" => ext_query = Some(val),
"--api-key" => ext_api_key = Some(val),
_ => {}
}
i += 2;
} else {
i += 1;
}
} else {
filtered_args.push(str_args[i]);
i += 1;
}
}
}
let ext_format_owned = ext_output.map(|s| s.to_string());
let ext_query_owned = ext_query.map(|s| s.to_string());
let ext_api_key_owned = ext_api_key.map(|s| s.to_string());
let output_opts = cli::dispatch::OutputOpts {
format: ext_format_owned
.as_deref()
.unwrap_or(base_output_opts.format),
query: ext_query_owned.as_deref().or(base_output_opts.query),
no_header: base_output_opts.no_header || ext_no_header,
wait: base_output_opts.wait || ext_wait,
api_key: ext_api_key_owned.as_deref().or(base_output_opts.api_key),
};
let 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) if cli::help::is_hand_written_op(resource_name, op) => {
let help_args = vec!["--help".to_string()];
match (resource_name, op) {
("backup", "create") => {
cli::backup::create(models, config_mgr, &help_args, &output_opts)
.await?;
}
("cluster", "create") => {
cli::cluster::create(models, config_mgr, &help_args, &output_opts)
.await?;
}
("cluster", "create-vectorlake") => {
cli::on_demand_cluster::create_vectorlake(
models,
config_mgr,
&help_args,
&output_opts,
)
.await?;
}
("on-demand-cluster", "create") => {
cli::on_demand_cluster::create(
models,
config_mgr,
&help_args,
&output_opts,
)
.await?;
}
("cluster", "metrics") => {
cli::metrics::run_from_args(
models,
config_mgr,
&help_args,
&output_opts,
)
.await?;
}
("collection", "metrics") => {
cli::metrics::run_from_args_for_collection(
models,
config_mgr,
&help_args,
&output_opts,
)
.await?;
}
("billing", "usage") => {
cli::billing::usage(models, config_mgr, &help_args, &output_opts)
.await?;
}
("billing", "invoices") => {
cli::billing::invoices(models, config_mgr, &help_args, &output_opts)
.await?;
}
("billing", "download-invoice") => {
cli::billing::download_invoice(
models,
config_mgr,
&help_args,
&output_opts,
)
.await?;
}
_ => {
if let Some(desc) =
cli::help::hand_written_op_description(resource_name, op)
{
println!(
"{}\n\nUsage: zilliz {} {} [OPTIONS]",
desc, resource_name, op
);
}
}
}
}
Some(op) => {
let mut ops: Vec<_> = resource.operations.keys().map(|s| s.as_str()).collect();
for &(res, hw_op, _) in cli::help::hand_written_ops() {
if res == resource_name && !ops.contains(&hw_op) {
ops.push(hw_op);
}
}
bail!(
"Unknown operation '{}' for resource '{}'. Available operations: {}",
op,
resource_name,
ops.join(", ")
);
}
None => {
print!(
"{}",
cli::help::render_resource_help(resource_name, resource)
);
}
}
} else if cli::help::is_hand_written_resource(resource_name) {
if resource_name == "milvus" {
let mut milvus_args: Vec<String> = filtered_args
.iter()
.skip(1)
.map(|s| s.to_string())
.collect();
milvus_args.push("--help".to_string());
cli::milvus::run_from_args(models, config_mgr, &milvus_args, &output_opts).await?;
return Ok(());
}
if resource_name == "external-collection" {
let mut ec_args: Vec<String> = filtered_args
.iter()
.skip(1)
.map(|s| s.to_string())
.collect();
ec_args.push("--help".to_string());
cli::external_collection::run_from_args(models, config_mgr, &ec_args, &output_opts)
.await?;
return Ok(());
}
match op_name {
Some(op) if cli::help::is_hand_written_op(resource_name, op) => {
let help_args = vec![op.to_string(), "--help".to_string()];
match resource_name {
"alert" => {
cli::alert::run_from_args(models, config_mgr, &help_args, &output_opts)
.await?;
}
_ => {
if let Some(desc) =
cli::help::hand_written_op_description(resource_name, op)
{
println!(
"{}\n\nUsage: zilliz {} {} [OPTIONS]",
desc, resource_name, op
);
}
}
}
}
Some(op) => {
bail!(
"Unknown operation '{}' for resource '{}'. Available: list, create, update, delete, enable, disable",
op, resource_name
);
}
None => {
if let Some(help) = cli::help::render_hand_written_resource_help(resource_name)
{
print!("{}", help);
}
}
}
} 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![]
};
if operation.is_empty() {
if let Some(res) = cli::help::find_resource(models, resource) {
print!("{}", cli::help::render_resource_help(resource, res));
} else if let Some(help) = cli::help::render_hand_written_resource_help(resource) {
print!("{}", help);
} else {
let names = cli::help::available_resources(models);
bail!(
"Unknown resource '{}'. Available resources: {}",
resource,
names.join(", ")
);
}
return Ok(());
}
match (resource, operation) {
("alert", op) => {
let mut alert_args = vec![op.to_string()];
alert_args.extend(rest);
cli::alert::run_from_args(models, config_mgr, &alert_args, &output_opts).await?;
}
("milvus", op) => {
let mut milvus_args = vec![op.to_string()];
milvus_args.extend(rest);
cli::milvus::run_from_args(models, config_mgr, &milvus_args, &output_opts).await?;
}
("external-collection", op) => {
let mut ec_args = vec![op.to_string()];
ec_args.extend(rest);
cli::external_collection::run_from_args(models, config_mgr, &ec_args, &output_opts)
.await?;
}
("backup", "create") => {
cli::backup::create(models, config_mgr, &rest, &output_opts).await?;
}
("cluster", "create") => {
cli::cluster::create(models, config_mgr, &rest, &output_opts).await?;
}
("cluster", "create-vectorlake") => {
cli::on_demand_cluster::create_vectorlake(models, config_mgr, &rest, &output_opts)
.await?;
}
("on-demand-cluster", "create") => {
cli::on_demand_cluster::create(models, config_mgr, &rest, &output_opts).await?;
}
("cluster", "metrics") => {
cli::metrics::run_from_args(models, config_mgr, &rest, &output_opts).await?;
}
("collection", "metrics") => {
cli::metrics::run_from_args_for_collection(models, 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?;
}
("billing", "download-invoice") => {
cli::billing::download_invoice(models, config_mgr, &rest, &output_opts).await?;
}
_ => {
cli::dispatch::run(
models,
config_mgr,
resource,
operation,
&rest,
&output_opts,
fetch_all,
)
.await?;
}
}
Ok(())
}