mod bundle;
mod config;
mod eval;
mod export;
mod fixture_utils;
mod publish;
mod registry_client;
mod report;
mod test;
mod trust_check;
mod validate;
use clap::{ArgAction, Parser, Subcommand};
use std::path::PathBuf;
use config::{apply_history_db_env, load_config};
#[derive(Debug, Parser)]
#[command(
name = "agentcarousel",
version,
about = "Agent & skill evaluation CLI: validate, test, eval, report, init, bundle, export, trust-check. Quick start (no API keys): agentcarousel validate fixtures/skills/cmmc-assessor.yaml && agentcarousel test fixtures/skills/cmmc-assessor.yaml fixtures/skills/terraform-sentinel-scaffold.yaml --filter-tags smoke --offline true. Use `agentcarousel SUBCOMMAND -h` for flags and examples."
)]
pub struct Cli {
#[arg(long, global = true)]
config: Option<PathBuf>,
#[arg(long, global = true)]
run_id: Option<String>,
#[arg(long, global = true)]
no_color: bool,
#[arg(short = 'q', long, global = true)]
quiet: bool,
#[arg(short = 'v', long, action = ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
pub struct GlobalOptions {
pub run_id: Option<String>,
pub quiet: bool,
pub verbose: u8,
}
#[derive(Debug, Subcommand)]
enum Command {
Validate(validate::ValidateArgs),
Test(test::TestArgs),
Eval(eval::EvalArgs),
Report(report::ReportArgs),
Init(InitArgs),
Bundle(bundle::BundleArgs),
Publish(publish::PublishArgs),
Export(export::ExportArgs),
TrustCheck(trust_check::TrustCheckArgs),
}
#[derive(Debug, Parser)]
pub struct InitArgs {
#[arg(short = 's', long, conflicts_with = "agent")]
skill: bool,
#[arg(short = 'a', long, conflicts_with = "skill")]
agent: bool,
name: String,
}
pub fn run() -> i32 {
let cli = Cli::parse();
let config = match load_config(cli.config.as_deref()) {
Ok(config) => config,
Err(err) => {
eprintln!("error: {err}");
return exit_codes::ExitCode::ConfigError.as_i32();
}
};
apply_history_db_env(&config);
apply_color_settings(&config, cli.no_color);
let globals = GlobalOptions {
run_id: cli.run_id.clone(),
quiet: cli.quiet,
verbose: cli.verbose,
};
match cli.command {
Command::Validate(args) => validate::run_validate(args, &config, &globals),
Command::Test(args) => test::run_test(args, &config, &globals),
Command::Eval(args) => eval::run_eval_command(args, &config, &globals),
Command::Report(args) => report::run_report(args, &config),
Command::Init(args) => run_init(args),
Command::Bundle(args) => bundle::run_bundle(args),
Command::Publish(args) => publish::run_publish(args, &config, &globals),
Command::Export(args) => export::run_export(args, &globals),
Command::TrustCheck(args) => trust_check::run_trust_check(args, &config),
}
}
fn apply_color_settings(config: &config::ResolvedConfig, no_color: bool) {
if no_color {
console::set_colors_enabled(false);
return;
}
match config.output.color.as_str() {
"always" => console::set_colors_enabled(true),
"never" => console::set_colors_enabled(false),
"auto" => {}
_ => {}
}
}
fn run_init(args: InitArgs) -> i32 {
if !args.skill && !args.agent {
eprintln!("error: either --skill or --agent is required");
return exit_codes::ExitCode::ConfigError.as_i32();
}
match init_scaffold(&args.name) {
Ok(path) => {
println!("created {}", path.display());
exit_codes::ExitCode::Ok.as_i32()
}
Err(err) => {
eprintln!("error: {err}");
exit_codes::ExitCode::RuntimeError.as_i32()
}
}
}
fn init_scaffold(name: &str) -> Result<std::path::PathBuf, String> {
let safe_name = sanitize_fixture_name(name)?;
let rendered = FIXTURE_TEMPLATE.replace("<skill-or-agent-id>", &safe_name);
let fixtures_dir = std::path::Path::new("fixtures");
std::fs::create_dir_all(fixtures_dir)
.map_err(|err| format!("failed to create fixtures dir: {err}"))?;
let out_path = fixtures_dir.join(format!("{safe_name}.yaml"));
if out_path.exists() {
return Err(format!("fixture already exists: {}", out_path.display()));
}
std::fs::write(&out_path, rendered).map_err(|err| format!("failed to write fixture: {err}"))?;
Ok(out_path)
}
const FIXTURE_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/templates/fixture-skeleton.yaml"
));
fn sanitize_fixture_name(name: &str) -> Result<String, String> {
if name.is_empty() {
return Err("fixture name cannot be empty".to_string());
}
if std::path::Path::new(name).components().count() != 1 {
return Err("fixture name must not include path separators".to_string());
}
if !fixture_utils::is_kebab_case(name) {
return Err("fixture name must be lowercase kebab-case".to_string());
}
Ok(name.to_string())
}
mod exit_codes {
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub enum ExitCode {
Ok = 0,
Failed = 1,
ValidationFailed = 2,
ConfigError = 3,
RuntimeError = 4,
}
impl ExitCode {
pub fn as_i32(self) -> i32 {
self as i32
}
}
}