@intentius/behold 0.2.2
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 +338 -0
- package/bin/behold.js +15 -0
- package/dist/cli.js +4830 -0
- package/package.json +48 -0
- package/web/app.js +2352 -0
- package/web/index.html +248 -0
- package/web/theme.js +188 -0
- package/web/themes.js +559 -0
package/README.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# behold
|
|
2
|
+
|
|
3
|
+
**A live control plane on [chant](https://github.com/INTENTIUS/chant).** Behold your
|
|
4
|
+
whole estate β every substrate (AWS, k8s, GCP, Azure, Fly) in one graph, coloured by
|
|
5
|
+
drift β then act through delegated, gated Ops.
|
|
6
|
+
|
|
7
|
+
π **[Documentation](https://intentius.io/behold/)** β starts with an agent prompt that
|
|
8
|
+
gets you to a running graph without reading anything else first.
|
|
9
|
+
|
|
10
|
+
Where Argo CD shows one cluster's tree, behold shows the mixed-substrate estate:
|
|
11
|
+
cloud drift on AWS, supply-chain drift on GitHub Actions, artifact presence on Helm β
|
|
12
|
+
each substrate's own kind of truth, in one picture.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
chant source ββbuild/lintβββΆ graph IR ββbeholdβββΆ live graph + drift + (delegated) actions
|
|
16
|
+
(deterministic) (server + browser)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Preview: your project, or the Loom-on-Floci demo (v0.1.0)
|
|
20
|
+
|
|
21
|
+
`behold preview` is the quick way to look at a chant project's graph in a
|
|
22
|
+
browser at one port. Plain, it just opens the project you point it at β no env,
|
|
23
|
+
no emulator:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install
|
|
27
|
+
npm run dev -- preview # β http://localhost:4600, cwd as the project
|
|
28
|
+
npm run dev -- preview ../my-project # β someone else's project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
With no path, it opens the **current directory** β run it from inside your chant
|
|
32
|
+
project. Pass a path to look at another one.
|
|
33
|
+
|
|
34
|
+
behold also ships a turnkey **demo**: the whole live experience running Loom on a
|
|
35
|
+
local [Floci](https://github.com/lex00/floci) emulator, no cloud account and
|
|
36
|
+
nothing to configure. Opt in with `--emulator`:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm run dev -- preview ../loomster --emulator # β http://localhost:4600
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--emulator` injects the env Loom's own Floci setup expects
|
|
43
|
+
(`AWS_ENDPOINT_URL=http://localhost:4566`, dummy AWS creds, `LOOM_ENV=local`) and
|
|
44
|
+
locks the UI into previewMode (git/PR ops hidden, substrate strip scoped to
|
|
45
|
+
Docker+Floci, no arbitrary-project switching). If Floci isn't up, **Bring up** on
|
|
46
|
+
its substrate pill boots the emulator and deploys Loom. Needs Docker.
|
|
47
|
+
|
|
48
|
+
**What you can do (with `--emulator`):** explore the graph at every **zoom**
|
|
49
|
+
(components β logical β composites β resources β attributes, with an optional
|
|
50
|
+
radial layout β where _logical_ is a traditional AWS architecture diagram: nested
|
|
51
|
+
VPC/subnet β component boxes, CIDRs as labels, one headline resource per
|
|
52
|
+
composite), watch live per-component status, read the reconcile plan, inspect any
|
|
53
|
+
node, and **deploy to the emulator** β the full observe β reconcile β apply,
|
|
54
|
+
Apply all, and a one-click Reset.
|
|
55
|
+
|
|
56
|
+
**Not yet** (this demo is a preview of what's coming): any real cloud, and the
|
|
57
|
+
git/PR actions (Rollback, Sync, Adopt) β `--emulator` is Loom-on-Floci only. To
|
|
58
|
+
look at your own real infra, `preview`/`export` without `--emulator`, or `serve`
|
|
59
|
+
with your own `--env` and creds.
|
|
60
|
+
|
|
61
|
+
## Export & host β a shareable, interactive snapshot
|
|
62
|
+
|
|
63
|
+
`behold export` freezes whatever estate you're looking at into a **self-contained
|
|
64
|
+
static folder** that any static host can serve β a read-only but fully
|
|
65
|
+
interactive snapshot. Pan/zoom, the zoom dial (components β logical β composites β
|
|
66
|
+
resources β attributes), radial layout, the inspect pane, and the env/tier
|
|
67
|
+
pickers all work client-side; there's no live observe or deploy.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm run dev -- export --out ./behold-export # defaults to cwd, like preview
|
|
71
|
+
# or someone else's project, with its live overlay:
|
|
72
|
+
# npm run dev -- export <project> --env <name> --out ./behold-export
|
|
73
|
+
# or the turnkey Loom-on-Floci demo:
|
|
74
|
+
# npm run dev -- export ../loomster --emulator --out ./behold-export
|
|
75
|
+
npx serve ./behold-export # β open it, no backend running
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Like `preview`, `export` defaults to the current directory and stays plain (no
|
|
79
|
+
env, no emulator) unless you ask. `--env <name>` turns on that project's live
|
|
80
|
+
overlay for the snapshot; `--emulator` injects the same turnkey Loom-on-Floci env
|
|
81
|
+
as `preview --emulator`, for exporting that demo.
|
|
82
|
+
|
|
83
|
+
It captures every read endpoint for the whole lens matrix (each env/tier Γ zoom Γ
|
|
84
|
+
radial) in-process β the exact same handlers the live server runs, so a snapshot
|
|
85
|
+
is byte-identical to live. (The live app can't run on a Worker β it needs Docker +
|
|
86
|
+
Floci + a chant subprocess β but the pre-baked export can.)
|
|
87
|
+
|
|
88
|
+
The bundle is **deploy-ready for Cloudflare** β `behold export` writes an
|
|
89
|
+
assets-only `wrangler.jsonc` (no server code, pure static), so:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
cd ./behold-export && npx wrangler deploy # β https://<name>.<account>.workers.dev
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Set the Worker name with `--name`, or edit `wrangler.jsonc`. Auth via
|
|
96
|
+
`wrangler login` or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` (same as any
|
|
97
|
+
Workers deploy). Any other static host works too β GitHub Pages, S3, nginx, or
|
|
98
|
+
Cloudflare Pages (`wrangler pages deploy .`).
|
|
99
|
+
|
|
100
|
+
## Try it β your first apply, no cloud account
|
|
101
|
+
|
|
102
|
+
The bundled `example-writes` is one S3 bucket. `serve --local` boots *that
|
|
103
|
+
project's own* local emulator (Floci, via Docker, generically through
|
|
104
|
+
`chant emulator up` β chant #920), points behold's live overlay at it, and gives
|
|
105
|
+
you a **βΆ Deploy (floci-apply)** button in the header that deploys to it β no AWS
|
|
106
|
+
account, no creds, no cost. This is `serve`'s generic mechanism, separate from `preview`/`export`'s
|
|
107
|
+
`--emulator` flag above, which is a Loom-specific turnkey demo path β see
|
|
108
|
+
`behold --help` for how the two relate:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm install
|
|
112
|
+
npm run demo # installs example-writes' deps, then serves it with --local
|
|
113
|
+
# β http://localhost:4600
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
<sub>(or by hand: `npm install --prefix example-writes && npm run dev -- serve example-writes --local --env prod`)</sub>
|
|
117
|
+
|
|
118
|
+
1. The graph shows the bucket + its TLS policy β **blue** (declared, not yet deployed).
|
|
119
|
+
2. Click **βΆ Deploy (floci-apply)** in the header (or βK β "Deploy: Sync"). The
|
|
120
|
+
now-line streams Build β Apply β Verify; the bucket is created in the emulator
|
|
121
|
+
via the CloudFormation API.
|
|
122
|
+
3. The nodes flip **green (managed)** β behold's overlay observes the live emulator.
|
|
123
|
+
|
|
124
|
+
No Docker running? behold still serves the source graph and tells you to start it β
|
|
125
|
+
it never dies on you.
|
|
126
|
+
|
|
127
|
+
**Real AWS.** The same project's **βΆ Deploy** button starts its `ApplyOp` against a
|
|
128
|
+
real account: `npm run dev -- serve example-writes --env prod` (needs AWS
|
|
129
|
+
credentials). What the header offers depends on what the project committed: a
|
|
130
|
+
committed `ApplyOp` gets the **βΆ Deploy (<op>)** button (plus **Approve** when
|
|
131
|
+
gated); a project with only components gets **βΆ Deployβ¦**, which opens the dial's
|
|
132
|
+
component picker; **Adopt** appears per foreign node (`ReconcileOp`); every other
|
|
133
|
+
Op runs from βK (**Run: \<name\>**). Full walkthrough:
|
|
134
|
+
**[example-writes/README.md](example-writes/README.md)**.
|
|
135
|
+
|
|
136
|
+
## The k3d demo β the Kubernetes counterpart to Loom-on-Floci
|
|
137
|
+
|
|
138
|
+
`npm run demo:k8s` is the k8s analogue of the Floci demo above: it brings up a
|
|
139
|
+
local, single-node [k3d](https://k3d.io) cluster (Docker only, no cloud
|
|
140
|
+
account), then serves the bundled `example-k8s` β an nginx Deployment +
|
|
141
|
+
Service β with `--local`. Same mechanism, same shape: declared-not-deployed
|
|
142
|
+
(blue) β click **Run** on `k3d-apply` β managed (green), server-side applied
|
|
143
|
+
with chant's own field manager. Ctrl-C tears the cluster back down.
|
|
144
|
+
|
|
145
|
+
Beyond the AWS demo's single flip, this one also demonstrates Kubernetes'
|
|
146
|
+
two additional tiers (epic #84): zoom into the Deployment to see its **Pods**
|
|
147
|
+
as **runtime children** (owned by the cluster, never declared, never drift);
|
|
148
|
+
induce an out-of-band `kubectl scale`/`kubectl label` and refresh to see
|
|
149
|
+
**managed-fields drift** (chant's field manager vs. a competing one); and
|
|
150
|
+
switch away from the bound kubectl context to see an **unobserved** refusal
|
|
151
|
+
(an honest "did not look," never a false "all gone"). Full walkthrough,
|
|
152
|
+
including the exact commands and what each state looks like over the API:
|
|
153
|
+
**[example-k8s/README.md](example-k8s/README.md)**.
|
|
154
|
+
|
|
155
|
+
## Read-only core, delegated gated writes (the invariant)
|
|
156
|
+
|
|
157
|
+
**behold never mutates anything itself.**
|
|
158
|
+
|
|
159
|
+
- The vizes only read (`chant graph`, snapshots, Temporal history).
|
|
160
|
+
- Actions don't mutate directly. Sync starts your `ApplyOp`; Adopt starts your
|
|
161
|
+
`ReconcileOp` (opens a PR a human merges). behold *triggers* Ops you committed,
|
|
162
|
+
running on your executor β it holds no apply creds.
|
|
163
|
+
- Two write gestures, both human-confirmed: **Apply** (gate signal) and **Open PR**
|
|
164
|
+
(merge). Authority stays in your source and your worker, never in behold.
|
|
165
|
+
- The first product is **read-only, full stop**: the mixed graph + drift + source
|
|
166
|
+
deep-links. Writes are a later, opt-in layer.
|
|
167
|
+
|
|
168
|
+
## Why a Node service (not an edge function)
|
|
169
|
+
|
|
170
|
+
The live path (`chant graph --live --overlay`, `chant lifecycle plan`) shells
|
|
171
|
+
`kubectl`/`aws`/`az`/the Temporal client and holds cloud creds. That needs a real
|
|
172
|
+
process, so behold is a Node service you run where your creds live β like
|
|
173
|
+
`argocd-server`, not like a hosted SaaS. Read-only means it only needs **read**
|
|
174
|
+
roles (describe/list), so it's least-privilege to run.
|
|
175
|
+
|
|
176
|
+
## Agent-drivable, on chant's MCP
|
|
177
|
+
|
|
178
|
+
behold is drivable by an agent, and leans on chant's MCP rather than reinventing it:
|
|
179
|
+
|
|
180
|
+
- **Reads** β `lifecycle-diff`, `lifecycle-snapshot`, plus behold's own read API
|
|
181
|
+
(the overlay graph as JSON, blast radius, frame diffs).
|
|
182
|
+
- **Delegated actions** β the writes *are* chant MCP Op tools: `op-run` starts an
|
|
183
|
+
`ApplyOp`/`ReconcileOp`, `op-signal` approves a gate, `op-status`/`op-report`
|
|
184
|
+
watch it. So an agent "syncing prod" is `op-run prod-apply` then
|
|
185
|
+
`op-signal prod-apply approve-apply` β gated, durable, no creds in behold.
|
|
186
|
+
|
|
187
|
+
behold's value over raw MCP is the live spatial + temporal view and the coupling
|
|
188
|
+
between them; the underlying capabilities are chant's, exposed the same way to a
|
|
189
|
+
human and an agent. See [AGENTS.md](./AGENTS.md).
|
|
190
|
+
|
|
191
|
+
## Status
|
|
192
|
+
|
|
193
|
+
Bootstrap. `behold serve <project>` renders the **source** mixed-substrate graph
|
|
194
|
+
(cross-lexicon edges are real today β verified in chant core) in a browser, with
|
|
195
|
+
click-to-inspect and source deep-links.
|
|
196
|
+
|
|
197
|
+
Both read paths work: `/api/graph` (the source mixed-substrate graph) and
|
|
198
|
+
`/api/overlay` (the source-anchored **live drift** graph β declared topology kept,
|
|
199
|
+
nodes coloured managed/foreign/pending; needs chant β₯ 0.18.1 and cloud creds).
|
|
200
|
+
|
|
201
|
+
Not yet: the deployment-lanes timeline and the delegated actions. See the concept
|
|
202
|
+
notes and the issue set below.
|
|
203
|
+
|
|
204
|
+
## Usage
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npm install
|
|
208
|
+
npm run dev -- serve ./path/to/chant-project # source graph (tsx)
|
|
209
|
+
npm run dev -- serve ./path/to/chant-project --env prod # + live drift overlay
|
|
210
|
+
# or, built:
|
|
211
|
+
npm run build && ./bin/behold.js serve ./path/to/chant-project --port 4600
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Then open http://localhost:4600. With `--env`, the SPA shows the live overlay;
|
|
215
|
+
without it, the source graph.
|
|
216
|
+
|
|
217
|
+
**Live updates.** The server watches the served project's source and pushes a
|
|
218
|
+
refresh over SSE (`/api/events`) when a `.ts` file changes β edit your infra, the
|
|
219
|
+
graph updates, no reload. Add `--poll <secs>` (with `--env`) to also re-query live
|
|
220
|
+
drift on an interval and push updates when a node's status changes:
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
behold serve ./infra --env prod --poll 30 # watch source + poll drift every 30s
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
behold shells the **project's own** chant (resolved from the project's
|
|
227
|
+
`node_modules` first), so the project decides the chant version β pin it to
|
|
228
|
+
`@intentius/chant ^0.18.1` or later for the live overlay (`graph --live` observed
|
|
229
|
+
nothing before that fix).
|
|
230
|
+
|
|
231
|
+
## Configuration β `.behold.json`
|
|
232
|
+
|
|
233
|
+
An optional `.behold.json` in the served project's root is **behold's own**
|
|
234
|
+
config β kept separate from `chant.config.ts` so behold's concerns (like the
|
|
235
|
+
tier picker) don't leak into chant's. Today it declares one thing: the
|
|
236
|
+
project's deploy-**tier** axis, a dimension orthogonal to `environment` (chant
|
|
237
|
+
has no native tier concept β it's entirely a project convention, e.g. Loom's
|
|
238
|
+
components branching on an env-conditioned `namingParams.tier`):
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"tiers": {
|
|
243
|
+
"envVar": "LOOM_TIER",
|
|
244
|
+
"values": ["light", "production", "production-ha"]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- `envVar` β the env var name the project's source branches on; behold sets it
|
|
250
|
+
for the chant shell-out whenever a tier is picked (`?tier=` β this var, never
|
|
251
|
+
a chant CLI flag).
|
|
252
|
+
- `values` β the tier picker's options.
|
|
253
|
+
|
|
254
|
+
**No `.behold.json` (or no `tiers` key) β no tier axis:** the picker doesn't
|
|
255
|
+
render and the graph loads with no tier selected β the default for any project
|
|
256
|
+
that doesn't opt in. There's no other tier config surface (not
|
|
257
|
+
`chant.config.ts`, not an env var behold guesses the name of).
|
|
258
|
+
|
|
259
|
+
## Layout
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
src/
|
|
263
|
+
cli.ts serve verb + arg parsing
|
|
264
|
+
server.ts Hono read-only API (/api/graph, /api/overlay) + static SPA
|
|
265
|
+
chant.ts shell-out to the chant bin (graph IR, live/overlay) β reads, never mutates
|
|
266
|
+
render.ts pinhole painter (layoutIr + renderSvg) β IR β SVG
|
|
267
|
+
overlay.ts _status β drift semantics (managed/foreign/pending)
|
|
268
|
+
web/
|
|
269
|
+
index.html SPA shell
|
|
270
|
+
app.js inlines pinhole's SVG + click-inspect by data-node-id
|
|
271
|
+
example/ a tiny AWS chant project for local dev + e2e
|
|
272
|
+
e2e/run.sh end-to-end runner (install example chant β serve β assert the API)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## The painter
|
|
276
|
+
|
|
277
|
+
behold reuses [pinhole](https://github.com/INTENTIUS/pinhole)'s SVG painter as a
|
|
278
|
+
library β a mature renderer (themes, icons, `_status` drift colouring that already
|
|
279
|
+
speaks the overlay vocabulary managed/foreign/pending). The server lays the IR out
|
|
280
|
+
and paints it with `layoutIr` + `renderSvg` (`src/render.ts`); the SPA inlines the
|
|
281
|
+
SVG and wires click-inspect by `data-node-id` against the IR. pinhole's layout is
|
|
282
|
+
dagre β pure JS, no native dependency.
|
|
283
|
+
|
|
284
|
+
## Local development
|
|
285
|
+
|
|
286
|
+
`just` lists everything. The core loop:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
just install # behold's own deps
|
|
290
|
+
just check # tsc + unit tests + build (the fast gate)
|
|
291
|
+
just example-install # install the example project's chant + aws lexicon (once)
|
|
292
|
+
just serve # serve example/ read-only β http://localhost:4600 (source graph)
|
|
293
|
+
just serve example prod # same server, live drift overlay (needs AWS creds)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
One server, one SPA: passing an env turns on the live overlay (`/api/overlay`),
|
|
297
|
+
omitting it shows the source graph (`/api/graph`). `serve` runs via `tsx` (no build
|
|
298
|
+
step); for the built binary, `just build` then `./bin/behold.js serve <project>`.
|
|
299
|
+
|
|
300
|
+
**Which chant runs.** behold does not bundle chant β it *shells* the chant binary
|
|
301
|
+
resolved from the served project's `node_modules` (falling back to behold's own dep).
|
|
302
|
+
So local testing means installing chant into a project, not into behold. The bundled
|
|
303
|
+
`example/` does exactly that; point `serve` at any real chant project the same way.
|
|
304
|
+
|
|
305
|
+
## E2E
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
just e2e
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`e2e/run.sh` installs the example's chant (the **chant install under test**), builds
|
|
312
|
+
behold, serves the example, and asserts the read-only API against a live server. It
|
|
313
|
+
auto-detects AWS credentials:
|
|
314
|
+
|
|
315
|
+
- **no creds** β asserts `/api/graph` (the source mixed-substrate graph, offline).
|
|
316
|
+
- **AWS creds** β asserts `/api/overlay` (the source-anchored live overlay β queries
|
|
317
|
+
CloudFormation and checks every node carries a drift status; all `pending` when
|
|
318
|
+
nothing is deployed is a valid pass, since the point is the live path).
|
|
319
|
+
|
|
320
|
+
It's hermetic apart from the chant install and (optionally) the cloud read; the
|
|
321
|
+
server is torn down on exit. `BEHOLD_E2E_PORT` overrides the port.
|
|
322
|
+
|
|
323
|
+
## Unit tests
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
npm test # vitest β pure units (graphFlags, _status mapping); no server, no cloud
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Related issues
|
|
330
|
+
|
|
331
|
+
- chant **#821** β source-anchored overlay (the linchpin: cross-substrate topology + live
|
|
332
|
+
status) β done, shipped chant 0.18.31; behold adopted it in M4 (see `src/overlay.ts`).
|
|
333
|
+
- chant **#822** β diff two historical snapshots (feeds the timeline).
|
|
334
|
+
- chant **#513** β compose separate stacks into one IR β done; behold's `composeEstate`
|
|
335
|
+
(`src/estate.ts`, #31) consumes pinhole's `composeStacks` built on it.
|
|
336
|
+
- pinhole **#82** β ship the painter as a library (done; behold consumes it).
|
|
337
|
+
- pinhole **#79/#80/#81** β drive `--live`/`--overlay`, first-class drift rendering, morph-over-time.
|
|
338
|
+
- Concept notes: `~/Documents/research/chant-live-control-plane.md`.
|
package/bin/behold.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* behold bin launcher.
|
|
4
|
+
*
|
|
5
|
+
* Checked into git so it exists at npm pack-validation time (before `prepack` /
|
|
6
|
+
* `build` runs). It loads the built dist/cli.js and calls the exported `run()`
|
|
7
|
+
* with the process argv.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import(new URL("../dist/cli.js", import.meta.url).href)
|
|
11
|
+
.then((mod) => mod.run(process.argv.slice(2)))
|
|
12
|
+
.catch((err) => {
|
|
13
|
+
process.stderr.write(`behold: fatal: ${err?.message ?? err}\n`);
|
|
14
|
+
process.exit(3);
|
|
15
|
+
});
|