mod cmd_attest;
mod cmd_compile;
mod cmd_couple;
mod cmd_index;
mod cmd_init;
mod cmd_lint;
mod cmd_registry;
mod seal;
mod verify_attestation;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use spec_spine_types::{Config, Error};
#[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,
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,
},
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>,
},
Init {
#[arg(long)]
force: bool,
},
Attest {
#[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>,
},
VerifyAttestation {
#[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>,
},
}
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 result = match &cli.command {
Command::Compile => cmd_compile::run(&repo),
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,
} => cmd_lint::run(&repo, *fail_on_warn, *fail_on_info),
Command::Couple {
base,
head,
pr_body,
paths_from,
} => cmd_couple::run(
&repo,
&cmd_couple::CoupleArgs {
base: base.clone(),
head: head.clone(),
pr_body: pr_body.clone(),
paths_from: paths_from.clone(),
},
),
Command::Init { force } => cmd_init::run(&repo, *force),
Command::Attest {
with_coupling,
sign,
key,
key_id,
} => cmd_attest::run(
&repo,
&cmd_attest::AttestArgs {
with_coupling: *with_coupling,
sign: *sign,
key: key.clone(),
key_id: key_id.clone(),
},
),
Command::VerifyAttestation {
recompute,
signature,
attestation,
public_key,
seal,
} => verify_attestation::run(
&repo,
&verify_attestation::VerifyArgs {
recompute: *recompute,
signature: *signature,
attestation: attestation.clone(),
public_key: public_key.clone(),
seal: seal.clone(),
},
),
};
match result {
Ok(code) => ExitCode::from(code),
Err(e) => {
eprintln!("spec-spine: {e}");
ExitCode::from(e.exit_code())
}
}
}
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()))),
}
}