OXYGENHUB

Coverage / src/cli/project.rs 80.7%

92 of 114 lines run, 15 of 38 functions · run #24 on main · run missed

1
//! `spectra project`: the checkouts agents work in (spec §23.1).
2
3
use super::identity::owner_home;
4
use super::{call, 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
1224×
pub async fn add(ctx: &Ctx, path: &Path, name: Option<String>, remote: Option<String>, owner_home_flag: Option<PathBuf>) -> Result<i32> {
1324×
    let path = path.canonicalize().with_context(|| format!("{} does not exist", path.display()))?;
1424×
    let name = match name {
151×
        Some(n) => n,
1623×
        None => projects::default_name(&path)?,
17
    };
1824×
    if !super::agent::valid_name(&name) {
190×
        bail!("a project's name is lower-case letters, digits and dashes (--name)");
2024×
    }
2124×
    let checkout = projects::inspect(&path)?;
2224×
    let home = owner_home(ctx, owner_home_flag);
2324×
    let w = ctx.owner_writer(&home).await?;
2424×
    let path_s = path.display().to_string();
2524×
    let existing = all(&w).await?;
2624×
    if let Some(e) = existing.iter().find(|p| p["name"] == name.as_str() || p["path"] == path_s.as_str()) {
271×
        bail!("{} is already a project ({})", path.display(), e["name"].as_str().unwrap_or(""));
2823×
    }
2923×
    let mut events: Vec<_> = w.refresh_event(&home).await?.into_iter().collect();
3023×
    let owner = KeyPair::load_home(&home)?;
3123×
    let remote = remote.or(checkout.remote.clone());
3223×
    let payload = serde_json::json!({
3323×
        "name": name, "path": path.display().to_string(), "repo_id": checkout.repo_id, "remote": remote, "modules": checkout.modules,
34
    });
3523×
    events.push(signed_event(&owner, "human", "project.added", payload).project(name.clone()).subject(name.clone()));
3623×
    events.push(spectra_core::conversation::project_channel_event(&name));
3723×
    w.append(events).await?;
3823×
    println!("{} {}  {}", ui::done("Added"), ui::subject(&name), ui::path(path.display()));
3923×
    if let Some(r) = &remote {
405×
        println!("{}", ui::kv("remote", r));
4118×
    }
4223×
    if let Some(id) = &checkout.repo_id {
435×
        println!("{}", ui::kv("repo", ui::hash(id)));
4418×
    }
4523×
    let now = all(&w).await?;
4624×
    let me = now.iter().find(|p| p["name"] == name.as_str()).cloned().unwrap_or_default();
4723×
    let names =
4846×
        |k: &str| me[k].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<_>>()).unwrap_or_default();
4923×
    let (deps, users) = (names("depends_on"), names("depended_on_by"));
5023×
    if !deps.is_empty() {
511×
        println!("{}", ui::kv("pins", deps.join(", ")));
5222×
    }
5323×
    if !users.is_empty() {
541×
        println!("{}", ui::kv("pinned by", users.join(", ")));
5522×
    }
5623×
    if checkout.repo_id.is_none() {
5718×
        println!("  {} {}", ui::note(), ui::dim("not an Oxygen checkout: relations come from .oxymodules, so it has none"));
5818×
    }
5923×
    Ok(0)
6024×
}
61
6231×
async fn relations(ledger: &Ledger, name: &str) -> Result<(Vec<String>, Vec<String>)> {
6331×
    let deps = sqlx::query_scalar("SELECT depends_on FROM project_relations WHERE project = ? ORDER BY 1")
6431×
        .bind(name)
6531×
        .fetch_all(ledger.pool())
6631×
        .await?;
6731×
    let users = sqlx::query_scalar("SELECT project FROM project_relations WHERE depends_on = ? ORDER BY 1")
6831×
        .bind(name)
6931×
        .fetch_all(ledger.pool())
7031×
        .await?;
7131×
    Ok((deps, users))
7231×
}
73
74
/// Every project with its relations and agents: from the daemon when one
75
/// serves, else from the ledger here.
7647×
async fn all(w: &super::Writer) -> Result<Vec<serde_json::Value>> {
7747×
    match (w.client(), w.ledger()) {
784×
        (Some(c), _) => Ok(call(c, "GET", "/api/v1/projects", None).await?.as_array().cloned().unwrap_or_default()),
7943×
        (None, Some(l)) => local(l).await,
800×
        (None, None) => Ok(vec![]),
81
    }
8247×
}
83
8446×
async fn local(ledger: &Ledger) -> Result<Vec<serde_json::Value>> {
8546×
    let rows: Vec<(String, String, Option<String>)> =
8646×
        sqlx::query_as("SELECT name, path, remote FROM projects ORDER BY name").fetch_all(ledger.pool()).await?;
8746×
    let mut out = Vec::new();
8846×
    for (name, path, remote) in rows {
8931×
        let (deps, users) = relations(ledger, &name).await?;
9031×
        let agents: Vec<String> = sqlx::query_scalar("SELECT name FROM agents WHERE project = ? AND disabled_ms IS NULL ORDER BY name")
9131×
            .bind(&name)
9231×
            .fetch_all(ledger.pool())
9331×
            .await?;
9431×
        out.push(serde_json::json!({ "name": name, "path": path, "remote": remote, "depends_on": deps, "depended_on_by": users, "agents": agents }));
95
    }
9646×
    Ok(out)
9746×
}
98
994×
pub async fn list(ctx: &Ctx, json: bool) -> Result<i32> {
1004×
    let out = match ctx.owner_client(&owner_home(ctx, None)).await? {
1011×
        Some(c) => call(&c, "GET", "/api/v1/projects", None).await?.as_array().cloned().unwrap_or_default(),
1023×
        None => local(&Ledger::open_read_only(&ctx.db()?).await?).await?,
103
    };
1044×
    if json {
1054×
        println!("{}", serde_json::Value::Array(out));
1064×
        return Ok(0);
1070×
    }
1080×
    if out.is_empty() {
1090×
        println!("{} {}", ui::note(), ui::dim("no projects yet: spectra project add PATH"));
1100×
    }
1110×
    for p in out {
1120×
        let list =
1130×
            |k: &str| p[k].as_array().map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(", ")).unwrap_or_default();
1140×
        println!(
115
            "{} {}  {}",
1160×
            ui::glyph("▸", ">"),
1170×
            ui::bookmark(p["name"].as_str().unwrap_or("")),
1180×
            ui::path(p["path"].as_str().unwrap_or(""))
119
        );
1200×
        for (k, label) in [("depends_on", "pins"), ("depended_on_by", "pinned by"), ("agents", "agents")] {
1210×
            let v = list(k);
1220×
            if !v.is_empty() {
1230×
                println!("{}", ui::kv(label, v));
1240×
            }
125
        }
126
    }
1270×
    Ok(0)
1284×
}