@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 +21 -0
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/web/kiosk-vendor.js +24 -5
- package/web/kiosk-vendor.test.js +11 -2
- package/web/kiosk.js +23 -3
- package/web/kiosk.test.js +15 -1
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.
|
|
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.",
|
package/web/kiosk-vendor.js
CHANGED
|
@@ -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
|
|
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)
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
package/web/kiosk-vendor.test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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.
|