use std::process::ExitCode;
use clap::{ColorChoice, Parser, Subcommand};
use serde::Serialize;
use crate::error::{CliError, ExitKind};
use crate::output::OutputFormat;
const GIT_COMMIT: &str = env!("OSSCTL_GIT_COMMIT");
const CARGO_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser, Debug)]
#[command(
name = "ossctl",
version = CARGO_VERSION,
about = "Release & readiness coordinator for the /oss-* skill family.",
long_about = "ossctl takes any repo to OSS release quality and cuts releases from it. \
It owns the OSS-RELEASE.md contract normalizer, repo-fact detection, the readiness \
audit, and the resumable per-ecosystem release-cut engine. The prose /oss-* skills \
are thin callers of this binary.",
disable_help_subcommand = true,
disable_version_flag = true,
color = ColorChoice::Never,
)]
struct Cli {
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Contract {
#[command(subcommand)]
action: ContractAction,
},
Facts(crate::facts::FactsArgs),
Audit(crate::audit::AuditArgs),
Release {
#[command(subcommand)]
action: ReleaseAction,
},
Skill {
#[command(subcommand)]
action: SkillAction,
},
Doctor(crate::doctor::DoctorArgs),
Version,
}
#[derive(Subcommand, Debug)]
pub enum ContractAction {
Show(crate::contract::ShowArgs),
Validate(crate::contract::ValidateArgs),
}
#[derive(Subcommand, Debug)]
pub enum ReleaseAction {
Plan(crate::release::PlanArgs),
Cut(crate::release::CutArgs),
Resume(crate::release::ResumeArgs),
Verify(crate::release::RunIdArgs),
Show(crate::release::RunIdArgs),
List,
Abandon(crate::release::AbandonArgs),
}
#[derive(Subcommand, Debug)]
pub enum SkillAction {
List,
Install(crate::skill::InstallArgs),
Print(crate::skill::PrintArgs),
}
pub fn run() -> ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => return handle_clap_error(&e),
};
let format = OutputFormat::from_json_flag(cli.json);
let result = match cli.command {
Command::Version => cmd_version(format),
Command::Contract { action } => match action {
ContractAction::Show(args) => crate::contract::show(&args, format),
ContractAction::Validate(args) => crate::contract::validate(&args, format),
},
Command::Facts(args) => crate::facts::run(&args, format),
Command::Audit(args) => crate::audit::run(&args, format),
Command::Release { action } => crate::release::dispatch(action, format),
Command::Skill { action } => crate::skill::dispatch(action, format),
Command::Doctor(args) => return crate::doctor::run(&args, format),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
e.emit();
ExitCode::from(e.kind as u8)
}
}
}
fn handle_clap_error(e: &clap::Error) -> ExitCode {
use clap::error::{ContextKind, ErrorKind};
if matches!(
e.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
) {
let _ = e.print();
return ExitCode::SUCCESS;
}
let message = e
.to_string()
.lines()
.filter(|l| !l.trim_start().starts_with("For more information"))
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string();
let message = if message.is_empty() {
"invalid arguments".to_string()
} else {
message
};
let code = match e.kind() {
ErrorKind::InvalidSubcommand => "unknown_subcommand",
ErrorKind::UnknownArgument => "unknown_flag",
ErrorKind::MissingRequiredArgument | ErrorKind::MissingSubcommand => "missing_argument",
ErrorKind::InvalidValue => "invalid_value",
_ => "invalid_arguments",
};
let mut err = CliError::user(code, message);
if let Some(invalid) = clap_context(e, ContextKind::InvalidValue) {
err = err.with_invalid_value(invalid);
}
if let Some(valid) = clap_context(e, ContextKind::ValidValue) {
let expected: Vec<&str> = valid.split(", ").collect();
err = err.with_expected(serde_json::json!(expected));
}
err.emit();
ExitCode::from(ExitKind::User as u8)
}
fn clap_context(e: &clap::Error, want: clap::error::ContextKind) -> Option<String> {
e.context().find_map(|(kind, value)| {
if kind == want {
Some(value.to_string())
} else {
None
}
})
}
#[derive(Debug, Serialize)]
struct SkillCatalogEntry {
name: &'static str,
cli_version: &'static str,
schema_version: u32,
}
#[derive(Debug, Serialize)]
struct VersionPayload {
version: &'static str,
commit: &'static str,
schema_version: u32,
supported_schemas: &'static [u32],
skills: Vec<SkillCatalogEntry>,
}
fn cmd_version(format: OutputFormat) -> Result<(), CliError> {
let payload = VersionPayload {
version: CARGO_VERSION,
commit: GIT_COMMIT,
schema_version: ossctl_core::SCHEMA_VERSION,
supported_schemas: ossctl_core::SUPPORTED_SCHEMAS,
skills: crate::skill::CATALOG
.iter()
.map(|s| SkillCatalogEntry {
name: s.name,
cli_version: CARGO_VERSION,
schema_version: crate::skill::SKILL_SCHEMA_VERSION,
})
.collect(),
};
match format {
OutputFormat::Json => crate::output::emit_json(&payload, &[])?,
OutputFormat::Text => {
println!("ossctl {}", payload.version);
println!("commit: {}", payload.commit);
println!("schema version: {}", payload.schema_version);
println!(
"supported schemas: {}",
format_u32_list(payload.supported_schemas)
);
println!("bundled skills: {}", payload.skills.len());
}
}
Ok(())
}
fn format_u32_list(values: &[u32]) -> String {
values
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ")
}