@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 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
+ });