//! The `spectra` command line (spec ยง19). Output follows `oxy`: colour and //! glyphs on a UTF-8 terminal, plain when piped, with `--plain` or with //! `NO_COLOR`; `--json` prints one document on stdout and nothing else. pub mod admin; pub mod format; pub mod json; pub mod ui; use anyhow::{bail, Result}; use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] #[command(name = "spectra", version = env!("SPECTRA_VERSION_LONG"), about = "A control plane for AI coding agents")] pub struct Cli { /// Plain output: no colour or glyphs (also NO_COLOR, or when not a terminal) #[arg(long, global = true)] pub plain: bool, /// The data directory (default: $XDG_DATA_HOME/spectra, else ~/.local/share/spectra) #[arg(long, global = true, env = "SPECTRA_DATA")] pub data: Option, #[command(subcommand)] pub command: Command, } #[derive(Subcommand)] pub enum Command { /// The version and build of this binary Version { /// Machine-readable output #[arg(long)] json: bool, }, /// The ledger's own checks: verify, check, rebuild Admin { #[command(subcommand)] command: Admin, }, } #[derive(Subcommand)] pub enum Admin { /// Recompute the hash chain and check every checkpoint and proof (exit 1 on any finding) Verify { /// Machine-readable output #[arg(long)] json: bool, }, /// Whether the projections equal a rebuild from the ledger Check, /// Replay the ledger into every projection Rebuild, } /// What every command needs: where the data lives. pub struct Ctx { pub data: PathBuf, } impl Ctx { /// The ledger's database, which must exist. pub fn db(&self) -> Result { let db = self.data.join("spectra.db"); if !db.exists() { bail!("no Spectra in {} (run `spectra init`, or pass --data)", self.data.display()); } Ok(db) } } fn default_data() -> PathBuf { if let Ok(x) = std::env::var("XDG_DATA_HOME") { return PathBuf::from(x).join("spectra"); } let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); PathBuf::from(home).join(".local").join("share").join("spectra") } fn runtime() -> Result { Ok(tokio::runtime::Builder::new_multi_thread().enable_all().build()?) } pub fn run() -> Result { let cli = Cli::parse(); ui::init(cli.plain); let ctx = Ctx { data: cli.data.unwrap_or_else(default_data) }; match cli.command { Command::Version { json } => version(json), Command::Admin { command } => runtime()?.block_on(async { match command { Admin::Verify { json } => admin::verify(&ctx, json).await, Admin::Check => admin::check(&ctx).await, Admin::Rebuild => admin::rebuild(&ctx).await, } }), } } fn version(as_json: bool) -> Result { if as_json { println!( "{}", serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "build": env!("SPECTRA_BUILD_ID"), "mcp_protocol_versions": spectra_mcp::PROTOCOL_VERSIONS, "ledger_format": spectra_core::LEDGER_FORMAT, }) ); } else { println!("spectra {}", env!("SPECTRA_VERSION_LONG")); } Ok(0) }