OXYGENHUB
# 813b6330f3ad spectra / src / cli / project.rs
5.5 KiB · Rust · last changed The command line from another machine: every command through the API (#23, completes #21) 1 day ago source blame history raw
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
12
pub async fn add(ctx: &Ctx, path: &Path, name: Option<String>, remote: Option<String>, owner_home_flag: Option<PathBuf>) -> Result<i32> {
13
    let path = path.canonicalize().with_context(|| format!("{} does not exist", path.display()))?;
14
    let name = match name {
15
        Some(n) => n,
16
        None => projects::default_name(&path)?,
17
    };
18
    if !super::agent::valid_name(&name) {
19
        bail!("a project's name is lower-case letters, digits and dashes (--name)");
20
    }
21
    let checkout = projects::inspect(&path)?;
22
    let home = owner_home(ctx, owner_home_flag);
23
    let w = ctx.owner_writer(&home).await?;
24
    let path_s = path.display().to_string();
25
    let existing = all(&w).await?;
26
    if let Some(e) = existing.iter().find(|p| p["name"] == name.as_str() || p["path"] == path_s.as_str()) {
27
        bail!("{} is already a project ({})", path.display(), e["name"].as_str().unwrap_or(""));
28
    }
29
    let mut events: Vec<_> = w.refresh_event(&home).await?.into_iter().collect();
30
    let owner = KeyPair::load_home(&home)?;
31
    let remote = remote.or(checkout.remote.clone());
32
    let payload = serde_json::json!({
33
        "name": name, "path": path.display().to_string(), "repo_id": checkout.repo_id, "remote": remote, "modules": checkout.modules,
34
    });
35
    events.push(signed_event(&owner, "human", "project.added", payload).project(name.clone()).subject(name.clone()));
36
    events.push(spectra_core::conversation::project_channel_event(&name));
37
    w.append(events).await?;
38
    println!("{} {}  {}", ui::done("Added"), ui::subject(&name), ui::path(path.display()));
39
    if let Some(r) = &remote {
40
        println!("{}", ui::kv("remote", r));
41
    }
42
    if let Some(id) = &checkout.repo_id {
43
        println!("{}", ui::kv("repo", ui::hash(id)));
44
    }
45
    let now = all(&w).await?;
46
    let me = now.iter().find(|p| p["name"] == name.as_str()).cloned().unwrap_or_default();
47
    let names =
48
        |k: &str| me[k].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<_>>()).unwrap_or_default();
49
    let (deps, users) = (names("depends_on"), names("depended_on_by"));
50
    if !deps.is_empty() {
51
        println!("{}", ui::kv("pins", deps.join(", ")));
52
    }
53
    if !users.is_empty() {
54
        println!("{}", ui::kv("pinned by", users.join(", ")));
55
    }
56
    if checkout.repo_id.is_none() {
57
        println!("  {} {}", ui::note(), ui::dim("not an Oxygen checkout: relations come from .oxymodules, so it has none"));
58
    }
59
    Ok(0)
60
}
61
62
async fn relations(ledger: &Ledger, name: &str) -> Result<(Vec<String>, Vec<String>)> {
63
    let deps = sqlx::query_scalar("SELECT depends_on FROM project_relations WHERE project = ? ORDER BY 1")
64
        .bind(name)
65
        .fetch_all(ledger.pool())
66
        .await?;
67
    let users = sqlx::query_scalar("SELECT project FROM project_relations WHERE depends_on = ? ORDER BY 1")
68
        .bind(name)
69
        .fetch_all(ledger.pool())
70
        .await?;
71
    Ok((deps, users))
72
}
73
74
/// Every project with its relations and agents: from the daemon when one
75
/// serves, else from the ledger here.
76
async fn all(w: &super::Writer) -> Result<Vec<serde_json::Value>> {
77
    match (w.client(), w.ledger()) {
78
        (Some(c), _) => Ok(call(c, "GET", "/api/v1/projects", None).await?.as_array().cloned().unwrap_or_default()),
79
        (None, Some(l)) => local(l).await,
80
        (None, None) => Ok(vec![]),
81
    }
82
}
83
84
async fn local(ledger: &Ledger) -> Result<Vec<serde_json::Value>> {
85
    let rows: Vec<(String, String, Option<String>)> =
86
        sqlx::query_as("SELECT name, path, remote FROM projects ORDER BY name").fetch_all(ledger.pool()).await?;
87
    let mut out = Vec::new();
88
    for (name, path, remote) in rows {
89
        let (deps, users) = relations(ledger, &name).await?;
90
        let agents: Vec<String> = sqlx::query_scalar("SELECT name FROM agents WHERE project = ? AND disabled_ms IS NULL ORDER BY name")
91
            .bind(&name)
92
            .fetch_all(ledger.pool())
93
            .await?;
94
        out.push(serde_json::json!({ "name": name, "path": path, "remote": remote, "depends_on": deps, "depended_on_by": users, "agents": agents }));
95
    }
96
    Ok(out)
97
}
98
99
pub async fn list(ctx: &Ctx, json: bool) -> Result<i32> {
100
    let out = match ctx.owner_client(&owner_home(ctx, None)).await? {
101
        Some(c) => call(&c, "GET", "/api/v1/projects", None).await?.as_array().cloned().unwrap_or_default(),
102
        None => local(&Ledger::open_read_only(&ctx.db()?).await?).await?,
103
    };
104
    if json {
105
        println!("{}", serde_json::Value::Array(out));
106
        return Ok(0);
107
    }
108
    if out.is_empty() {
109
        println!("{} {}", ui::note(), ui::dim("no projects yet: spectra project add PATH"));
110
    }
111
    for p in out {
112
        let list =
113
            |k: &str| p[k].as_array().map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(", ")).unwrap_or_default();
114
        println!(
115
            "{} {}  {}",
116
            ui::glyph("▸", ">"),
117
            ui::bookmark(p["name"].as_str().unwrap_or("")),
118
            ui::path(p["path"].as_str().unwrap_or(""))
119
        );
120
        for (k, label) in [("depends_on", "pins"), ("depended_on_by", "pinned by"), ("agents", "agents")] {
121
            let v = list(k);
122
            if !v.is_empty() {
123
                println!("{}", ui::kv(label, v));
124
            }
125
        }
126
    }
127
    Ok(0)
128
}