OXYGENHUB

Coverage / src/cli/mod.rs 58.1%

43 of 74 lines run, 8 of 24 functions · run #5 on main · run missed

1
//! The `spectra` command line (spec §19). Output follows `oxy`: colour and
2
//! glyphs on a UTF-8 terminal, plain when piped, with `--plain` or with
3
//! `NO_COLOR`; `--json` prints one document on stdout and nothing else.
4
5
pub mod admin;
6
pub mod agent;
7
pub mod format;
8
pub mod identity;
9
pub mod json;
10
pub mod marks;
11
pub mod ui;
12
13
use anyhow::{bail, Result};
14
use clap::{Parser, Subcommand};
15
use std::path::PathBuf;
16
17
#[derive(Parser)]
18
#[command(name = "spectra", version = env!("SPECTRA_VERSION_LONG"), about = "A control plane for AI coding agents")]
19
pub struct Cli {
20
    /// Plain output: no colour or glyphs (also NO_COLOR, or when not a terminal)
21
    #[arg(long, global = true)]
22
    pub plain: bool,
23
    /// The data directory (default: $XDG_DATA_HOME/spectra, else ~/.local/share/spectra)
24
    #[arg(long, global = true, env = "SPECTRA_DATA")]
25
    pub data: Option<PathBuf>,
26
    #[command(subcommand)]
27
    pub command: Command,
28
}
29
30
#[derive(Subcommand)]
31
pub enum Command {
32
    /// The version and build of this binary
33
    Version {
34
        /// Machine-readable output
35
        #[arg(long)]
36
        json: bool,
37
    },
38
    /// Make this machine's Spectra: record the owner's identity (read from
39
    /// their OXY_HOME and proven by signing) and make the instance key
40
    Init {
41
        /// The owner's OXY_HOME (default: $OXY_HOME, else ~/.config/oxy)
42
        #[arg(long)]
43
        owner_home: Option<PathBuf>,
44
        /// This instance's name (default: the host name)
45
        #[arg(long)]
46
        name: Option<String>,
47
    },
48
    /// The owner's and the instance's identities: show, refresh
49
    Identity {
50
        #[command(subcommand)]
51
        command: IdentityCmd,
52
    },
53
    /// Agents, each its owner's delegate: add, list, disable
54
    Agent {
55
        #[command(subcommand)]
56
        command: AgentCmd,
57
    },
58
    /// The ledger's own checks: verify, check, rebuild
59
    Admin {
60
        #[command(subcommand)]
61
        command: Admin,
62
    },
63
}
64
65
#[derive(Subcommand)]
66
pub enum IdentityCmd {
67
    /// The owner and the instance as the ledger knows them
68
    Show {
69
        #[arg(long)]
70
        json: bool,
71
    },
72
    /// Record the owner's current identity after `oxy identity delegate add|remove`
73
    Refresh {
74
        #[arg(long)]
75
        owner_home: Option<PathBuf>,
76
    },
77
}
78
79
#[derive(Subcommand)]
80
pub enum AgentCmd {
81
    /// Make an agent's key in its own home and record it once you delegate to it
82
    Add {
83
        /// Lower-case letters, digits and dashes
84
        name: String,
85
        /// claude-code, antigravity, kimi-code, codex or other
86
        #[arg(long)]
87
        kind: String,
88
        /// The project it works in
89
        #[arg(long)]
90
        project: String,
91
        /// The agent's own OXY_HOME, where its key is made and kept
92
        #[arg(long)]
93
        home: PathBuf,
94
        /// Bind the key to this Unix user: a request from another is refused
95
        #[arg(long)]
96
        uid: Option<u32>,
97
        /// Bind the key to the Unix user of its first signed connection
98
        #[arg(long, conflicts_with = "uid")]
99
        learn_uid: bool,
100
        /// How long to wait for your delegation, in seconds
101
        #[arg(long, default_value_t = 600)]
102
        wait: u64,
103
        /// The owner's OXY_HOME
104
        #[arg(long)]
105
        owner_home: Option<PathBuf>,
106
    },
107
    /// Every agent with its owner, key, delegation and state
108
    List {
109
        #[arg(long)]
110
        json: bool,
111
    },
112
    /// Refuse an agent's key from now on
113
    Disable {
114
        name: String,
115
        #[arg(long)]
116
        owner_home: Option<PathBuf>,
117
    },
118
}
119
120
#[derive(Subcommand)]
121
pub enum Admin {
122
    /// Recompute the hash chain and check every checkpoint and proof (exit 1 on any finding)
123
    Verify {
124
        /// Machine-readable output
125
        #[arg(long)]
126
        json: bool,
127
    },
128
    /// Whether the projections equal a rebuild from the ledger
129
    Check,
130
    /// Replay the ledger into every projection
131
    Rebuild,
132
}
133
134
/// What every command needs: where the data lives.
135
pub struct Ctx {
136
    pub data: PathBuf,
137
}
138
139
impl Ctx {
140
    /// The ledger's database, which must exist.
14118×
    pub fn db(&self) -> Result<PathBuf> {
14218×
        let db = self.data.join("spectra.db");
14318×
        if !db.exists() {
1441×
            bail!("no Spectra in {} (run `spectra init`, or pass --data)", self.data.display());
14517×
        }
14617×
        Ok(db)
14718×
    }
148
149
    /// The ledger, for writing: the instance key signs its checkpoints, and
150
    /// the lock keeps any other writer out.
1516×
    pub async fn writer(&self) -> Result<spectra_core::ledger::Ledger> {
1526×
        let db = self.db()?;
1536×
        let instance = spectra_core::keys::KeyPair::instance(&self.data, "spectra")?;
1546×
        spectra_core::ledger::Ledger::open(&db, Some(instance)).await
1556×
    }
156
157
    /// `[owner] home` from spectra.toml.
1586×
    pub fn config_owner_home(&self) -> Option<PathBuf> {
1596×
        let text = std::fs::read_to_string(self.data.join("spectra.toml")).ok()?;
1603×
        let t: toml::Table = text.parse().ok()?;
1613×
        t.get("owner")?.get("home")?.as_str().map(PathBuf::from)
1626×
    }
163
}
164
1650×
fn default_data() -> PathBuf {
1660×
    if let Ok(x) = std::env::var("XDG_DATA_HOME") {
1670×
        return PathBuf::from(x).join("spectra");
1680×
    }
1690×
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
1700×
    PathBuf::from(home).join(".local").join("share").join("spectra")
1710×
}
172
17321×
fn runtime() -> Result<tokio::runtime::Runtime> {
17421×
    Ok(tokio::runtime::Builder::new_multi_thread().enable_all().build()?)
17521×
}
176
17721×
pub fn run() -> Result<i32> {
17821×
    let cli = Cli::parse();
17921×
    ui::init(cli.plain);
18021×
    let ctx = Ctx { data: cli.data.unwrap_or_else(default_data) };
18121×
    match cli.command {
1820×
        Command::Version { json } => version(json),
1834×
        Command::Init { owner_home, name } => runtime()?.block_on(identity::init(&ctx, owner_home, name)),
1840×
        Command::Identity { command } => runtime()?.block_on(async {
1850×
            match command {
1860×
                IdentityCmd::Show { json } => identity::show(&ctx, json).await,
1870×
                IdentityCmd::Refresh { owner_home } => identity::refresh(&ctx, owner_home).await,
188
            }
1890×
        }),
1907×
        Command::Agent { command } => runtime()?.block_on(async {
1917×
            match command {
1922×
                AgentCmd::Add { name, kind, project, home, uid, learn_uid, wait, owner_home } => {
1932×
                    agent::add(&ctx, agent::Add { name, kind, project, home, uid, learn_uid, wait_secs: wait, owner_home }).await
194
                }
1953×
                AgentCmd::List { json } => agent::list(&ctx, json).await,
1962×
                AgentCmd::Disable { name, owner_home } => agent::disable(&ctx, &name, owner_home).await,
197
            }
1987×
        }),
19910×
        Command::Admin { command } => runtime()?.block_on(async {
20010×
            match command {
2017×
                Admin::Verify { json } => admin::verify(&ctx, json).await,
2022×
                Admin::Check => admin::check(&ctx).await,
2031×
                Admin::Rebuild => admin::rebuild(&ctx).await,
204
            }
20510×
        }),
206
    }
20721×
}
208
2090×
fn version(as_json: bool) -> Result<i32> {
2100×
    if as_json {
2110×
        println!(
2120×
            "{}",
2130×
            serde_json::json!({
2140×
                "version": env!("CARGO_PKG_VERSION"),
2150×
                "build": env!("SPECTRA_BUILD_ID"),
2160×
                "mcp_protocol_versions": spectra_mcp::PROTOCOL_VERSIONS,
2170×
                "ledger_format": spectra_core::LEDGER_FORMAT,
2180×
            })
2190×
        );
2200×
    } else {
2210×
        println!("spectra {}", env!("SPECTRA_VERSION_LONG"));
2220×
    }
2230×
    Ok(0)
2240×
}