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