mod modules {
pub mod args;
pub mod handlers;
pub mod output;
pub mod payload;
}
use clap::Parser;
use colored::*;
use modelexpress_client::ClientConfig;
use modules::args::{ApiCommands, Cli, Commands};
use modules::handlers::{handle_api_send, handle_health_command, handle_model_command};
use modules::output::{print_output, setup_logging};
use std::process;
use tracing::{debug, error};
#[tokio::main]
async fn main() {
let cli = Cli::parse();
setup_logging(cli.verbose, cli.client_args.quiet);
let config = match ClientConfig::load(cli.client_args.clone()) {
Ok(config) => config,
Err(e) => {
error!("Configuration error: {e}");
if !cli.client_args.quiet {
eprintln!("{}: {e}", "Configuration Error".red().bold());
}
process::exit(1);
}
};
debug!(
"CLI initialized with config: endpoint={}, timeout={:?}, format={:?}",
config.connection.endpoint, config.connection.timeout_secs, cli.format
);
let result = match cli.command {
Commands::Health => {
debug!("Executing health command");
handle_health_command(config, &cli.format).await
}
Commands::Model { command } => {
debug!("Executing model command");
handle_model_command(
command,
cli.client_args.cache_path.clone(),
config,
&cli.format,
)
.await
}
Commands::Api { command } => match command {
ApiCommands::Send {
action,
payload,
payload_file,
} => {
debug!("Executing API send command");
handle_api_send(action, payload, payload_file, config, &cli.format).await
}
},
};
if let Err(e) = result {
error!("Command execution failed: {}", e);
if !cli.client_args.quiet {
match cli.format {
modules::args::OutputFormat::Human => {
eprintln!("{}: {}", "Error".red().bold(), e);
}
_ => {
let error_output = serde_json::json!({
"success": false,
"error": e.to_string()
});
print_output(&error_output, &cli.format);
}
}
}
process::exit(1);
} else {
debug!("Command executed successfully");
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::modules::args::{Cli, Commands};
use clap::{Parser, ValueEnum};
use modelexpress_client::ModelProvider;
#[test]
fn test_cli_model_provider_parsing() {
let parsed = Cli::try_parse_from([
"modelexpress-cli",
"model",
"download",
"--provider",
"hugging-face",
"google-t5/t5-small",
])
.expect("Failed to parse CLI model provider");
let Commands::Model { command } = parsed.command else {
panic!("Expected model command");
};
let super::modules::args::ModelCommands::Download { provider, .. } = command else {
panic!("Expected download subcommand");
};
assert_eq!(provider, ModelProvider::HuggingFace);
}
#[test]
fn test_cli_model_provider_value_enum() {
let parsed = ModelProvider::from_str("hugging-face", false)
.expect("Failed to parse hugging-face provider");
assert_eq!(parsed, ModelProvider::HuggingFace);
let parsed = ModelProvider::from_str("gcs", false).expect("Failed to parse gcs provider");
assert_eq!(parsed, ModelProvider::Gcs);
}
#[test]
fn test_cli_flattened_client_args_parsing() {
let cli = Cli::try_parse_from([
"modelexpress-cli",
"--endpoint",
"http://test:9999",
"--timeout",
"90",
"--no-shared-storage",
"--transfer-chunk-size",
"524288",
"health",
])
.expect("Failed to parse CLI");
assert_eq!(
cli.client_args.endpoint,
Some("http://test:9999".to_string())
);
assert_eq!(cli.client_args.timeout, Some(90));
assert!(cli.client_args.no_shared_storage);
assert_eq!(cli.client_args.transfer_chunk_size, Some(524288));
}
#[test]
fn test_cli_with_cache_path() {
let cli = Cli::try_parse_from([
"modelexpress-cli",
"--cache-path",
"/custom/cache/path",
"health",
])
.expect("Failed to parse CLI with cache path");
assert_eq!(
cli.client_args.cache_path,
Some(std::path::PathBuf::from("/custom/cache/path"))
);
}
#[test]
fn test_cli_quiet_mode() {
let cli = Cli::try_parse_from(["modelexpress-cli", "--quiet", "health"])
.expect("Failed to parse");
assert!(cli.client_args.quiet);
}
#[test]
fn test_cli_format_and_verbose_are_cli_specific() {
let cli = Cli::try_parse_from(["modelexpress-cli", "--format", "json", "-vvv", "health"])
.expect("Failed to parse");
assert_eq!(cli.format, super::modules::args::OutputFormat::Json);
assert_eq!(cli.verbose, 3);
}
#[test]
fn test_cli_model_clear_parses_explicit_provider() {
let parsed = Cli::try_parse_from([
"modelexpress-cli",
"model",
"clear",
"--provider",
"gcs",
"gs://bucket/dev/bake/qwen/rev123",
])
.expect("Expected clear command to parse with explicit provider");
let Commands::Model { command } = parsed.command else {
panic!("Expected model command");
};
let super::modules::args::ModelCommands::Clear {
provider,
model_name,
} = command
else {
panic!("Expected clear subcommand");
};
assert_eq!(provider, ModelProvider::Gcs);
assert_eq!(model_name, "gs://bucket/dev/bake/qwen/rev123");
}
#[test]
fn test_cli_model_clear_defaults_to_hugging_face_provider() {
let missing_provider = Cli::try_parse_from([
"modelexpress-cli",
"model",
"clear",
"gs://bucket/dev/bake/qwen/rev123",
])
.expect("Expected clear command to parse without provider");
let Commands::Model { command } = missing_provider.command else {
panic!("Expected model command");
};
let super::modules::args::ModelCommands::Clear {
provider,
model_name,
} = command
else {
panic!("Expected clear subcommand");
};
assert_eq!(provider, ModelProvider::HuggingFace);
assert_eq!(model_name, "gs://bucket/dev/bake/qwen/rev123");
}
}