macro_rules! outln {
() => { $crate::out::line(format_args!("")) };
($($arg:tt)*) => { $crate::out::line(format_args!($($arg)*)) };
}
macro_rules! out {
($($arg:tt)*) => { $crate::out::block(format_args!($($arg)*)) };
}
mod cmd_attest;
mod cmd_compile;
mod cmd_couple;
mod cmd_index;
mod cmd_init;
mod cmd_lint;
mod cmd_registry;
mod out;
mod seal;
mod verify_attestation;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use spec_spine_types::{Config, Error, Verdict, verdict::verb};
#[derive(Parser)]
#[command(
name = "spec-spine",
version,
about = "A typed, hash-verifiable authority ledger over a markdown spec corpus."
)]
struct Cli {
#[arg(long, global = true, value_name = "DIR")]
repo: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Compile {
#[arg(long)]
check: bool,
#[arg(long)]
json: bool,
},
Registry {
#[command(subcommand)]
query: cmd_registry::RegistryQuery,
},
Index {
#[command(subcommand)]
action: Option<cmd_index::IndexAction>,
},
Lint {
#[arg(long)]
fail_on_warn: bool,
#[arg(long)]
fail_on_info: bool,
#[arg(long)]
json: bool,
},
Couple {
#[arg(long, default_value = "origin/main")]
base: String,
#[arg(long, default_value = "HEAD")]
head: String,
#[arg(long)]
pr_body: Option<PathBuf>,
#[arg(long)]
paths_from: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Init {
#[arg(long)]
force: bool,
},
Attest {
#[arg(long, value_name = "ID")]
spec: Option<String>,
#[arg(long)]
with_coupling: bool,
#[arg(long)]
sign: bool,
#[arg(long, value_name = "PATH")]
key: Option<PathBuf>,
#[arg(long, value_name = "ID")]
key_id: Option<String>,
#[arg(long)]
json: bool,
},
VerifyAttestation {
#[arg(long, value_name = "ID")]
spec: Option<String>,
#[arg(long)]
recompute: bool,
#[arg(long)]
signature: bool,
#[arg(long, value_name = "PATH")]
attestation: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
public_key: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
seal: Option<PathBuf>,
#[arg(long)]
json: bool,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
let repo = match cli.repo {
Some(p) => p,
None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};
let json_verb = cli.command.json_verb();
let result = match &cli.command {
Command::Compile { check, json } => cmd_compile::run(&repo, *check, *json),
Command::Registry { query } => cmd_registry::run(&repo, query),
Command::Index { action } => cmd_index::run(&repo, action.as_ref()),
Command::Lint {
fail_on_warn,
fail_on_info,
json,
} => cmd_lint::run(&repo, *fail_on_warn, *fail_on_info, *json),
Command::Couple {
base,
head,
pr_body,
paths_from,
json,
} => cmd_couple::run(
&repo,
&cmd_couple::CoupleArgs {
base: base.clone(),
head: head.clone(),
pr_body: pr_body.clone(),
paths_from: paths_from.clone(),
json: *json,
},
),
Command::Init { force } => cmd_init::run(&repo, *force),
Command::Attest {
spec,
with_coupling,
sign,
key,
key_id,
json,
} => cmd_attest::run(
&repo,
&cmd_attest::AttestArgs {
spec: spec.clone(),
with_coupling: *with_coupling,
sign: *sign,
key: key.clone(),
key_id: key_id.clone(),
json: *json,
},
),
Command::VerifyAttestation {
spec,
recompute,
signature,
attestation,
public_key,
seal,
json,
} => verify_attestation::run(
&repo,
&verify_attestation::VerifyArgs {
spec: spec.clone(),
recompute: *recompute,
signature: *signature,
attestation: attestation.clone(),
public_key: public_key.clone(),
seal: seal.clone(),
json: *json,
},
),
};
match result {
Ok(code) => ExitCode::from(code),
Err(e) => {
let code = e.exit_code();
match json_verb {
Some(v) => emit_error_envelope(v, &e),
None => eprintln!("spec-spine: {e}"),
}
ExitCode::from(code)
}
}
}
pub(crate) fn emit_error_envelope(verb: &str, error: &Error) {
let verdict = Verdict::failure(verb, error);
if let Err(e) = out::verdict(&verdict) {
eprintln!("spec-spine: {error}");
eprintln!("spec-spine: (could not render the JSON verdict: {e})");
}
}
impl Command {
fn json_verb(&self) -> Option<&'static str> {
match self {
Command::Compile {
json: true,
check: true,
} => Some(verb::COMPILE_CHECK),
Command::Lint { json: true, .. } => Some(verb::LINT),
Command::Couple { json: true, .. } => Some(verb::COUPLE),
Command::Attest { json: true, .. } => Some(verb::ATTEST),
Command::VerifyAttestation { json: true, .. } => Some(verb::VERIFY_ATTESTATION),
Command::Index {
action: Some(cmd_index::IndexAction::Check { json: true, .. }),
} => Some(verb::INDEX_CHECK),
_ => None,
}
}
}
pub(crate) fn load_repo_config(repo: &Path) -> Result<Config, Error> {
let path = repo.join("spec-spine.toml");
match std::fs::read_to_string(&path) {
Ok(src) => spec_spine_types::load_config(&src),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Config::default()),
Err(e) => Err(Error::Io(format!("read {}: {e}", path.display()))),
}
}