@intentius/behold 0.29.15 → 0.29.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,27 @@ substrate in one graph, coloured by drift. Then act through delegated, gated Ops
6
6
  📖 **[Documentation](https://intentius.io/behold/)** starts with an agent prompt
7
7
  that gets you to a running graph without reading anything else first.
8
8
 
9
+ Paste it into Claude Code, or any coding agent with a shell, from anywhere:
10
+
11
+ ```text
12
+ Clone https://github.com/INTENTIUS/behold and get me looking at a live
13
+ infrastructure graph in my browser.
14
+
15
+ I want the bundled demo. It needs Docker and no cloud account. Install what
16
+ it needs and start it, then tell me the URL.
17
+
18
+ Walk me through what I am looking at: what the colours mean, and how I make a
19
+ resource change colour while I watch.
20
+
21
+ When Docker is not running, say so. Show me the source graph on its own
22
+ instead.
23
+
24
+ For anything past the demo, read https://intentius.io/behold/llms.txt and
25
+ https://intentius.io/behold/using/agents/.
26
+
27
+ Never deploy with the Deploy button, `POST /api/apply` or `POST /api/ci/dispatch`. Gate approvals stay mine as well (`/api/ops/:name/signal/:gate`, `/api/workspace/gates/approve`, `/api/operator/approve/:op/:gate`). Op runs (`/api/ops/:name/run`) and rollbacks (`/api/rollback`) are mine too. Leave `--auto-sync` off, and keep a `behold preview` or `--public` serve read-only.
28
+ ```
29
+
9
30
  Where Argo CD shows one cluster's tree, behold shows the mixed-substrate estate:
10
31
  cloud drift on AWS, supply-chain drift on GitHub Actions, artifact presence on
11
32
  Helm. Each substrate's own kind of truth, in one picture.
package/dist/cli.js CHANGED
@@ -24594,7 +24594,7 @@ function kioskRoute(app, webRoot2, enabled) {
24594
24594
  return new Response(readFileSync29(join40(webRoot2, "kiosk.html"), "utf8"), { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
24595
24595
  });
24596
24596
  }
24597
- var VENDOR_TOOLS = ["terraform", "terragrunt", "cdk-terrain", "choudoufu"];
24597
+ var VENDOR_TOOLS = ["terraform", "terragrunt", "cdk-terrain", "choudoufu", "chant"];
24598
24598
  var isVendorTool = (v) => VENDOR_TOOLS.includes(v);
24599
24599
  var cycleKey = (tool) => `cycles/${tool}/latest.json`;
24600
24600
  var CYCLE_READ_TTL_MS = 1e4;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/behold",
3
- "version": "0.29.15",
3
+ "version": "0.29.16",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
@@ -16,7 +16,11 @@
16
16
  * null, the name as text), and the footer's credit. `terms` are the words
17
17
  * that name the tool or its owner. `kind` is what it competes as (see
18
18
  * COMPETES): an `engine` plans and applies, an `orchestrator` drives an
19
- * engine. Keep the keys in step with VENDOR_TOOLS in src/terragucci-demo.ts.
19
+ * engine, and a `pipeline` plans and applies a pull request's change with an
20
+ * engine. `reports: false` marks a tool whose pipeline writes no terragucci
21
+ * reports or run view: its page draws the checkout behold serves for its repo
22
+ * (kiosk.js `frameOk`). Keep the keys in step with VENDOR_TOOLS in
23
+ * src/terragucci-demo.ts.
20
24
  *
21
25
  * - Terraform: HashiCorp's rules allow the name as text, with the mark noted.
22
26
  * - Terragrunt: Gruntwork publishes logo files, but a logo on a page like
@@ -24,6 +28,8 @@
24
28
  * - CDK Terrain: the Open Construct Foundation's continuation of CDKTF; its
25
29
  * site, cdktn.io, publishes no rules for its logo, so the name is text.
26
30
  * - choudoufu: ours, logo and all.
