The command line in oxy's style: ticks and verbs, key-value rows, sections, hints, spinners; plain when piped 5d695d869fbf
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 708a464d29bd
- When
- · 2026-11-02 22:03 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
5d695d869fbf6968e55f7fa2784a417f5ecf8758dcdfd845a4a2ed378d81e2b3- Depends on
- 09752034b5a8 258bb3824813 36537112fcac
- Depended on by
- 8d803d858c8c 98a0fb9f0948 813b6330f3ad 67bfa06a894f eb675dfa5e18 571572f5fb2c 2fc88a7b3a5c
- In bookmarks
- main
- In tags
- v0.1.0
- Edits
- 39 atoms, 1 tree operation, 7 files
Browse the tree at this change
added src/cli/marks.rs +34 −0
| 1 | + | //! Spectra's own marks, in the style of `oxy`'s (`ui.rs` stays exactly |
|
| 2 | + | //! `oxy`'s, so the two tools look the same): presence and agent health. |
|
| 3 | + | ||
| 4 | + | use super::ui::{dim, glyph, ok, warn}; |
|
| 5 | + | use console::style; |
|
| 6 | + | ||
| 7 | + | /// An agent's presence (spec §8.1) as a coloured glyph and word. |
|
| 8 | + | #[allow(dead_code)] // Used by agent list once presence lands (#5). |
|
| 9 | + | pub fn presence(state: &str) -> String { |
|
| 10 | + | let (g, a) = match state { |
|
| 11 | + | "online" => ("●", "*"), |
|
| 12 | + | "busy" => ("◆", "+"), |
|
| 13 | + | "idle" => ("◌", "o"), |
|
| 14 | + | "blocked" => ("‼", "!"), |
|
| 15 | + | "paused" => ("⏸", "="), |
|
| 16 | + | _ => ("○", "-"), |
|
| 17 | + | }; |
|
| 18 | + | let text = format!("{} {state:<7}", glyph(g, a)); |
|
| 19 | + | match state { |
|
| 20 | + | "online" => ok(text), |
|
| 21 | + | "busy" => style(text).cyan().to_string(), |
|
| 22 | + | "blocked" | "paused" => warn(text), |
|
| 23 | + | _ => dim(text), |
|
| 24 | + | } |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | /// Whether an agent may act: a tick, or a warning with the reason. |
|
| 28 | + | pub fn health(state: &str) -> String { |
|
| 29 | + | if state == "ok" { |
|
| 30 | + | ok(glyph("✔", "ok")) |
|
| 31 | + | } else { |
|
| 32 | + | warn(glyph("⚠", "!")) |
|
| 33 | + | } |
|
| 34 | + | } |
modified CHANGELOG.md +1 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **Changed: the command line looks like oxy's.** Every command reports in `oxy`'s style, through `oxy`'s own `ui` module: a green tick and verb, dimmed key-value rows, section bars, hints and warnings, author names and separators, and a spinner while it works; plain ASCII when piped, with `--plain` or with `NO_COLOR`. |
|
| 5 | 6 | - **Added: identities instead of tokens (#4).** `spectra init` records the owner's Oxygen identity, proven by signing with it, and makes the instance key. `spectra agent add` makes an agent's key pair in its own home and records the agent only once the owner's identity delegates to it (`oxy identity delegate add`), so every agent is somebody's: `claude-oxygen · Andy McCall's agent`. Expiry and `spectra agent disable` stop an agent's key. `spectra admin verify` replays identities and delegations and checks every signed event, refusing an event whose actor is not its signer. Requests are authenticated by signature, a five-minute window, single-use nonces and an optional Unix-user binding. One process writes a ledger at a time. |
|
| 6 | 7 | - **Added: the ledger (#3).** Every event is deterministic CBOR (through `oxy-core`) hashed with a domain-separated BLAKE3 and chained to the one before; the envelope, with its hybrid logical clock, is frozen. The instance key signs a checkpoint every 256 events, every ten minutes while writing, and on demand. `spectra admin verify [--json]` recomputes the chain and checks every checkpoint, catching an edited, removed or re-signed event; `spectra admin check` and `rebuild` keep projections equal to the ledger. A single append takes 0.15 ms. |
|
| 7 | 8 | - **Added: CI on OxygenFlow (#2).** `.flow/pipeline.kdl` in the shape of Oxygen's: fmt, clippy, test (nextest under llvm-cov, with JUnit and coverage for the hub) and build, in the vendored image with no network. |
|
| … | 1 unchanged lines |
modified src/cli/admin.rs +30 −21
| … | 8 unchanged lines |
||
| 9 | 9 | /// proof; exit 1 when anything is wrong. |
|
| 10 | 10 | pub async fn verify(ctx: &Ctx, json: bool) -> Result<i32> { |
|
| 11 | 11 | let ledger = Ledger::open_read_only(&ctx.db()?).await?; |
|
| 12 | + | let spin = if json { None } else { Some(ui::spinner("verifying the ledger")) }; |
|
| 12 | 13 | let v = ledger.verify(&mut Principals::default()).await?; |
|
| 14 | + | if let Some(s) = spin { |
|
| 15 | + | s.clear(); |
|
| 16 | + | } |
|
| 13 | 17 | if json { |
|
| 14 | 18 | println!("{}", serde_json::to_string(&v)?); |
|
| 15 | - | } else if v.clean() { |
|
| 16 | - | println!("{} ledger verified", ui::done(ui::glyph("✔", "ok"))); |
|
| 17 | - | println!("{}", ui::kv("events", v.events)); |
|
| 19 | + | return Ok(if v.clean() { 0 } else { 1 }); |
|
| 20 | + | } |
|
| 21 | + | if v.clean() { |
|
| 22 | + | println!("{} {}", ui::done("Verified"), ui::subject("the ledger")); |
|
| 23 | + | } else { |
|
| 18 | 24 | println!( |
|
| 19 | - | "{}", |
|
| 20 | - | ui::kv( |
|
| 21 | - | "checkpoints", |
|
| 22 | - | format!("{} (last at {})", v.checkpoints, v.last_checkpoint.map(|s| s.to_string()).unwrap_or("none".into())) |
|
| 23 | - | ) |
|
| 25 | + | "{} {} {}", |
|
| 26 | + | ui::err(ui::glyph("✘", "x")), |
|
| 27 | + | ui::err("The ledger does not verify:"), |
|
| 28 | + | ui::err(format!("{} finding(s)", v.findings.len())) |
|
| 24 | 29 | ); |
|
| 25 | - | println!("{}", ui::kv("uncovered", format!("{} event(s) after the last checkpoint", v.uncovered))); |
|
| 26 | - | println!("{}", ui::kv("head", ui::hash(hex::encode(v.head)))); |
|
| 27 | - | } else { |
|
| 28 | - | println!("{} ledger does not verify: {} finding(s)", ui::err(ui::glyph("✘", "x")), v.findings.len()); |
|
| 29 | - | for f in &v.findings { |
|
| 30 | - | println!(" {} {:?}: {}", ui::id(format!("#{}", f.seq)), f.problem, f.detail); |
|
| 31 | - | } |
|
| 30 | + | } |
|
| 31 | + | println!("{}", ui::kv("events", v.events)); |
|
| 32 | + | let last = v.last_checkpoint.map(|s| format!("last at #{s}")).unwrap_or_else(|| "none yet".into()); |
|
| 33 | + | println!("{}", ui::kv("signed", format!("{} checkpoint(s){}{last}", v.checkpoints, ui::sep()))); |
|
| 34 | + | if v.uncovered > 0 { |
|
| 35 | + | println!("{}", ui::kv("pending", ui::dim(format!("{} event(s) since, chained and awaiting the next checkpoint", v.uncovered)))); |
|
| 36 | + | } |
|
| 37 | + | println!("{}", ui::kv("head", ui::hash(hex::encode(v.head)))); |
|
| 38 | + | for f in &v.findings { |
|
| 39 | + | println!(" {} {} {}", ui::id(format!("#{}", f.seq)), ui::err(format!("{:?}", f.problem)), f.detail); |
|
| 32 | 40 | } |
|
| 33 | 41 | Ok(if v.clean() { 0 } else { 1 }) |
|
| 34 | 42 | } |
|
| 35 | 43 | ||
| 36 | 44 | /// `spectra admin check`: do the projections equal a rebuild? |
|
| 37 | 45 | pub async fn check(ctx: &Ctx) -> Result<i32> { |
|
| 38 | - | let ledger = Ledger::open(&ctx.db()?, None).await?; |
|
| 46 | + | let ledger = ctx.writer().await?; |
|
| 39 | 47 | let differ = ledger.check().await?; |
|
| 40 | 48 | if differ.is_empty() { |
|
| 41 | - | println!("{} projections match the ledger", ui::done(ui::glyph("✔", "ok"))); |
|
| 49 | + | println!("{} {}", ui::done("Checked"), ui::subject("every projection matches the ledger")); |
|
| 42 | 50 | Ok(0) |
|
| 43 | 51 | } else { |
|
| 44 | - | println!("{} projections differ from the ledger: {}", ui::err(ui::glyph("✘", "x")), differ.join(", ")); |
|
| 45 | - | println!(" run {} to rebuild them", ui::id("spectra admin rebuild")); |
|
| 52 | + | println!("{} projections differ from the ledger: {}", ui::caution(), ui::err(differ.join(", "))); |
|
| 53 | + | println!(" {} {}", ui::note(), ui::dim("rebuild them with spectra admin rebuild")); |
|
| 46 | 54 | Ok(1) |
|
| 47 | 55 | } |
|
| 48 | 56 | } |
|
| 49 | 57 | ||
| 50 | 58 | /// `spectra admin rebuild`: replay the ledger into every projection. |
|
| 51 | 59 | pub async fn rebuild(ctx: &Ctx) -> Result<i32> { |
|
| 52 | - | let ledger = Ledger::open(&ctx.db()?, None).await?; |
|
| 60 | + | let ledger = ctx.writer().await?; |
|
| 61 | + | let spin = ui::spinner("replaying the ledger"); |
|
| 53 | 62 | ledger.rebuild().await?; |
|
| 54 | - | println!("{} projections rebuilt from {} event(s)", ui::done(ui::glyph("✔", "ok")), ledger.head().await.0); |
|
| 63 | + | spin.done(&format!("{} {}", ui::done("Rebuilt"), ui::subject(format!("projections from {} events", ledger.head().await.0)))); |
|
| 55 | 64 | Ok(0) |
|
| 56 | 65 | } |
modified src/cli/agent.rs +32 −20
| … | 4 unchanged lines |
||
| 5 | 5 | //! owner's identity delegates to it. There is no agent without an owner. |
|
| 6 | 6 | ||
| 7 | 7 | use super::identity::{owner_home, refresh_owner}; |
|
| 8 | - | use super::{ui, Ctx}; |
|
| 8 | + | use super::{marks, ui, Ctx}; |
|
| 9 | 9 | use anyhow::{bail, Context, Result}; |
|
| 10 | 10 | use spectra_core::ids::now_ms; |
|
| 11 | 11 | use spectra_core::keys::{home_identity, KeyPair}; |
|
| … | 35 unchanged lines |
||
| 47 | 47 | KeyPair::load_home(&a.home).with_context(|| format!("reading the agent's key in {}", a.home.display()))? |
|
| 48 | 48 | } else { |
|
| 49 | 49 | let pair = KeyPair::create_home(&a.home, &a.name, "")?; |
|
| 50 | - | println!("{} made {}'s key in {}", ui::done(ui::glyph("✔", "ok")), a.name, ui::path(a.home.display())); |
|
| 50 | + | println!("{} {} {}", ui::done("Created"), ui::subject(format!("{}'s key", a.name)), ui::path(a.home.display())); |
|
| 51 | 51 | pair |
|
| 52 | 52 | }; |
|
| 53 | 53 | let delegated = |id: &oxy_core::identity::Identity| id.delegates_to(&agent.key, DELEGATION_SCOPE, now_ms()); |
|
| 54 | 54 | if !delegated(&home_identity(&home)?) { |
|
| 55 | - | println!("To make {} yours, delegate to its key (as yourself):", a.name); |
|
| 55 | + | println!("{} {}", ui::caution(), ui::warn(format!("{} is not yours yet: delegate to its key, as yourself:", a.name))); |
|
| 56 | 56 | println!( |
|
| 57 | - | " {}", |
|
| 57 | + | " {}", |
|
| 58 | 58 | ui::id(format!( |
|
| 59 | 59 | "oxy identity delegate add {} --scope changes --expires 8h", |
|
| 60 | 60 | a.home.join("keys").join(format!("{}.identity", agent.key.to_hex())).display() |
|
| 61 | 61 | )) |
|
| 62 | - | ); |
|
| 63 | - | println!( |
|
| 64 | - | "{}", |
|
| 65 | - | ui::dim(format!("waiting up to {} s for the delegation; Ctrl-C stops, and running this again resumes", a.wait_secs)) |
|
| 66 | 62 | ); |
|
| 63 | + | let spin = ui::spinner(&format!( |
|
| 64 | + | "waiting for your delegation to {} (up to {} s; Ctrl-C stops, and running this again resumes)", |
|
| 65 | + | a.name, a.wait_secs |
|
| 66 | + | )); |
|
| 67 | 67 | let deadline = std::time::Instant::now() + Duration::from_secs(a.wait_secs); |
|
| 68 | 68 | loop { |
|
| 69 | 69 | if home_identity(&home).is_ok_and(|id| delegated(&id)) { |
|
| 70 | + | spin.clear(); |
|
| 70 | 71 | break; |
|
| 71 | 72 | } |
|
| 72 | 73 | if std::time::Instant::now() >= deadline { |
|
| 74 | + | spin.clear(); |
|
| 73 | 75 | bail!("{}'s key is not delegated yet: run the command above, then this again", a.name); |
|
| 74 | 76 | } |
|
| 75 | 77 | tokio::time::sleep(Duration::from_secs(2)).await; |
|
| … | 18 unchanged lines |
||
| 94 | 96 | p["delegation"] = serde_json::json!({ "scope": d.scope.names(), "expires": d.expires }); |
|
| 95 | 97 | ledger.append_one(signed_event(&owner, "human", "agent.added", p)).await?; |
|
| 96 | 98 | ledger.checkpoint().await?; |
|
| 97 | - | println!("{} {} · {}'s agent", ui::done(ui::glyph("✔", "ok")), a.name, owner.identity.name); |
|
| 98 | - | println!("Connect its client with {}.", ui::id(format!("spectra mcp --agent {}", a.name))); |
|
| 99 | + | println!("{} {}{}{}", ui::done("Added"), ui::subject(&a.name), ui::sep(), ui::author(format!("{}'s agent", owner.identity.name))); |
|
| 100 | + | println!("{}", ui::kv("key", ui::hash(agent.key.to_hex()))); |
|
| 101 | + | let until = d.expires.map(|e| format!(" until {}", super::format::when(e))).unwrap_or_default(); |
|
| 102 | + | println!("{}", ui::kv("delegated", format!("{}{until}", d.scope.names().join(",")))); |
|
| 103 | + | println!(" {} {} {}", ui::note(), ui::dim("connect its client with"), ui::id(format!("spectra mcp --agent {}", a.name))); |
|
| 99 | 104 | Ok(0) |
|
| 100 | 105 | } |
|
| 101 | 106 | ||
| … | 23 unchanged lines |
||
| 125 | 130 | return Ok(0); |
|
| 126 | 131 | } |
|
| 127 | 132 | if out.is_empty() { |
|
| 128 | - | println!("No agents yet: {}", ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR")); |
|
| 133 | + | println!("{} {}", ui::note(), ui::dim("no agents yet: spectra agent add NAME --kind KIND --project PROJECT --home DIR")); |
|
| 129 | 134 | } |
|
| 130 | 135 | for a in out { |
|
| 131 | 136 | let state = a["state"].as_str().unwrap_or(""); |
|
| 132 | - | let mark = if state == "ok" { ui::ok(ui::glyph("●", "*")) } else { ui::warn(ui::glyph("○", "o")) }; |
|
| 133 | - | let until = a["delegation_expires"].as_u64().map(|e| format!(" until {}", super::format::when(e))).unwrap_or_default(); |
|
| 137 | + | let until = a["delegation_expires"].as_u64().map(|e| format!("{}until {}", ui::sep(), super::format::when(e))).unwrap_or_default(); |
|
| 138 | + | let display = a["display"].as_str().unwrap_or(""); |
|
| 139 | + | let (name, owner) = display.split_once(" · ").unwrap_or((display, "")); |
|
| 140 | + | println!("{} {}{}{}", marks::health(state), ui::subject(name), ui::sep(), ui::author(owner)); |
|
| 134 | 141 | println!( |
|
| 135 | - | "{mark} {} {} {} {}{until}", |
|
| 136 | - | a["display"].as_str().unwrap_or(""), |
|
| 137 | - | a["kind"].as_str().unwrap_or(""), |
|
| 138 | - | a["project"].as_str().unwrap_or(""), |
|
| 139 | - | ui::hash(&a["key"].as_str().unwrap_or("")[..12]) |
|
| 142 | + | " {}", |
|
| 143 | + | ui::dim(format!( |
|
| 144 | + | "{}{}{}{}{}{until}", |
|
| 145 | + | a["kind"].as_str().unwrap_or(""), |
|
| 146 | + | ui::sep(), |
|
| 147 | + | a["project"].as_str().unwrap_or(""), |
|
| 148 | + | ui::sep(), |
|
| 149 | + | &a["key"].as_str().unwrap_or("")[..12] |
|
| 150 | + | )) |
|
| 140 | 151 | ); |
|
| 141 | 152 | if state != "ok" { |
|
| 142 | - | println!(" {}", ui::warn(state)); |
|
| 153 | + | println!(" {} {}", ui::caution(), ui::warn(state)); |
|
| 143 | 154 | } |
|
| 144 | 155 | } |
|
| 145 | 156 | Ok(0) |
|
| … | 8 unchanged lines |
||
| 154 | 165 | let owner = KeyPair::load_home(&home)?; |
|
| 155 | 166 | ledger.append_one(signed_event(&owner, "human", "agent.disabled", serde_json::json!({ "name": name }))).await?; |
|
| 156 | 167 | ledger.checkpoint().await?; |
|
| 157 | - | println!("{} {name} is disabled; its key is refused from now on", ui::done(ui::glyph("✔", "ok"))); |
|
| 168 | + | println!("{} {}", ui::done("Disabled"), ui::subject(name)); |
|
| 169 | + | println!(" {} {}", ui::note(), ui::dim("its key is refused from now on; earlier events keep their signatures")); |
|
| 158 | 170 | Ok(0) |
|
| 159 | 171 | } |
|
| 160 | 172 | ||
| … | 6 unchanged lines |
modified src/cli/identity.rs +37 −11
| … | 46 unchanged lines |
||
| 47 | 47 | data.join("spectra.toml"), |
|
| 48 | 48 | format!("# Spectra's configuration (spec §22.1).\n\n[owner]\nkey = \"{}\"\nhome = {}\n", owner.key.to_hex(), toml_str(&home_s)), |
|
| 49 | 49 | )?; |
|
| 50 | - | println!("{} Spectra initialised in {}", ui::done(ui::glyph("✔", "ok")), ui::path(data.display())); |
|
| 51 | - | println!("{}", ui::kv("owner", format!("{} {}", owner.identity.name, ui::hash(owner.key.short(12))))); |
|
| 52 | - | println!("{}", ui::kv("instance", format!("{} {}", name, ui::hash(instance.key.short(12))))); |
|
| 50 | + | println!("{} {} {}", ui::done("Initialised"), ui::subject("Spectra"), ui::path(data.display())); |
|
| 51 | + | println!("{}", ui::kv("owner", format!("{}{}{}", ui::author(&owner.identity.name), ui::sep(), ui::hash(owner.key.short(12))))); |
|
| 52 | + | println!("{}", ui::kv("instance", format!("{}{}{}", name, ui::sep(), ui::hash(instance.key.short(12))))); |
|
| 53 | + | println!("{}", ui::kv("ledger", format!("{}{}", ui::path(db.display()), ui::dim(" (verified with spectra admin verify)")))); |
|
| 53 | 54 | println!(); |
|
| 54 | - | println!("So that checkpoints are verifiably from your machine, delegate to the instance key:"); |
|
| 55 | + | println!(" {} {}", ui::note(), ui::dim("so that checkpoints are verifiably from your machine, delegate to the instance key:")); |
|
| 55 | 56 | println!( |
|
| 56 | - | " {}", |
|
| 57 | + | " {}", |
|
| 57 | 58 | ui::id(format!("oxy identity delegate add {} --scope changes", data.join("keys").join("instance.identity").display())) |
|
| 58 | 59 | ); |
|
| 59 | - | println!("Then add agents with {}.", ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR")); |
|
| 60 | + | println!( |
|
| 61 | + | " {} {} {}", |
|
| 62 | + | ui::note(), |
|
| 63 | + | ui::dim("then add agents with"), |
|
| 64 | + | ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR") |
|
| 65 | + | ); |
|
| 60 | 66 | Ok(0) |
|
| 61 | 67 | } |
|
| 62 | 68 | ||
| … | 14 unchanged lines |
||
| 77 | 83 | ); |
|
| 78 | 84 | return Ok(0); |
|
| 79 | 85 | } |
|
| 80 | - | println!("{}", ui::kv("owner", format!("{} <{}> {}", owner.name, owner.email, ui::hash(owner.hash().to_hex())))); |
|
| 86 | + | println!("{}", ui::section("Owner")); |
|
| 87 | + | println!("{}", ui::kv("name", ui::author(format!("{} <{}>", owner.name, owner.email)))); |
|
| 88 | + | println!("{}", ui::kv("key", ui::hash(owner.hash().to_hex()))); |
|
| 89 | + | println!("{}", ui::kv("delegates", owner.delegates.len())); |
|
| 81 | 90 | if let Some(i) = instance { |
|
| 82 | - | let note = if delegated { ui::ok("delegated by the owner") } else { ui::warn("not delegated by the owner yet") }; |
|
| 83 | - | println!("{}", ui::kv("instance", format!("{} {} {}", i.name, ui::hash(i.hash().short(12)), note))); |
|
| 91 | + | println!("{}", ui::section("Instance")); |
|
| 92 | + | println!("{}", ui::kv("name", &i.name)); |
|
| 93 | + | println!("{}", ui::kv("key", ui::hash(i.hash().to_hex()))); |
|
| 94 | + | if delegated { |
|
| 95 | + | println!("{}", ui::kv("status", ui::ok("delegated by the owner: checkpoints are verifiably yours"))); |
|
| 96 | + | } else { |
|
| 97 | + | println!("{}", ui::kv("status", ui::warn("not delegated by the owner yet"))); |
|
| 98 | + | println!( |
|
| 99 | + | " {} {}", |
|
| 100 | + | ui::note(), |
|
| 101 | + | ui::id(format!("oxy identity delegate add {} --scope changes", ctx.data.join("keys").join("instance.identity").display())) |
|
| 102 | + | ); |
|
| 103 | + | } |
|
| 84 | 104 | } |
|
| 85 | 105 | Ok(0) |
|
| 86 | 106 | } |
|
| … | 5 unchanged lines |
||
| 92 | 112 | let outcome = refresh_owner(&ledger, &owner_home(ctx, owner_home_flag)).await?; |
|
| 93 | 113 | ledger.checkpoint().await?; |
|
| 94 | 114 | match outcome { |
|
| 95 | - | Some(id) => println!("{} recorded the owner's current identity {}", ui::done(ui::glyph("✔", "ok")), ui::hash(id.hash().short(12))), |
|
| 96 | - | None => println!("{} the owner's identity is current", ui::done(ui::glyph("✔", "ok"))), |
|
| 115 | + | Some(id) => println!( |
|
| 116 | + | "{} {}{}{}", |
|
| 117 | + | ui::done("Recorded"), |
|
| 118 | + | ui::subject("the owner's current identity"), |
|
| 119 | + | ui::sep(), |
|
| 120 | + | ui::hash(id.hash().short(12)) |
|
| 121 | + | ), |
|
| 122 | + | None => println!("{} {}", ui::done("Current"), ui::subject("the owner's identity has not moved")), |
|
| 97 | 123 | } |
|
| 98 | 124 | Ok(0) |
|
| 99 | 125 | } |
|
| … | 46 unchanged lines |
modified src/cli/mod.rs +1 −0
| … | 6 unchanged lines |
||
| 7 | 7 | pub mod format; |
|
| 8 | 8 | pub mod identity; |
|
| 9 | 9 | pub mod json; |
|
| 10 | + | pub mod marks; |
|
| 10 | 11 | pub mod ui; |
|
| 11 | 12 | ||
| 12 | 13 | use anyhow::{bail, Result}; |
|
| … | 211 unchanged lines |
modified tests/cli_identity.rs +6 −1
| … | 70 unchanged lines |
||
| 71 | 71 | delegate(&d.join("owner"), &agent.identity); |
|
| 72 | 72 | let o = run(d, &args); |
|
| 73 | 73 | assert!(o.status.success(), "{}", text(&o)); |
|
| 74 | - | assert!(text(&o).contains("claude-oxygen · Andy McCall's agent"), "{}", text(&o)); |
|
| 74 | + | assert!(text(&o).contains("Added claude-oxygen") && text(&o).contains("Andy McCall's agent"), "{}", text(&o)); |
|
| 75 | 75 | assert_eq!(KeyPair::load_home(&home).unwrap().key, agent.key, "the key made first is the key recorded"); |
|
| 76 | 76 | ||
| 77 | 77 | let list: serde_json::Value = serde_json::from_slice(&run(d, &["agent", "list", "--json"]).stdout).unwrap(); |
|
| … | 6 unchanged lines |
||
| 84 | 84 | assert!(list[0]["state"].as_str().unwrap().contains("disabled"), "{list}"); |
|
| 85 | 85 | verify_clean(d); |
|
| 86 | 86 | assert!(run(d, &["admin", "check"]).status.success()); |
|
| 87 | + | ||
| 88 | + | // Piped, as here, output is plain like oxy's: no colour codes, no glyphs. |
|
| 89 | + | let o = run(d, &["agent", "list"]); |
|
| 90 | + | let t = text(&o); |
|
| 91 | + | assert!(!t.contains('\x1b') && !t.contains('✔'), "{t}"); |
|
| 87 | 92 | } |
|
| 88 | 93 | ||
| 89 | 94 | #[test] |
|
| … | 20 unchanged lines |