//! 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 agent; pub mod format; pub mod identity; pub mod json; pub mod marks; 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, }, /// Make this machine's Spectra: record the owner's identity (read from /// their OXY_HOME and proven by signing) and make the instance key Init { /// The owner's OXY_HOME (default: $OXY_HOME, else ~/.config/oxy) #[arg(long)] owner_home: Option, /// This instance's name (default: the host name) #[arg(long)] name: Option, }, /// The owner's and the instance's identities: show, refresh Identity { #[command(subcommand)] command: IdentityCmd, }, /// Agents, each its owner's delegate: add, list, disable Agent { #[command(subcommand)] command: AgentCmd, }, /// The ledger's own checks: verify, check, rebuild Admin { #[command(subcommand)] command: Admin, }, } #[derive(Subcommand)] pub enum IdentityCmd { /// The owner and the instance as the ledger knows them Show { #[arg(long)] json: bool, }, /// Record the owner's current identity after `oxy identity delegate add|remove` Refresh { #[arg(long)] owner_home: Option, }, } #[derive(Subcommand)] pub enum AgentCmd { /// Make an agent's key in its own home and record it once you delegate to it Add { /// Lower-case letters, digits and dashes name: String, /// claude-code, antigravity, kimi-code, codex or other #[arg(long)] kind: String, /// The project it works in #[arg(long)] project: String, /// The agent's own OXY_HOME, where its key is made and kept #[arg(long)] home: PathBuf, /// Bind the key to this Unix user: a request from another is refused #[arg(long)] uid: Option, /// Bind the key to the Unix user of its first signed connection #[arg(long, conflicts_with = "uid")] learn_uid: bool, /// How long to wait for your delegation, in seconds #[arg(long, default_value_t = 600)] wait: u64, /// The owner's OXY_HOME #[arg(long)] owner_home: Option, }, /// Every agent with its owner, key, delegation and state List { #[arg(long)] json: bool, }, /// Refuse an agent's key from now on Disable { name: String, #[arg(long)] owner_home: Option, }, } #[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) } /// The ledger, for writing: the instance key signs its checkpoints, and /// the lock keeps any other writer out. pub async fn writer(&self) -> Result { let db = self.db()?; let instance = spectra_core::keys::KeyPair::instance(&self.data, "spectra")?; spectra_core::ledger::Ledger::open(&db, Some(instance)).await } /// `[owner] home` from spectra.toml. pub fn config_owner_home(&self) -> Option { let text = std::fs::read_to_string(self.data.join("spectra.toml")).ok()?; let t: toml::Table = text.parse().ok()?; t.get("owner")?.get("home")?.as_str().map(PathBuf::from) } } 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::Init { owner_home, name } => runtime()?.block_on(identity::init(&ctx, owner_home, name)), Command::Identity { command } => runtime()?.block_on(async { match command { IdentityCmd::Show { json } => identity::show(&ctx, json).await, IdentityCmd::Refresh { owner_home } => identity::refresh(&ctx, owner_home).await, } }), Command::Agent { command } => runtime()?.block_on(async { match command { AgentCmd::Add { name, kind, project, home, uid, learn_uid, wait, owner_home } => { agent::add(&ctx, agent::Add { name, kind, project, home, uid, learn_uid, wait_secs: wait, owner_home }).await } AgentCmd::List { json } => agent::list(&ctx, json).await, AgentCmd::Disable { name, owner_home } => agent::disable(&ctx, &name, owner_home).await, } }), 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) }