31
+ * - chant: ours too. Its docs ship no logo, so the name is text. Its scenario
32
+ * runs chant's own pull-request pipeline, which installs OpenTofu.
27
33
  */
28
34
  export const VENDORS = {
29
35
  terraform: {
@@ -67,6 +73,17 @@ export const VENDORS = {
67
73
  terms: ["choudoufu"],
68
74
  runsWith: [],
69
75
  },
76
+ chant: {
77
+ name: "chant",
78
+ owner: "Intentius",
79
+ url: "https://intentius.io/chant/",
80
+ logo: null,
81
+ credit: "chant is made by Intentius, who also make this demo.",
82
+ kind: "pipeline",
83
+ terms: ["chant"],
84
+ runsWith: ["opentofu"],
85
+ reports: false,
86
+ },
70
87
  };
71
88
 
72
89
  /** What a tool's scenarios run on, said on its page as the owner's rules allow ("works with OpenTofu"). */
@@ -92,11 +109,13 @@ export const COMPETITORS = {
92
109
  /**
93
110
  * What competes with what. Engines compete with engines (Terraform, OpenTofu,
94
111
  * choudoufu); orchestrators with orchestrators (Terragrunt, CDK Terrain,
95
- * Atmos, Terramate). An orchestrator runs on an engine, so its page names the
96
- * engine freely; an engine's page names no other engine, but for the one its
97
- * scenarios really run with (`runsWith`).
112
+ * Atmos, Terramate); pipelines with pipelines (chant, and none other has a
113
+ * page). An orchestrator runs on an engine, so its page names the engine
114
+ * freely; an engine's page names no other engine, but for the one its
115
+ * scenarios really run with (`runsWith`). chant is a pipeline, so the engine
116
+ * and orchestrator pages keep the `chant approve` lines their runs print.
98
117
  */
99
- export const COMPETES = { engine: "engine", orchestrator: "orchestrator" };
118
+ export const COMPETES = { engine: "engine", orchestrator: "orchestrator", pipeline: "pipeline" };
100
119
 
101
120
  const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
102
121
 
@@ -147,8 +147,8 @@ describe("the branding table (behold#571)", () => {
147
147
  const v = VENDORS[tool];
148
148
  const words = [v.name, ...creditLines(tool).map((l) => l.map((s) => s.text).join("")), ...Object.values(notRunWords(v)), legendWords("components", tool), legendWords("resources", tool)];
149
149
  for (const w of words) expect(namesOther(w, tool), `${tool}: ${w}`).toBe(false);
150
- // choudoufu is ours: its credit says so instead.
151
- if (tool !== "choudoufu") expect(v.credit).toMatch(/(does not|don't) endorse this demo\./);
150
+ // choudoufu and chant are ours: their credits say so instead.
151
+ if (tool !== "choudoufu" && tool !== "chant") expect(v.credit).toMatch(/(does not|don't) endorse this demo\./);
152
152
  }
153
153
  expect(VENDORS.choudoufu.credit).toMatch(/AWS and Kubernetes/);
154
154
  expect(VENDORS.choudoufu.credit).not.toMatch(/experimental/i);
@@ -170,9 +170,18 @@ describe("the branding table (behold#571)", () => {
170
170
  terragrunt: ["CDK Terrain", "CDKTN", "Open Construct", "Atmos", "Cloud Posse", "Terramate"],
171
171
  "cdk-terrain": ["Terragrunt", "Gruntwork", "Atmos", "Cloud Posse", "Terramate"],
172
172
  choudoufu: ["Terraform", "HashiCorp", "OpenTofu"],
173
+ // A pipeline: no other tool with a page is one.
174
+ chant: [],
173
175
  });
174
176
  expect(otherTerms(null)).toEqual([]);
175
177
  });
178
+ it("keeps the chant approve lines on every page, since chant is a pipeline and competes with no engine or orchestrator", () => {
179
+ for (const tool of VENDOR_TOOLS) expect(namesOther("chant approve tf-apply wave-2 --actor demo-reviewer --sign", tool), tool).toBe(false);
180
+ expect(VENDORS.chant.url).toBe("https://intentius.io/chant/");
181
+ expect(VENDORS.chant.owner).toBe("Intentius");
182
+ expect(creditLines("chant").flat().filter((s) => s.url).map((s) => s.text)).toEqual(["chant", "OpenTofu"]);
183
+ expect(toolCheckout("chant")).toBe("demo-chant");
184
+ });
176
185
  it("lets an orchestrator's page name the engines it runs on, and keeps an engine's page from naming another engine", () => {
177
186
  expect(namesOther("Big Terraform setups run on OpenTofu", "terragrunt")).toBe(false);
178
187
  expect(namesOther("Big Terraform setups", "cdk-terrain")).toBe(false);
package/web/kiosk.js CHANGED
@@ -412,6 +412,12 @@ export function kioskView(demo, { components = false, pick = null, now = Date.no
412
412
  export function legendWords(view, tool = null) {
413
413
  const root = rootWord(tool);
414
414
  const n = rootNoun(tool);
415
+ // A tool whose pipeline writes no reports: behold's own graph of its components, boxed by the wave each applies in.
416
+ if (reportless(vendorOf(tool))) {
417
+ return view === "components"
418
+ ? `Components shows the project's components, in a box per wave. Each card is ${root} with its own state, and an arrow is one component reading another's outputs.`
419
+ : `Each box is ${root} with its own state.`;
420
+ }
415
421
  const rest = `an arrow is one ${n.one} reading another's outputs, dashed while it reads planned values. A wave applies its ${n.many} together, after the waves it reads.`;
416
422
  if (view === "components") {
417
423
  return `Components shows the pipeline's jobs and waves. Each card is ${root} with its own state, in a box per wave titled with the job that applies it; ${rest}`;
@@ -420,6 +426,16 @@ export function legendWords(view, tool = null) {
420
426
  }
421
427
 
422
428
 
429
+ /** Whether a tool's pipeline writes no terragucci reports or run view (VENDORS' `reports: false`). */
430
+ export function reportless(vendor) {
431
+ return !!vendor && vendor.reports === false;
432
+ }
433
+
434
+ /** Whether /api/project's answer is a chant project: behold draws its components itself. */
435
+ export function chantProject(p) {
436
+ return !!p && Array.isArray(p.memberKinds) && p.memberKinds.includes("chant");
437
+ }
438
+
423
439
  /** The tool the page shows: its own on a tool's page, else the running scenario's (demo.json `tool`). */
424
440
  export function nounTool(s) {
425
441
  if (s.vendor) return s.vendor;
@@ -593,7 +609,7 @@ function drawBar(bar, s, now, foot) {
593
609
  then.append(`then comments `, el("code", "k-inline", appr.comment), ` on ${appr.on}, and the waves run again.`);
594
610
  a.appendChild(then);
595
611
  holdBox.appendChild(a);
596
- } else if (!s.noReports) {
612
+ } else if (!s.noReports && !reportless(vendor || vendorOf(demo.tool))) {
597
613
  const panel = runPanel(demo, s.answer, s.progress);
598
614
  if (panel.note) holdBox.appendChild(elRoots("p", "k-note", panel.note, s));
599
615
  else if (panel.stage === "pull-request") for (const u of unreviewedLines(s.answer)) holdBox.appendChild(elRoots("p", "k-note k-unknown", u, s));
@@ -744,7 +760,10 @@ export function startKiosk({ bar, frame, foot, side, main, views, tools = null }
744
760
  if (!vendor) return true;
745
761
  // Before the tool's first cycle: its repo's checkout, when this behold serves it.
746
762
  if (s.mode === "none") return !!notRunRepo(tool, s.viewRepos) && s.aimed === notRunRepo(tool, s.viewRepos);
747
- return servesRepo(s.answer && !s.answer.error ? s.answer.project : null, s.repo);
763
+ if (servesRepo(s.answer && !s.answer.error ? s.answer.project : null, s.repo)) return true;
764
+ // A tool whose pipeline writes no terragucci reports (chant's own loop) has no
765
+ // reports to name its repo: the frame is its repo's when behold serves that checkout.
766
+ return reportless(vendor) && !!repoParam(s.repo, s.viewRepos);
748
767
  };
749
768
  // behold#571: a behold serving several repos (`serve --repos`) answers the
750
769
  // graph's reads from the checkout of the repo the page is about, asked by
@@ -1066,7 +1085,8 @@ export function startKiosk({ bar, frame, foot, side, main, views, tools = null }
1066
1085
  s.viewCommits = p.viewCommits || null;
1067
1086
  if (s.aimed === null && p.viewRepo) s.aimed = p.viewRepo;
1068
1087
  }
1069
- s.components = !!(p && !p.error && p.terragucciComponents);
1088
+ // A chant project's graph is its components (web/app.js bootZoom), so it has that view too.
1089
+ s.components = !!(p && !p.error && (p.terragucciComponents || chantProject(p)));
1070
1090
  // behold#571: the declared roots (their colours' order) and the wave jobs that apply them.
1071
1091
  if (p && p.terragucciRoots) {
1072
1092
  s.roots = Array.isArray(p.terragucciRoots.roots) ? p.terragucciRoots.roots : [];
package/web/kiosk.test.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { INTRO_HEAD, STEP_VIEW, STEP_WORDS, aboutRows, approvalPanel, approveGloss, focusRoots, introText, scenarioHead, frameSrc, factLines, kioskView, legendWords, matchingPull, pendingApproval, renderKiosk, renderViewToggle, runPanel, runPin, stepView, viewZoom } from "./kiosk.js";
2
+ import { INTRO_HEAD, STEP_VIEW, STEP_WORDS, aboutRows, approvalPanel, approveGloss, chantProject, focusRoots, introText, scenarioHead, frameSrc, factLines, kioskView, legendWords, matchingPull, pendingApproval, renderKiosk, renderViewToggle, reportless, runPanel, runPin, stepView, viewZoom } from "./kiosk.js";
3
+ import { VENDORS } from "./kiosk-vendor.js";
3
4
  import { PICK_HOLD_MS } from "./kiosk-runs.js";
4
5
 
5
6
  const demo = (step, extra = {}) => ({ cycle: 1, scenario: "a-output", title: "t", step, label: "", started_at: "2026-10-15T03:00:00Z", next: null, pull: 7, commit: "abc12345", ...extra });
@@ -253,6 +254,19 @@ describe("which view the frame shows (#571)", () => {
253
254
  expect(words).toMatch(/one unit reading another's outputs.*applies its units together/);
254
255
  expect(words).not.toMatch(/\broots?\b/);
255
256
  }
257
+ // A tool whose pipeline writes no reports: behold's own components graph, with no jobs or planned values in it.
258
+ expect(legendWords("components", "chant")).toBe(
259
+ "Components shows the project's components, in a box per wave. Each card is a Terraform root with its own state, and an arrow is one component reading another's outputs.",
260
+ );
261
+ expect(legendWords("resources", "chant")).toBe("Each box is a Terraform root with its own state.");
262
+ });
263
+
264
+ it("knows a tool whose pipeline writes no terragucci reports, and a chant project behold draws itself", () => {
265
+ expect(Object.entries(VENDORS).filter(([, v]) => reportless(v)).map(([id]) => id)).toEqual(["chant"]);
266
+ expect(reportless(null)).toBe(false);
267
+ expect(chantProject({ memberKinds: ["chant"] })).toBe(true);
268
+ expect(chantProject({ memberKinds: ["terraform"] })).toBe(false);
269
+ expect(chantProject({ error: "nope" })).toBe(false);
256
270
  });
257
271
 
258
272
  // A DOM for the toggle: elements with attributes and click listeners.