use std::io::{self, BufRead, IsTerminal, Write};
use anyhow::{Context, Result};
use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::cli::args::{AuthCommands, ContextCommands};
use crate::cli::dispatch::OutputOpts;
use crate::config::credentials::resolve_api_key;
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;
pub fn is_interactive(non_interactive_flag: bool) -> bool {
if non_interactive_flag {
return false;
}
io::stdout().is_terminal()
}
pub async fn run(
models: &Models,
config_mgr: &ConfigManager,
non_interactive: bool,
skip_login: bool,
output_opts: &OutputOpts<'_>,
) -> Result<()> {
let interactive = is_interactive(non_interactive);
if !interactive {
print_cheatsheet(config_mgr);
return Ok(());
}
print_banner();
if !skip_login {
auth_bootstrap(models, config_mgr).await?;
} else {
println!("(Skipping auth bootstrap. Run `zilliz login` if you need to sign in.)\n");
}
context_bootstrap(config_mgr)?;
intent_menu(models, config_mgr, output_opts).await?;
println!();
print_cheatsheet(config_mgr);
Ok(())
}
fn print_banner() {
println!("Welcome to the Zilliz Cloud CLI quickstart.");
println!("This walks you through login, picking a cluster, and a few common operations.");
println!("Press Ctrl+C at any prompt to abort. Nothing is written without confirmation.\n");
}
async fn auth_bootstrap(models: &Models, config_mgr: &ConfigManager) -> Result<()> {
if let Some(user) = config_mgr.get_user_info() {
println!("Already authenticated as {} ({}).", user.name, user.email);
return Ok(());
}
if config_mgr.get_credential("api_key").is_some() {
println!("Already authenticated with API key.");
return Ok(());
}
println!("Step 1/3: Sign in to Zilliz Cloud.");
println!(" [1] Browser OAuth login (recommended)");
println!(" [2] API key");
println!(" [3] Skip — finish quickstart without authenticating");
let choice = prompt_line("Select [1-3]: ")?;
match choice.trim() {
"1" => {
let auth_config = models.control_plane.auth.as_ref();
crate::cli::auth::login(config_mgr, auth_config, false, None, false, false).await?;
}
"2" => {
let auth_config = models.control_plane.auth.as_ref();
crate::cli::auth::login(config_mgr, auth_config, true, Some(""), false, false).await?;
}
_ => {
println!("Skipped sign-in. Some next steps may be unavailable.");
}
}
println!();
Ok(())
}
fn context_bootstrap(config_mgr: &ConfigManager) -> Result<()> {
println!("Step 2/3: Choose your organization context.");
let orgs = config_mgr.get_all_orgs();
if orgs.is_empty() {
println!("(No organizations resolved yet — skip until you've signed in.)\n");
return Ok(());
}
if orgs.len() == 1 {
let org = &orgs[0];
println!(
"Single organization detected: {} ({}). No switch needed.\n",
org.name, org.org_id
);
return Ok(());
}
let confirm = prompt_line("Switch organization now? [y/N]: ")?;
if matches!(confirm.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
crate::cli::auth_cmd::run(config_mgr, AuthCommands::Switch { org_id: None })?;
}
println!();
Ok(())
}
async fn intent_menu(
models: &Models,
config_mgr: &ConfigManager,
output_opts: &OutputOpts<'_>,
) -> Result<()> {
loop {
println!("Step 3/3: What would you like to do?");
println!(" [1] List clusters");
println!(" [2] Set active cluster context");
println!(" [3] List collections in the active cluster");
println!(" [4] View billing usage");
println!(" [5] Exit quickstart");
let choice = prompt_line("Select [1-5]: ")?;
match choice.trim() {
"1" => {
echo_command(&["cluster", "list"]);
crate::cli::dispatch::run(
models,
config_mgr,
"cluster",
"list",
&[],
output_opts,
false,
)
.await?;
}
"2" => {
let api_key_owned = output_opts.api_key.map(|s| s.to_string());
match prompt_cluster_id(models, config_mgr, api_key_owned.as_deref()).await? {
Some(cluster_id) => {
echo_command(&["context", "set", "--cluster-id", &cluster_id]);
crate::cli::context::run(
models,
config_mgr,
ContextCommands::Set {
cluster_id: Some(cluster_id),
endpoint: None,
database: None,
on_demand: false,
},
output_opts.format,
api_key_owned.as_deref(),
)
.await?;
}
None => {
}
}
}
"3" => {
if config_mgr.get_context().cluster_id.is_none() {
println!("No active cluster context. Pick option 2 first to select a cluster.");
} else {
echo_command(&["collection", "list"]);
crate::cli::dispatch::run(
models,
config_mgr,
"collection",
"list",
&[],
output_opts,
false,
)
.await?;
}
}
"4" => {
echo_command(&["billing", "usage"]);
crate::cli::billing::usage(models, config_mgr, &[], output_opts).await?;
}
"5" | "" | "q" | "quit" | "exit" => {
println!("Exiting intent menu.");
return Ok(());
}
other => {
println!("Unknown selection: '{}'. Pick 1-5.", other);
continue;
}
}
let again = prompt_line("\nPick another action? [y/N]: ")?;
if !matches!(again.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
return Ok(());
}
}
}
async fn prompt_cluster_id(
models: &Models,
config_mgr: &ConfigManager,
api_key_override: Option<&str>,
) -> Result<Option<String>> {
let api_key = resolve_api_key(api_key_override, config_mgr).ok_or(ApiError::NoApiKey)?;
let base_url =
crate::cli::endpoint::resolve_control_plane_url(config_mgr, &models.control_plane, None);
let client = ApiClient::new(api_key, base_url);
let result = client.call("GET", "/v2/clusters", None, None).await?;
let clusters = result
.get("clusters")
.and_then(|v| v.as_array())
.or_else(|| result.as_array())
.context("Failed to fetch clusters")?;
if clusters.is_empty() {
println!(
"No clusters found in the current organization. Pick option [2] to create one first."
);
return Ok(None);
}
println!("Available clusters:");
for (i, c) in clusters.iter().enumerate() {
let id = c.get("clusterId").and_then(|v| v.as_str()).unwrap_or("?");
let name = c.get("clusterName").and_then(|v| v.as_str()).unwrap_or("");
let region = c.get("regionId").and_then(|v| v.as_str()).unwrap_or("");
let status = c.get("status").and_then(|v| v.as_str()).unwrap_or("");
let plan = c
.get("deploymentOption")
.or_else(|| c.get("plan"))
.and_then(|v| v.as_str())
.unwrap_or("");
println!(
" [{:>2}] {:<24} {:<20} {:<10} {:<12} {}",
i + 1,
id,
name,
plan,
status,
region
);
}
let input = prompt_line(&format!(
"Select cluster [1-{}] (Enter to cancel): ",
clusters.len()
))?;
let trimmed = input.trim();
if trimmed.is_empty() {
println!("Cancelled. Active context unchanged.");
return Ok(None);
}
let idx: usize = match trimmed.parse() {
Ok(n) => n,
Err(_) => {
println!("Invalid selection '{}'. Active context unchanged.", trimmed);
return Ok(None);
}
};
if idx < 1 || idx > clusters.len() {
println!("Selection out of range. Active context unchanged.");
return Ok(None);
}
let id = clusters[idx - 1]
.get("clusterId")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.context("Selected cluster has no clusterId")?;
Ok(Some(id))
}
fn print_cheatsheet(_config_mgr: &ConfigManager) {
println!("================ Zilliz CLI cheatsheet ================");
println!("\nAuthentication:");
println!(" zilliz login # browser OAuth login");
println!(" zilliz login --api-key # log in with an API key");
println!(" zilliz whoami # show current identity");
println!(" zilliz switch # switch organization (<orgId>)");
println!("\nContext:");
println!(" zilliz context current # show active cluster context");
println!(" zilliz context set --cluster-id <clusterId> # set the active cluster");
println!(" zilliz context clear # clear the active cluster");
println!("\nClusters:");
println!(" zilliz cluster list # list clusters in current organization");
println!(" zilliz cluster create --name <name> --type free # create a free cluster");
println!(" zilliz cluster describe --cluster-id <clusterId> # show cluster details");
println!(" zilliz cluster suspend --cluster-id <clusterId> # suspend the cluster");
println!(" zilliz cluster delete --cluster-id <clusterId> # delete the cluster");
println!("\nCollections & Vectors:");
println!(" zilliz collection list # list collections in active cluster");
println!(
" zilliz collection describe --name <collection> # show collection schema"
);
println!(" zilliz vector query --collection <collection> --filter 'id > 0'");
println!(
" zilliz vector search --collection <collection> --data '[[0.1, 0.2, 0.3]]' --limit 10"
);
println!("\nOutput formatting:");
println!(" zilliz cluster list -o json # JSON output");
println!(" zilliz cluster list -o yaml # YAML output");
println!(" zilliz cluster list --query '[*].id' # JMESPath filter");
println!("\nHelp:");
println!(" zilliz --help # full command list");
println!(" zilliz <command> --help # help for any command");
println!(" zilliz quickstart --non-interactive # reprint this cheatsheet");
println!("\n=======================================================");
}
fn echo_command(args: &[&str]) {
use colored::Colorize;
let line = format!("> zilliz {}", args.join(" "));
println!("{}", line.cyan().bold());
println!();
}
fn prompt_line(prompt: &str) -> Result<String> {
print!("{}", prompt);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().lock().read_line(&mut buf)?;
Ok(buf)
}