OXYGENHUB

Coverage / src/cli/project.rs 78.4%

76 of 97 lines run, 6 of 20 functions · run #20 on main · run missed

1
//! `spectra project`: the checkouts agents work in (spec §23.1).
2
3
use super::identity::{owner_home, refresh_event};
4
use super::{ui, Ctx};
5
use anyhow::{bail, Context, Result};
6
use spectra_core::keys::KeyPair;
7
use spectra_core::ledger::Ledger;
8
use spectra_core::principals::signed_event;
9
use spectra_core::projects;
10
use std::path::{Path, PathBuf};
11
1223×
pub async fn add(ctx: &Ctx, path: &Path, name: Option<String>, remote: Option<String>, owner_home_flag: Option<PathBuf>) -> Result<i32> {
1323×
    let path = path.canonicalize().with_context(|| format!("{} does not exist", path.display()))?;
1423×
    let name = match name {
151×
        Some(n) => n,
1622×
        None => projects::default_name(&path)?,
17
    };
1823×
    if !super::agent::valid_name(&name) {
190×
        bail!("a project's name is lower-case letters, digits and dashes (--name)");
2023×
    }
2123×
    let checkout = projects::inspect(&path)?;
2223×
    let home = owner_home(ctx, owner_home_flag);
2323×
    let w = ctx.owner_writer(&home).await?;
2423×
    let ledger = w.ledger();
2523×
    let exists: Option<String> = sqlx::query_scalar("SELECT name FROM projects WHERE name = ? OR path = ?")
2623×
        .bind(&name)
2723×
        .bind(path.display().to_string())
2823×
        .fetch_optional(ledger.pool())
2923×
        .await?;
3023×
    if let Some(e) = exists {
311×
        bail!("{} is already a project ({e})", path.display());
3222×
    }
3322×
    let mut events: Vec<_> = refresh_event(ledger, &home).await?.into_iter().collect();
3422×
    let owner = KeyPair::load_home(&home)?;
3522×
    let remote = remote.or(checkout.remote.clone());
3622×
    let payload = serde_json::json!({
3722×
        "name": name, "path": path.display().to_string(), "repo_id": checkout.repo_id, "remote": remote, "modules": checkout.modules,
38
    });
3922×
    events.push(signed_event(&owner, "human", "project.added", payload).project(name.clone()).subject(name.clone()));
4022×
    events.push(spectra_core::conversation::project_channel_event(&name));
4122×
    w.append(events).await?;
4222×
    println!("{} {}  {}", ui::done("Added"), ui::subject(&name), ui::path(path.display()));
4322×
    if let Some(r) = &remote {
445×
        println!("{}", ui::kv("remote", r));
4517×
    }
4622×
    if let Some(id) = &checkout.repo_id {
475×
        println!("{}", ui::kv("repo", ui::hash(id)));
4817×
    }
4922×
    let (deps, users) = relations(ledger, &name).await?;
5022×
    if !deps.is_empty() {
511×
        println!("{}", ui::kv("pins", deps.join(", ")));
5221×
    }
5322×
    if !users.is_empty() {
541×
        println!("{}", ui::kv("pinned by", users.join(", ")));
5521×
    }
5622×
    if checkout.repo_id.is_none() {
5717×
        println!("  {} {}", ui::note(), ui::dim("not an Oxygen checkout: relations come from .oxymodules, so it has none"));
5817×
    }
5922×
    Ok(0)
6023×
}
61
6227×
async fn relations(ledger: &Ledger, name: &str) -> Result<(Vec<String>, Vec<String>)> {
6327×
    let deps = sqlx::query_scalar("SELECT depends_on FROM project_relations WHERE project = ? ORDER BY 1")
6427×
        .bind(name)
6527×
        .fetch_all(ledger.pool())
6627×
        .await?;
6727×
    let users = sqlx::query_scalar("SELECT project FROM project_relations WHERE depends_on = ? ORDER BY 1")
6827×
        .bind(name)
6927×
        .fetch_all(ledger.pool())
7027×
        .await?;
7127×
    Ok((deps, users))
7227×
}
73
743×
pub async fn list(ctx: &Ctx, json: bool) -> Result<i32> {
753×
    let ledger = Ledger::open_read_only(&ctx.db()?).await?;
763×
    let rows: Vec<(String, String, Option<String>)> =
773×
        sqlx::query_as("SELECT name, path, remote FROM projects ORDER BY name").fetch_all(ledger.pool()).await?;
783×
    let mut out = Vec::new();
795×
    for (name, path, remote) in rows {
805×
        let (deps, users) = relations(&ledger, &name).await?;
815×
        let agents: Vec<String> = sqlx::query_scalar("SELECT name FROM agents WHERE project = ? AND disabled_ms IS NULL ORDER BY name")
825×
            .bind(&name)
835×
            .fetch_all(ledger.pool())
845×
            .await?;
855×
        out.push(serde_json::json!({ "name": name, "path": path, "remote": remote, "depends_on": deps, "depended_on_by": users, "agents": agents }));
86
    }
873×
    if json {
883×
        println!("{}", serde_json::Value::Array(out));
893×
        return Ok(0);
900×
    }
910×
    if out.is_empty() {
920×
        println!("{} {}", ui::note(), ui::dim("no projects yet: spectra project add PATH"));
930×
    }
940×
    for p in out {
950×
        let list =
960×
            |k: &str| p[k].as_array().map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(", ")).unwrap_or_default();
970×
        println!(
98
            "{} {}  {}",
990×
            ui::glyph("▸", ">"),
1000×
            ui::bookmark(p["name"].as_str().unwrap_or("")),
1010×
            ui::path(p["path"].as_str().unwrap_or(""))
102
        );
1030×
        for (k, label) in [("depends_on", "pins"), ("depended_on_by", "pinned by"), ("agents", "agents")] {
1040×
            let v = list(k);
1050×
            if !v.is_empty() {
1060×
                println!("{}", ui::kv(label, v));
1070×
            }
108
        }
109
    }
1100×
    Ok(0)
1113×
}