OXYGENHUB

Coverage / src/cli/mod.rs 61.3%

49 of 80 lines run, 9 of 26 functions · run #7 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 project;
12
pub mod ui;
13
14
use anyhow::{bail, Result};
15
use clap::{Parser, Subcommand};
16
use std::path::PathBuf;
17
18
#[derive(Parser)]
19
#[command(name = "spectra", version = env!("SPECTRA_VERSION_LONG"), about = "A control plane for AI coding agents")]
20
pub struct Cli {
21
    /// Plain output: no colour or glyphs (also NO_COLOR, or when not a terminal)
22
    #[arg(long, global = true)]
23
    pub plain: bool,
24
    /// The data directory (default: $XDG_DATA_HOME/spectra, else ~/.local/share/spectra)
25
    #[arg(long, global = true, env = "SPECTRA_DATA")]
26
    pub data: Option<PathBuf>,
27
    #[command(subcommand)]
28
    pub command: Command,
29
}
30
31
#[derive(Subcommand)]
32
pub enum Command {
33
    /// The version and build of this binary
34
    Version {
35
        /// Machine-readable output
36
        #[arg(long)]
37
        json: bool,
38
    },
39
    /// Make this machine's Spectra: record the owner's identity (read from
40
    /// their OXY_HOME and proven by signing) and make the instance key
41
    Init {
42
        /// The owner's OXY_HOME (default: $OXY_HOME, else ~/.config/oxy)
43
        #[arg(long)]
44
        owner_home: Option<PathBuf>,
45
        /// This instance's name (default: the host name)
46
        #[arg(long)]
47
        name: Option<String>,
48
    },
49
    /// The owner's and the instance's identities: show, refresh
50
    Identity {
51
        #[command(subcommand)]
52
        command: IdentityCmd,
53
    },
54
    /// The checkouts agents work in: add, list
55
    Project {
56
        #[command(subcommand)]
57
        command: ProjectCmd,
58
    },
59
    /// Agents, each its owner's delegate: add, list, disable
60
    Agent {
61
        #[command(subcommand)]
62
        command: AgentCmd,
63
    },
64
    /// The ledger's own checks: verify, check, rebuild
65
    Admin {
66
        #[command(subcommand)]
67
        command: Admin,
68
    },
69
}
70
71
#[derive(Subcommand)]
72
pub enum IdentityCmd {
73
    /// The owner and the instance as the ledger knows them
74
    Show {
75
        #[arg(long)]
76
        json: bool,
77
    },
78
    /// Record the owner's current identity after `oxy identity delegate add|remove`
79
    Refresh {
80
        #[arg(long)]
81
        owner_home: Option<PathBuf>,
82
    },
83
}
84
85
#[derive(Subcommand)]
86
pub enum ProjectCmd {
87
    /// Register a checkout: its repository id, remote and pinned subrepos
88
    Add {
89
        path: PathBuf,
90
        /// Its name (default: the directory's)
91
        #[arg(long)]
92
        name: Option<String>,
93
        /// Its remote URL (default: the checkout's origin)
94
        #[arg(long)]
95
        remote: Option<String>,
96
        #[arg(long)]
97
        owner_home: Option<PathBuf>,
98
    },
99
    /// Every project with what it pins, what pins it, and its agents
100
    List {
101
        #[arg(long)]
102
        json: bool,
103
    },
104
}
105
106
#[derive(Subcommand)]
107
pub enum AgentCmd {
108
    /// Make an agent's key in its own home and record it once you delegate to it
109
    Add {
110
        /// Lower-case letters, digits and dashes
111
        name: String,
112
        /// claude-code, antigravity, kimi-code, codex or other
113
        #[arg(long)]
114
        kind: String,
115
        /// The project it works in
116
        #[arg(long)]
117
        project: String,
118
        /// The agent's own OXY_HOME, where its key is made and kept
119
        #[arg(long)]
120
        home: PathBuf,
121
        /// Bind the key to this Unix user: a request from another is refused
122
        #[arg(long)]
123
        uid: Option<u32>,
124
        /// Bind the key to the Unix user of its first signed connection
125
        #[arg(long, conflicts_with = "uid")]
126
        learn_uid: bool,
127
        /// How long to wait for your delegation, in seconds
128
        #[arg(long, default_value_t = 600)]
129
        wait: u64,
130
        /// The owner's OXY_HOME
131
        #[arg(long)]
132
        owner_home: Option<PathBuf>,
133
    },
134
    /// Every agent with its owner, key, delegation and state
135
    List {
136
        #[arg(long)]
137
        json: bool,
138
    },
139
    /// Refuse an agent's key from now on
140
    Disable {
141
        name: String,
142
        #[arg(long)]
143
        owner_home: Option<PathBuf>,
144
    },
145
}
146
147
#[derive(Subcommand)]
148
pub enum Admin {
149
    /// Recompute the hash chain and check every checkpoint and proof (exit 1 on any finding)
150
    Verify {
151
        /// Machine-readable output
152
        #[arg(long)]
153
        json: bool,
154
    },
155
    /// Whether the projections equal a rebuild from the ledger
156
    Check,
157
    /// Replay the ledger into every projection
158
    Rebuild,
159
}
160
161
/// What every command needs: where the data lives.
162
pub struct Ctx {
163
    pub data: PathBuf,
164
}
165
166
impl Ctx {
167
    /// The ledger's database, which must exist.
16832×
    pub fn db(&self) -> Result<PathBuf> {
16932×
        let db = self.data.join("spectra.db");
17032×
        if !db.exists() {
1711×
            bail!("no Spectra in {} (run `spectra init`, or pass --data)", self.data.display());
17231×
        }
17331×
        Ok(db)
17432×
    }
175
176
    /// The ledger, for writing: the instance key signs its checkpoints, and
177
    /// the lock keeps any other writer out.
17815×
    pub async fn writer(&self) -> Result<spectra_core::ledger::Ledger> {
17915×
        let db = self.db()?;
18015×
        let instance = spectra_core::keys::KeyPair::instance(&self.data, "spectra")?;
18115×
        spectra_core::ledger::Ledger::open(&db, Some(instance)).await
18215×
    }
183
184
    /// `[owner] home` from spectra.toml.
18515×
    pub fn config_owner_home(&self) -> Option<PathBuf> {
18615×
        let text = std::fs::read_to_string(self.data.join("spectra.toml")).ok()?;
1879×
        let t: toml::Table = text.parse().ok()?;
1889×
        t.get("owner")?.get("home")?.as_str().map(PathBuf::from)
18915×
    }
190
}
191
1920×
fn default_data() -> PathBuf {
1930×
    if let Ok(x) = std::env::var("XDG_DATA_HOME") {
1940×
        return PathBuf::from(x).join("spectra");
1950×
    }
1960×
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
1970×
    PathBuf::from(home).join(".local").join("share").join("spectra")
1980×
}
199
20038×
fn runtime() -> Result<tokio::runtime::Runtime> {
20138×
    Ok(tokio::runtime::Builder::new_multi_thread().enable_all().build()?)
20238×
}
203
20438×
pub fn run() -> Result<i32> {
20538×
    let cli = Cli::parse();
20638×
    ui::init(cli.plain);
20738×
    let ctx = Ctx { data: cli.data.unwrap_or_else(default_data) };
20838×
    match cli.command {
2090×
        Command::Version { json } => version(json),
2107×
        Command::Init { owner_home, name } => runtime()?.block_on(identity::init(&ctx, owner_home, name)),
2110×
        Command::Identity { command } => runtime()?.block_on(async {
2120×
            match command {
2130×
                IdentityCmd::Show { json } => identity::show(&ctx, json).await,
2140×
                IdentityCmd::Refresh { owner_home } => identity::refresh(&ctx, owner_home).await,
215
            }
2160×
        }),
2179×
        Command::Project { command } => runtime()?.block_on(async {
2189×
            match command {
2197×
                ProjectCmd::Add { path, name, remote, owner_home } => project::add(&ctx, &path, name, remote, owner_home).await,
2202×
                ProjectCmd::List { json } => project::list(&ctx, json).await,
221
            }
2229×
        }),
2238×
        Command::Agent { command } => runtime()?.block_on(async {
2248×
            match command {
2253×
                AgentCmd::Add { name, kind, project, home, uid, learn_uid, wait, owner_home } => {
2263×
                    agent::add(&ctx, agent::Add { name, kind, project, home, uid, learn_uid, wait_secs: wait, owner_home }).await
227
                }
2283×
                AgentCmd::List { json } => agent::list(&ctx, json).await,
2292×
                AgentCmd::Disable { name, owner_home } => agent::disable(&ctx, &name, owner_home).await,
230
            }
2318×
        }),
23214×
        Command::Admin { command } => runtime()?.block_on(async {
23314×
            match command {
2349×
                Admin::Verify { json } => admin::verify(&ctx, json).await,
2354×
                Admin::Check => admin::check(&ctx).await,
2361×
                Admin::Rebuild => admin::rebuild(&ctx).await,
237
            }
23814×
        }),
239
    }
24038×
}
241
2420×
fn version(as_json: bool) -> Result<i32> {
2430×
    if as_json {
2440×
        println!(
2450×
            "{}",
2460×
            serde_json::json!({
2470×
                "version": env!("CARGO_PKG_VERSION"),
2480×
                "build": env!("SPECTRA_BUILD_ID"),
2490×
                "mcp_protocol_versions": spectra_mcp::PROTOCOL_VERSIONS,
2500×
                "ledger_format": spectra_core::LEDGER_FORMAT,
2510×
            })
2520×
        );
2530×
    } else {
2540×
        println!("spectra {}", env!("SPECTRA_VERSION_LONG"));
2550×
    }
2560×
    Ok(0)
2570×
}