@agent-surface/cli 0.12.0 → 0.13.0
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 +45 -7
- package/dist/bin.js +15 -7
- package/dist/bin.js.map +1 -1
- package/dist/check-TJIX7NDE.js +241 -0
- package/dist/check-TJIX7NDE.js.map +1 -0
- package/dist/{chunk-QIVOZAWX.js → chunk-3AJ343NA.js} +12 -3
- package/dist/{chunk-QIVOZAWX.js.map → chunk-3AJ343NA.js.map} +1 -1
- package/dist/chunk-IALBMW3R.js +278 -0
- package/dist/chunk-IALBMW3R.js.map +1 -0
- package/dist/chunk-L7GHSC2Z.js +465 -0
- package/dist/chunk-L7GHSC2Z.js.map +1 -0
- package/dist/{chunk-Q5WOLWEW.js → chunk-UGCLJ5JX.js} +26 -286
- package/dist/chunk-UGCLJ5JX.js.map +1 -0
- package/dist/chunk-VX6GBEP3.js +539 -0
- package/dist/chunk-VX6GBEP3.js.map +1 -0
- package/dist/{init-LFQ5R3G7.js → init-6XV64LK2.js} +9 -8
- package/dist/{init-LFQ5R3G7.js.map → init-6XV64LK2.js.map} +1 -1
- package/dist/{ink-P23VKP4H.js → ink-QR7X7TAC.js} +99 -25
- package/dist/ink-QR7X7TAC.js.map +1 -0
- package/dist/inspect-NJ4J3MPP.js +242 -0
- package/dist/inspect-NJ4J3MPP.js.map +1 -0
- package/dist/{snapshot-3E7MVMVG.js → snapshot-ZGTAF2Y5.js} +28 -12
- package/dist/snapshot-ZGTAF2Y5.js.map +1 -0
- package/package.json +4 -4
- package/dist/check-CS4Z3ZK3.js +0 -169
- package/dist/check-CS4Z3ZK3.js.map +0 -1
- package/dist/chunk-DYDSJM7R.js +0 -170
- package/dist/chunk-DYDSJM7R.js.map +0 -1
- package/dist/chunk-Q5WOLWEW.js.map +0 -1
- package/dist/chunk-TPWRSFK7.js +0 -380
- package/dist/chunk-TPWRSFK7.js.map +0 -1
- package/dist/ink-P23VKP4H.js.map +0 -1
- package/dist/inspect-ELUQ73MZ.js +0 -119
- package/dist/inspect-ELUQ73MZ.js.map +0 -1
- package/dist/snapshot-3E7MVMVG.js.map +0 -1
package/README.md
CHANGED
|
@@ -49,10 +49,30 @@ agent-surface check [scenario] # fail drift, gaps, rejections, stale scenar
|
|
|
49
49
|
Every command covers all scenarios in the config unless you name one. `inspect` prints each in turn:
|
|
50
50
|
|
|
51
51
|
```text
|
|
52
|
-
|
|
52
|
+
SURFACE INSPECT
|
|
53
|
+
Config agent-surface.config.tsx
|
|
54
|
+
Depth full — the source is read and every scenario is mounted
|
|
55
|
+
Scope whole surface — no component-type prefix filter
|
|
56
|
+
Scenarios 1 of 2 — admin
|
|
57
|
+
|
|
58
|
+
SURFACE SUMMARY
|
|
59
|
+
Reach 11/11 authored capabilities reached
|
|
60
|
+
Callable 9/11 mounted capabilities are callable in at least one scenario · 2 never callable
|
|
61
|
+
Risk 1 destructive · 1 confirmation-gated · 1 with bound input
|
|
62
|
+
Domain 1 capability reached against the authoritative oRPC manifest
|
|
63
|
+
Catalog every call site read
|
|
64
|
+
Scenarios 1 mounted
|
|
65
|
+
Verdict every authored capability is reached by a scenario
|
|
66
|
+
|
|
67
|
+
STATIC CATALOG
|
|
68
|
+
STATUS COMPLETE — every capability identity resolved
|
|
69
|
+
Capabilities 11 authored (upper bound) · 10 resolved call sites
|
|
70
|
+
Program 21 files analyzed · 40 agent-surface implementation files excluded
|
|
71
|
+
Metadata 5 call sites partially read · identity remains resolved
|
|
72
|
+
Domain 1 manifest capability
|
|
53
73
|
|
|
54
74
|
scenario admin route /devices
|
|
55
|
-
9 callable, 2 visible-disabled, 0 hidden
|
|
75
|
+
9 callable, 2 visible-disabled, 0 hidden · 1 destructive, 1 confirmation-gated
|
|
56
76
|
|
|
57
77
|
CAPABILITY KIND EFFECT STATE FLAGS
|
|
58
78
|
app.navigation.goTo action navigation callable reversible
|
|
@@ -62,13 +82,11 @@ devices.drawer.close action local-state disabled reversible
|
|
|
62
82
|
devices.table.sort action local-state callable idempotent · reversible
|
|
63
83
|
devices.disable procedure destructive disabled confirmation:required · deviceIds bound+locked
|
|
64
84
|
⤷ Select at least one device first
|
|
65
|
-
|
|
66
|
-
11 authored · 11 reached · 0 unreached · 1 scenario (admin)
|
|
67
85
|
```
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
Summaries first, details after. The run header states what every number below it is relative to — the config, the depth, the scope, the scenarios — and prints before the mounts it will spend its time on. Each scenario's own table repeats the qualifier that is local to it: a surface is a projection of one mounted context, never "the app".
|
|
70
88
|
|
|
71
|
-
`--detail`
|
|
89
|
+
`--detail` shows full capability, origin, and diagnostic detail; `--explain` and `--schemas` imply it.
|
|
72
90
|
|
|
73
91
|
### Depth
|
|
74
92
|
|
|
@@ -110,8 +128,28 @@ A route nobody visits never registers, so it is in no snapshot and drifts agains
|
|
|
110
128
|
UNREACHED — authored, and no scenario mounts it (1)
|
|
111
129
|
CAPABILITY ORIGIN
|
|
112
130
|
view:cov.unmounted.toCsv Unmounted.tsx:26
|
|
131
|
+
→ add a scenario that mounts them, delete the dead component, or record the decision in
|
|
132
|
+
.agent-surface/coverage-allow.json
|
|
133
|
+
```
|
|
113
134
|
|
|
114
|
-
|
|
135
|
+
`check` says the same thing as a report — a verdict, the health matrix behind it, one row per scenario, then the findings and the commands that clear them:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
SURFACE CHECK FAIL
|
|
139
|
+
Config agent-surface.config.tsx
|
|
140
|
+
Depth full — the source is read and every scenario is mounted
|
|
141
|
+
Scope whole surface — no component-type prefix filter
|
|
142
|
+
|
|
143
|
+
Coverage FAIL 2/3 authored capabilities reached · 1 unreached
|
|
144
|
+
Catalog PASS all static sites resolved
|
|
145
|
+
Domain PASS 1 manifest capability reached
|
|
146
|
+
Baselines FAIL 1/2 scenario baselines current
|
|
147
|
+
Runtime PASS 2 scenarios mounted
|
|
148
|
+
|
|
149
|
+
SCENARIOS (2)
|
|
150
|
+
SCENARIO ROUTE CALLABLE DISABLED HIDDEN REJECTED BASELINE
|
|
151
|
+
admin /devices 9 2 0 — drift (1)
|
|
152
|
+
anonymous /devices 0 0 11 — current
|
|
115
153
|
```
|
|
116
154
|
|
|
117
155
|
`inspect` reports it and `snapshot` reports it; **`check` fails on it**. It also fails on an unread call site, because the catalog is `unreached`'s denominator and holes in it make that count a floor rather than an answer — pass `--allow-unresolved` to accept that knowingly, which still prints the gap. Adoption ratchets through a committed `.agent-surface/coverage-allow.json`, whose stale entries fail the command.
|
package/dist/bin.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isDepth,
|
|
6
6
|
write,
|
|
7
7
|
writeError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-3AJ343NA.js";
|
|
9
9
|
|
|
10
10
|
// src/bin.ts
|
|
11
11
|
import { realpathSync } from "fs";
|
|
@@ -56,7 +56,7 @@ Options
|
|
|
56
56
|
--config <path> path to agent-surface.config.* (default: nearest, searching upward)
|
|
57
57
|
--baseline-dir where baselines live (default: .agent-surface next to the config)
|
|
58
58
|
--scope <prefix> restrict to a component-type prefix (repeatable)
|
|
59
|
-
--detail
|
|
59
|
+
--detail full capability, origin, and diagnostic detail
|
|
60
60
|
--explain name the policies behind every decision (implies --detail)
|
|
61
61
|
--schemas include input/output JSON Schemas (implies --detail)
|
|
62
62
|
--tsconfig <path> tsconfig the source read uses (default: nearest to the config)
|
|
@@ -131,7 +131,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
131
131
|
const depth = values.depth;
|
|
132
132
|
try {
|
|
133
133
|
if (command === "init") {
|
|
134
|
-
const { runInit } = await import("./init-
|
|
134
|
+
const { runInit } = await import("./init-6XV64LK2.js");
|
|
135
135
|
return await runInit({
|
|
136
136
|
cwd: process.cwd(),
|
|
137
137
|
...values.tsconfig ? { tsconfig: values.tsconfig } : {},
|
|
@@ -158,7 +158,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
158
158
|
...values["baseline-dir"] ? { baselineDir: values["baseline-dir"] } : {}
|
|
159
159
|
};
|
|
160
160
|
if (command === "inspect") {
|
|
161
|
-
const { runInspect } = await import("./inspect-
|
|
161
|
+
const { runInspect } = await import("./inspect-NJ4J3MPP.js");
|
|
162
162
|
return await runInspect({
|
|
163
163
|
...shared,
|
|
164
164
|
...values.detail ? { detail: true } : {},
|
|
@@ -167,12 +167,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
if (command === "snapshot") {
|
|
170
|
-
const { runSnapshot } = await import("./snapshot-
|
|
170
|
+
const { runSnapshot } = await import("./snapshot-ZGTAF2Y5.js");
|
|
171
171
|
return await runSnapshot(shared);
|
|
172
172
|
}
|
|
173
|
-
const { runCheck } = await import("./check-
|
|
173
|
+
const { runCheck } = await import("./check-TJIX7NDE.js");
|
|
174
174
|
return await runCheck({
|
|
175
175
|
...shared,
|
|
176
|
+
...values.detail ? { detail: true } : {},
|
|
176
177
|
...values["allow-unresolved"] ? { allowUnresolved: true } : {}
|
|
177
178
|
});
|
|
178
179
|
} catch (error) {
|
|
@@ -228,7 +229,14 @@ function exitWhenWedged(code) {
|
|
|
228
229
|
if (held.length > 0) {
|
|
229
230
|
const kinds = [...new Set(held)].sort().join(", ");
|
|
230
231
|
writeError(
|
|
231
|
-
|
|
232
|
+
[
|
|
233
|
+
"",
|
|
234
|
+
"PROCESS CLEANUP WARN",
|
|
235
|
+
`Open handles ${held.length} (${kinds})`,
|
|
236
|
+
"Impact report complete; exit code unchanged; process exit forced",
|
|
237
|
+
"Likely cause polling, websocket, or a cache timer created during mount",
|
|
238
|
+
`Exit ${code}`
|
|
239
|
+
].join("\n")
|
|
232
240
|
);
|
|
233
241
|
}
|
|
234
242
|
void flushOutput().then(() => process.exit(code));
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bin.ts","../src/dom.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { DEPTHS, isDepth } from \"./contract.js\";\nimport { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — the agent surface your app exposes\n\nUsage\n agent-surface init read the codebase, then scaffold a config\n agent-surface inspect [scenario] what an agent can reach, and what it cannot\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail on drift, or on a capability no scenario reaches\n\nEvery command covers all scenarios in the config unless you name one.\n\nDepth\n --depth full read the source AND mount the scenarios, and report the gap (default)\n --depth static read the source only — no Vite, no jsdom, no mount, no scenarios needed\n --depth runtime mount only — skip the TypeScript program on a repo wide enough to feel it\n\nOptions\n --config <path> path to agent-surface.config.* (default: nearest, searching upward)\n --baseline-dir where baselines live (default: .agent-surface next to the config)\n --scope <prefix> restrict to a component-type prefix (repeatable)\n --detail one paragraph per capability instead of the table\n --explain name the policies behind every decision (implies --detail)\n --schemas include input/output JSON Schemas (implies --detail)\n --tsconfig <path> tsconfig the source read uses (default: nearest to the config)\n --allow-unresolved check: do not fail on a call site that could not be read\n --yes init: write without asking\n --json emit data instead of a rendered view\n --plain force plain text (implied when piped, or under CI / NO_COLOR)\n -h, --help show this\n -v, --version print the version\n\nExit codes are the contract: 0 clean, 1 a finding, 2 the command could not run.\n`;\n\nconst COMMANDS = [\"init\", \"inspect\", \"snapshot\", \"check\"];\n\n/** Commands that were cut, and where their answer went (0.11.0). */\nconst RETIRED: Record<string, string> = {\n capabilities: \"agent-surface inspect --depth static\",\n coverage: \"agent-surface inspect (or `check`, which now fails on the gap)\",\n};\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n let parsed;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\" },\n \"baseline-dir\": { type: \"string\" },\n scope: { type: \"string\", multiple: true },\n depth: { type: \"string\", default: \"full\" },\n detail: { type: \"boolean\", default: false },\n explain: { type: \"boolean\", default: false },\n schemas: { type: \"boolean\", default: false },\n tsconfig: { type: \"string\" },\n \"allow-unresolved\": { type: \"boolean\", default: false },\n yes: { type: \"boolean\", default: false },\n json: { type: \"boolean\", default: false },\n plain: { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n writeError(USAGE);\n return 2;\n }\n\n const { values, positionals } = parsed;\n if (values.help) {\n write(USAGE);\n return 0;\n }\n if (values.version) {\n write(await readVersion());\n return 0;\n }\n\n const [command, scenario] = positionals;\n if (!command) {\n write(USAGE);\n return 2;\n }\n if (!COMMANDS.includes(command)) {\n const moved = RETIRED[command];\n writeError(\n moved\n ? `\"${command}\" was removed in 0.11 — its answer is now \\`${moved}\\`. ` +\n \"The static catalog and the live projection are one command, so the gap between \" +\n \"them is reported rather than left for whoever remembers to look.\"\n : `unknown command \"${command}\"`,\n );\n if (!moved) writeError(USAGE);\n return 2;\n }\n\n if (!isDepth(values.depth)) {\n writeError(`--depth must be one of ${DEPTHS.join(\", \")} — got \"${values.depth}\"`);\n return 2;\n }\n const depth = values.depth;\n\n try {\n if (command === \"init\") {\n // Nothing to find and nothing to mount: `init` is the command that runs\n // before a config exists.\n const { runInit } = await import(\"./commands/init.js\");\n return await runInit({\n cwd: process.cwd(),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.yes ? { yes: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n });\n }\n\n const configPath = values.config ?? findConfig();\n if (!configPath) {\n writeError(\n \"no agent-surface.config.* found (searched upward from the working directory).\\n\" +\n \"Run `agent-surface init`, or see https://agent-surface-docs.vercel.app/20-cli\",\n );\n return 2;\n }\n\n // A presentation surface needs a DOM to mount into, and react-dom reads\n // these globals at import time — so this must happen before any app module\n // loads. It stays installed for the life of the process, on purpose\n // (see dom.ts).\n //\n // `--depth static` is the exception, and deliberately so: it reads the\n // TypeScript program and mounts nothing, so it must not pay for — or be\n // able to be affected by — a DOM it never renders into.\n if (depth !== \"static\") installDom();\n\n const shared = {\n configPath,\n depth,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.json ? { json: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n ...(values[\"baseline-dir\"] ? { baselineDir: values[\"baseline-dir\"] } : {}),\n };\n\n if (command === \"inspect\") {\n const { runInspect } = await import(\"./commands/inspect.js\");\n return await runInspect({\n ...shared,\n ...(values.detail ? { detail: true } : {}),\n ...(values.explain ? { explain: true } : {}),\n ...(values.schemas ? { schemas: true } : {}),\n });\n }\n if (command === \"snapshot\") {\n const { runSnapshot } = await import(\"./commands/snapshot.js\");\n return await runSnapshot(shared);\n }\n const { runCheck } = await import(\"./commands/check.js\");\n return await runCheck({\n ...shared,\n ...(values[\"allow-unresolved\"] ? { allowUnresolved: true } : {}),\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n if (error instanceof Error && error.stack && process.env[\"AGENT_SURFACE_DEBUG\"]) {\n writeError(error.stack);\n }\n // `2` — the command could not run, as opposed to running and finding\n // something. CI has to tell those apart: a gate that exits 1 both when the\n // surface changed and when the tool never loaded the app is a gate whose\n // green is the only signal worth anything, and whose red says nothing.\n return 2;\n }\n}\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n return (JSON.parse(readFileSync(path, \"utf8\")) as { version: string }).version;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Self-execute only as a binary; importing this module (tests) must not run it.\n * `argv[1]` is compared through `realpathSync` because package managers install\n * the bin as a symlink — comparing the raw path silently never matches, and the\n * CLI exits 0 having done nothing.\n */\nfunction invokedAsBinary(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\n/**\n * How long a finished command is allowed to keep running before it is treated\n * as wedged. Costs a hung run one extra second; costs a healthy run nothing,\n * because a healthy run has already exited by then.\n */\nconst GRACE_MS = 1000;\n\n/**\n * What this process's own stdin/stdout/stderr are called, depending on where\n * they were pointed: a terminal, a `|`, or a `>`. None of the three holds the\n * event loop open — a clean run exits naturally through all of them — so when\n * something *else* has wedged the command they are still in the handle table,\n * and naming them would send the reader after the one thing that is not the\n * cause. Their own leak would be the CLI's bug to fix, not the app's to hear\n * about.\n */\nconst OWN_STDIO = new Set([\"TTYWrap\", \"PipeWrap\", \"FileWrap\"]);\n\n/** Resource types still holding the loop once the command is provably wedged. */\nfunction heldHandles(): string[] {\n const active = process.getActiveResourcesInfo?.() ?? [];\n return active.filter((resource) => !OWN_STDIO.has(resource));\n}\n\n/**\n * `process.exit()` discards whatever is still buffered on a pipe, so a\n * redirected run could lose its last lines — and redirected runs are the ones\n * that matter (`--json`, CI logs). Drain both streams first, but never wait\n * indefinitely: a reader that has stopped consuming must not turn a forced exit\n * back into the hang it exists to prevent.\n */\nasync function flushOutput(): Promise<void> {\n const drained = Promise.all(\n [process.stdout, process.stderr].map(\n (stream) =>\n new Promise<void>((resolve) => {\n if (stream.writableLength === 0) resolve();\n else stream.write(\"\", () => resolve());\n }),\n ),\n );\n const deadline = new Promise<void>((resolve) => {\n setTimeout(resolve, 2000).unref();\n });\n await Promise.race([drained, deadline]);\n}\n\n/**\n * Ends the process, and says why it had to be ended when that is the case.\n *\n * The mount is an arbitrary React tree, not code written for a one-shot\n * process: a polling interval, a websocket, an animation loop or a data layer's\n * cache timer all keep Node's event loop alive long after the surface has been\n * rendered. Setting `process.exitCode` alone means such a command prints its\n * full, correct output and then appears to hang — with a successful exit code\n * already set, and nothing on screen to explain the wait (`AS-CLI-005`).\n *\n * The detector is the timer itself, not a reading of the handle table. An\n * unref'd timer does not hold the loop open, so a command with nothing left to\n * do exits naturally on `process.exitCode` and this never fires. Its firing is\n * therefore the diagnosis — this run *was* about to hang — and whatever it then\n * finds in the handle table is genuinely the cause. Reading the table eagerly\n * instead would blame the app for the CLI's own teardown: vite's dev server is\n * still closing its socket at the moment the last scenario is rendered, so\n * every healthy run would accuse its own app of leaking a `TCPServerWrap`.\n *\n * Naming the handles is the same move the package already makes for\n * capabilities: the invisible thing becomes inspectable. Exiting anyway is what\n * makes the tool usable unattended.\n */\nfunction exitWhenWedged(code: number): void {\n process.exitCode = code;\n setTimeout(() => {\n const held = heldHandles();\n if (held.length > 0) {\n const kinds = [...new Set(held)].sort().join(\", \");\n writeError(\n `agent-surface: the output above is complete, but ${held.length} handle(s) are still ` +\n `open (${kinds}) — something started during the mount is still running, so this ` +\n `command would have waited instead of exiting. Common causes: a polling interval, a ` +\n `websocket, or a data layer whose cache timer outlives the render. Exiting ${code}.`,\n );\n }\n void flushOutput().then(() => process.exit(code));\n }, GRACE_MS).unref();\n}\n\nif (invokedAsBinary()) {\n main().then(\n (code) => exitWhenWedged(code),\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n exitWhenWedged(1);\n },\n );\n}\n","import { JSDOM } from \"jsdom\";\n\n/**\n * A presentation surface only exists once components mount, and mounting needs\n * a DOM. Vitest gets one from its `jsdom` environment; a plain Node process has\n * to install one itself — *before* anything imports `react-dom`, which reads\n * these globals at module scope.\n *\n * Process-wide on purpose, and permanent: the app tree runs inside the\n * vite-node graph, which shares this realm's globals, and there is deliberately\n * no way to take the DOM back down — see the note on teardown below. Returning\n * nothing is the honest signature; an installer that handed back a disposer\n * doing nothing would read, at every call site, as cleanup that happens.\n */\nexport function installDom(url = \"http://localhost/\"): void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return;\n\n const dom = new JSDOM(\"<!doctype html><html><body></body></html>\", {\n url,\n pretendToBeVisual: true,\n });\n const { window } = dom;\n\n // Everything jsdom's window defines that this realm does not already have.\n // Skipping existing keys matters: Node's own `fetch`, `URL` and timers are\n // more capable than jsdom's shims, and clobbering them breaks app code.\n for (const key of Object.getOwnPropertyNames(window)) {\n if (key.startsWith(\"_\")) continue;\n if (key in globals) continue;\n const descriptor = Object.getOwnPropertyDescriptor(window, key);\n if (!descriptor) continue;\n Object.defineProperty(globals, key, descriptor);\n }\n\n for (const key of [\"window\", \"document\", \"navigator\"] as const) {\n if (!(key in globals)) {\n Object.defineProperty(globals, key, { value: window[key], configurable: true });\n }\n }\n}\n\n/**\n * There is deliberately no teardown, and the DOM is deliberately process-wide.\n *\n * `react-dom` captures `window`/`document` when it is first imported. Removing\n * the globals — or worse, calling `window.close()` — leaves that captured\n * reference pointing at a dead realm, so the *next* mount in the same process\n * fails in a way that looks nothing like its cause. A CLI invocation ends by\n * exiting (see `exitWhenWedged` in `bin.ts`), so there is nothing to reclaim; only\n * in-process callers (the test suite) run more than one command, and those are\n * exactly the ones this protects.\n */\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts","../src/dom.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { DEPTHS, isDepth } from \"./contract.js\";\nimport { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — the agent surface your app exposes\n\nUsage\n agent-surface init read the codebase, then scaffold a config\n agent-surface inspect [scenario] what an agent can reach, and what it cannot\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail on drift, or on a capability no scenario reaches\n\nEvery command covers all scenarios in the config unless you name one.\n\nDepth\n --depth full read the source AND mount the scenarios, and report the gap (default)\n --depth static read the source only — no Vite, no jsdom, no mount, no scenarios needed\n --depth runtime mount only — skip the TypeScript program on a repo wide enough to feel it\n\nOptions\n --config <path> path to agent-surface.config.* (default: nearest, searching upward)\n --baseline-dir where baselines live (default: .agent-surface next to the config)\n --scope <prefix> restrict to a component-type prefix (repeatable)\n --detail full capability, origin, and diagnostic detail\n --explain name the policies behind every decision (implies --detail)\n --schemas include input/output JSON Schemas (implies --detail)\n --tsconfig <path> tsconfig the source read uses (default: nearest to the config)\n --allow-unresolved check: do not fail on a call site that could not be read\n --yes init: write without asking\n --json emit data instead of a rendered view\n --plain force plain text (implied when piped, or under CI / NO_COLOR)\n -h, --help show this\n -v, --version print the version\n\nExit codes are the contract: 0 clean, 1 a finding, 2 the command could not run.\n`;\n\nconst COMMANDS = [\"init\", \"inspect\", \"snapshot\", \"check\"];\n\n/** Commands that were cut, and where their answer went (0.11.0). */\nconst RETIRED: Record<string, string> = {\n capabilities: \"agent-surface inspect --depth static\",\n coverage: \"agent-surface inspect (or `check`, which now fails on the gap)\",\n};\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n let parsed;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\" },\n \"baseline-dir\": { type: \"string\" },\n scope: { type: \"string\", multiple: true },\n depth: { type: \"string\", default: \"full\" },\n detail: { type: \"boolean\", default: false },\n explain: { type: \"boolean\", default: false },\n schemas: { type: \"boolean\", default: false },\n tsconfig: { type: \"string\" },\n \"allow-unresolved\": { type: \"boolean\", default: false },\n yes: { type: \"boolean\", default: false },\n json: { type: \"boolean\", default: false },\n plain: { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n writeError(USAGE);\n return 2;\n }\n\n const { values, positionals } = parsed;\n if (values.help) {\n write(USAGE);\n return 0;\n }\n if (values.version) {\n write(await readVersion());\n return 0;\n }\n\n const [command, scenario] = positionals;\n if (!command) {\n write(USAGE);\n return 2;\n }\n if (!COMMANDS.includes(command)) {\n const moved = RETIRED[command];\n writeError(\n moved\n ? `\"${command}\" was removed in 0.11 — its answer is now \\`${moved}\\`. ` +\n \"The static catalog and the live projection are one command, so the gap between \" +\n \"them is reported rather than left for whoever remembers to look.\"\n : `unknown command \"${command}\"`,\n );\n if (!moved) writeError(USAGE);\n return 2;\n }\n\n if (!isDepth(values.depth)) {\n writeError(`--depth must be one of ${DEPTHS.join(\", \")} — got \"${values.depth}\"`);\n return 2;\n }\n const depth = values.depth;\n\n try {\n if (command === \"init\") {\n // Nothing to find and nothing to mount: `init` is the command that runs\n // before a config exists.\n const { runInit } = await import(\"./commands/init.js\");\n return await runInit({\n cwd: process.cwd(),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.yes ? { yes: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n });\n }\n\n const configPath = values.config ?? findConfig();\n if (!configPath) {\n writeError(\n \"no agent-surface.config.* found (searched upward from the working directory).\\n\" +\n \"Run `agent-surface init`, or see https://agent-surface-docs.vercel.app/20-cli\",\n );\n return 2;\n }\n\n // A presentation surface needs a DOM to mount into, and react-dom reads\n // these globals at import time — so this must happen before any app module\n // loads. It stays installed for the life of the process, on purpose\n // (see dom.ts).\n //\n // `--depth static` is the exception, and deliberately so: it reads the\n // TypeScript program and mounts nothing, so it must not pay for — or be\n // able to be affected by — a DOM it never renders into.\n if (depth !== \"static\") installDom();\n\n const shared = {\n configPath,\n depth,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.json ? { json: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n ...(values[\"baseline-dir\"] ? { baselineDir: values[\"baseline-dir\"] } : {}),\n };\n\n if (command === \"inspect\") {\n const { runInspect } = await import(\"./commands/inspect.js\");\n return await runInspect({\n ...shared,\n ...(values.detail ? { detail: true } : {}),\n ...(values.explain ? { explain: true } : {}),\n ...(values.schemas ? { schemas: true } : {}),\n });\n }\n if (command === \"snapshot\") {\n const { runSnapshot } = await import(\"./commands/snapshot.js\");\n return await runSnapshot(shared);\n }\n const { runCheck } = await import(\"./commands/check.js\");\n return await runCheck({\n ...shared,\n ...(values.detail ? { detail: true } : {}),\n ...(values[\"allow-unresolved\"] ? { allowUnresolved: true } : {}),\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n if (error instanceof Error && error.stack && process.env[\"AGENT_SURFACE_DEBUG\"]) {\n writeError(error.stack);\n }\n // `2` — the command could not run, as opposed to running and finding\n // something. CI has to tell those apart: a gate that exits 1 both when the\n // surface changed and when the tool never loaded the app is a gate whose\n // green is the only signal worth anything, and whose red says nothing.\n return 2;\n }\n}\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n return (JSON.parse(readFileSync(path, \"utf8\")) as { version: string }).version;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Self-execute only as a binary; importing this module (tests) must not run it.\n * `argv[1]` is compared through `realpathSync` because package managers install\n * the bin as a symlink — comparing the raw path silently never matches, and the\n * CLI exits 0 having done nothing.\n */\nfunction invokedAsBinary(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\n/**\n * How long a finished command is allowed to keep running before it is treated\n * as wedged. Costs a hung run one extra second; costs a healthy run nothing,\n * because a healthy run has already exited by then.\n */\nconst GRACE_MS = 1000;\n\n/**\n * What this process's own stdin/stdout/stderr are called, depending on where\n * they were pointed: a terminal, a `|`, or a `>`. None of the three holds the\n * event loop open — a clean run exits naturally through all of them — so when\n * something *else* has wedged the command they are still in the handle table,\n * and naming them would send the reader after the one thing that is not the\n * cause. Their own leak would be the CLI's bug to fix, not the app's to hear\n * about.\n */\nconst OWN_STDIO = new Set([\"TTYWrap\", \"PipeWrap\", \"FileWrap\"]);\n\n/** Resource types still holding the loop once the command is provably wedged. */\nfunction heldHandles(): string[] {\n const active = process.getActiveResourcesInfo?.() ?? [];\n return active.filter((resource) => !OWN_STDIO.has(resource));\n}\n\n/**\n * `process.exit()` discards whatever is still buffered on a pipe, so a\n * redirected run could lose its last lines — and redirected runs are the ones\n * that matter (`--json`, CI logs). Drain both streams first, but never wait\n * indefinitely: a reader that has stopped consuming must not turn a forced exit\n * back into the hang it exists to prevent.\n */\nasync function flushOutput(): Promise<void> {\n const drained = Promise.all(\n [process.stdout, process.stderr].map(\n (stream) =>\n new Promise<void>((resolve) => {\n if (stream.writableLength === 0) resolve();\n else stream.write(\"\", () => resolve());\n }),\n ),\n );\n const deadline = new Promise<void>((resolve) => {\n setTimeout(resolve, 2000).unref();\n });\n await Promise.race([drained, deadline]);\n}\n\n/**\n * Ends the process, and says why it had to be ended when that is the case.\n *\n * The mount is an arbitrary React tree, not code written for a one-shot\n * process: a polling interval, a websocket, an animation loop or a data layer's\n * cache timer all keep Node's event loop alive long after the surface has been\n * rendered. Setting `process.exitCode` alone means such a command prints its\n * full, correct output and then appears to hang — with a successful exit code\n * already set, and nothing on screen to explain the wait (`AS-CLI-005`).\n *\n * The detector is the timer itself, not a reading of the handle table. An\n * unref'd timer does not hold the loop open, so a command with nothing left to\n * do exits naturally on `process.exitCode` and this never fires. Its firing is\n * therefore the diagnosis — this run *was* about to hang — and whatever it then\n * finds in the handle table is genuinely the cause. Reading the table eagerly\n * instead would blame the app for the CLI's own teardown: vite's dev server is\n * still closing its socket at the moment the last scenario is rendered, so\n * every healthy run would accuse its own app of leaking a `TCPServerWrap`.\n *\n * Naming the handles is the same move the package already makes for\n * capabilities: the invisible thing becomes inspectable. Exiting anyway is what\n * makes the tool usable unattended.\n */\nfunction exitWhenWedged(code: number): void {\n process.exitCode = code;\n setTimeout(() => {\n const held = heldHandles();\n if (held.length > 0) {\n const kinds = [...new Set(held)].sort().join(\", \");\n writeError(\n [\n \"\",\n \"PROCESS CLEANUP WARN\",\n `Open handles ${held.length} (${kinds})`,\n \"Impact report complete; exit code unchanged; process exit forced\",\n \"Likely cause polling, websocket, or a cache timer created during mount\",\n `Exit ${code}`,\n ].join(\"\\n\"),\n );\n }\n void flushOutput().then(() => process.exit(code));\n }, GRACE_MS).unref();\n}\n\nif (invokedAsBinary()) {\n main().then(\n (code) => exitWhenWedged(code),\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n exitWhenWedged(1);\n },\n );\n}\n","import { JSDOM } from \"jsdom\";\n\n/**\n * A presentation surface only exists once components mount, and mounting needs\n * a DOM. Vitest gets one from its `jsdom` environment; a plain Node process has\n * to install one itself — *before* anything imports `react-dom`, which reads\n * these globals at module scope.\n *\n * Process-wide on purpose, and permanent: the app tree runs inside the\n * vite-node graph, which shares this realm's globals, and there is deliberately\n * no way to take the DOM back down — see the note on teardown below. Returning\n * nothing is the honest signature; an installer that handed back a disposer\n * doing nothing would read, at every call site, as cleanup that happens.\n */\nexport function installDom(url = \"http://localhost/\"): void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return;\n\n const dom = new JSDOM(\"<!doctype html><html><body></body></html>\", {\n url,\n pretendToBeVisual: true,\n });\n const { window } = dom;\n\n // Everything jsdom's window defines that this realm does not already have.\n // Skipping existing keys matters: Node's own `fetch`, `URL` and timers are\n // more capable than jsdom's shims, and clobbering them breaks app code.\n for (const key of Object.getOwnPropertyNames(window)) {\n if (key.startsWith(\"_\")) continue;\n if (key in globals) continue;\n const descriptor = Object.getOwnPropertyDescriptor(window, key);\n if (!descriptor) continue;\n Object.defineProperty(globals, key, descriptor);\n }\n\n for (const key of [\"window\", \"document\", \"navigator\"] as const) {\n if (!(key in globals)) {\n Object.defineProperty(globals, key, { value: window[key], configurable: true });\n }\n }\n}\n\n/**\n * There is deliberately no teardown, and the DOM is deliberately process-wide.\n *\n * `react-dom` captures `window`/`document` when it is first imported. Removing\n * the globals — or worse, calling `window.close()` — leaves that captured\n * reference pointing at a dead realm, so the *next* mount in the same process\n * fails in a way that looks nothing like its cause. A CLI invocation ends by\n * exiting (see `exitWhenWedged` in `bin.ts`), so there is nothing to reclaim; only\n * in-process callers (the test suite) run more than one command, and those are\n * exactly the ones this protects.\n */\n"],"mappings":";;;;;;;;;;AACA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;;;ACH1B,SAAS,aAAa;AAcf,SAAS,WAAW,MAAM,qBAA2B;AAC1D,QAAM,UAAU;AAChB,MAAI,OAAO,QAAQ,UAAU,MAAM,YAAa;AAEhD,QAAM,MAAM,IAAI,MAAM,6CAA6C;AAAA,IACjE;AAAA,IACA,mBAAmB;AAAA,EACrB,CAAC;AACD,QAAM,EAAE,OAAO,IAAI;AAKnB,aAAW,OAAO,OAAO,oBAAoB,MAAM,GAAG;AACpD,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,QAAI,OAAO,QAAS;AACpB,UAAM,aAAa,OAAO,yBAAyB,QAAQ,GAAG;AAC9D,QAAI,CAAC,WAAY;AACjB,WAAO,eAAe,SAAS,KAAK,UAAU;AAAA,EAChD;AAEA,aAAW,OAAO,CAAC,UAAU,YAAY,WAAW,GAAY;AAC9D,QAAI,EAAE,OAAO,UAAU;AACrB,aAAO,eAAe,SAAS,KAAK,EAAE,OAAO,OAAO,GAAG,GAAG,cAAc,KAAK,CAAC;AAAA,IAChF;AAAA,EACF;AACF;;;AD/BA,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiCd,IAAM,WAAW,CAAC,QAAQ,WAAW,YAAY,OAAO;AAGxD,IAAM,UAAkC;AAAA,EACtC,cAAc;AAAA,EACd,UAAU;AACZ;AAEA,eAAsB,KAAK,OAAiB,QAAQ,KAAK,MAAM,CAAC,GAAoB;AAClF,MAAI;AACJ,MAAI;AACF,aAAS,UAAU;AAAA,MACjB,MAAM;AAAA,MACN,kBAAkB;AAAA,MAClB,SAAS;AAAA,QACP,QAAQ,EAAE,MAAM,SAAS;AAAA,QACzB,gBAAgB,EAAE,MAAM,SAAS;AAAA,QACjC,OAAO,EAAE,MAAM,UAAU,UAAU,KAAK;AAAA,QACxC,OAAO,EAAE,MAAM,UAAU,SAAS,OAAO;AAAA,QACzC,QAAQ,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC1C,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,UAAU,EAAE,MAAM,SAAS;AAAA,QAC3B,oBAAoB,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACtD,KAAK,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACvC,MAAM,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACxC,OAAO,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACzC,MAAM,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,QACpD,SAAS,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,MACzD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,eAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,eAAW,KAAK;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,QAAQ,YAAY,IAAI;AAChC,MAAI,OAAO,MAAM;AACf,UAAM,KAAK;AACX,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS;AAClB,UAAM,MAAM,YAAY,CAAC;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,CAAC,SAAS,QAAQ,IAAI;AAC5B,MAAI,CAAC,SAAS;AACZ,UAAM,KAAK;AACX,WAAO;AAAA,EACT;AACA,MAAI,CAAC,SAAS,SAAS,OAAO,GAAG;AAC/B,UAAM,QAAQ,QAAQ,OAAO;AAC7B;AAAA,MACE,QACI,IAAI,OAAO,oDAA+C,KAAK,wJAG/D,oBAAoB,OAAO;AAAA,IACjC;AACA,QAAI,CAAC,MAAO,YAAW,KAAK;AAC5B,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,QAAQ,OAAO,KAAK,GAAG;AAC1B,eAAW,0BAA0B,OAAO,KAAK,IAAI,CAAC,gBAAW,OAAO,KAAK,GAAG;AAChF,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,OAAO;AAErB,MAAI;AACF,QAAI,YAAY,QAAQ;AAGtB,YAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAoB;AACrD,aAAO,MAAM,QAAQ;AAAA,QACnB,KAAK,QAAQ,IAAI;AAAA,QACjB,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;AAAA,QACvD,GAAI,OAAO,MAAM,EAAE,KAAK,KAAK,IAAI,CAAC;AAAA,QAClC,GAAI,OAAO,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,MACxC,CAAC;AAAA,IACH;AAEA,UAAM,aAAa,OAAO,UAAU,WAAW;AAC/C,QAAI,CAAC,YAAY;AACf;AAAA,QACE;AAAA,MAEF;AACA,aAAO;AAAA,IACT;AAUA,QAAI,UAAU,SAAU,YAAW;AAEnC,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9C,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;AAAA,MACvD,GAAI,OAAO,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAAA,MACpC,GAAI,OAAO,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,MACtC,GAAI,OAAO,cAAc,IAAI,EAAE,aAAa,OAAO,cAAc,EAAE,IAAI,CAAC;AAAA,IAC1E;AAEA,QAAI,YAAY,WAAW;AACzB,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,uBAAuB;AAC3D,aAAO,MAAM,WAAW;AAAA,QACtB,GAAG;AAAA,QACH,GAAI,OAAO,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,QACxC,GAAI,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,QAC1C,GAAI,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,MAC5C,CAAC;AAAA,IACH;AACA,QAAI,YAAY,YAAY;AAC1B,YAAM,EAAE,YAAY,IAAI,MAAM,OAAO,wBAAwB;AAC7D,aAAO,MAAM,YAAY,MAAM;AAAA,IACjC;AACA,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,qBAAqB;AACvD,WAAO,MAAM,SAAS;AAAA,MACpB,GAAG;AAAA,MACH,GAAI,OAAO,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,MACxC,GAAI,OAAO,kBAAkB,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAAA,IAChE,CAAC;AAAA,EACH,SAAS,OAAO;AACd,eAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,QAAI,iBAAiB,SAAS,MAAM,SAAS,QAAQ,IAAI,qBAAqB,GAAG;AAC/E,iBAAW,MAAM,KAAK;AAAA,IACxB;AAKA,WAAO;AAAA,EACT;AACF;AAEA,eAAe,cAA+B;AAC5C,MAAI;AACF,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,IAAS;AAC/C,UAAM,OAAO,cAAc,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC;AACtE,WAAQ,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,EAA0B;AAAA,EACzE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,SAAS,kBAA2B;AAClC,QAAM,QAAQ,QAAQ,KAAK,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,cAAc,YAAY,GAAG,MAAM,aAAa,KAAK;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,IAAM,WAAW;AAWjB,IAAM,YAAY,oBAAI,IAAI,CAAC,WAAW,YAAY,UAAU,CAAC;AAG7D,SAAS,cAAwB;AAC/B,QAAM,SAAS,QAAQ,yBAAyB,KAAK,CAAC;AACtD,SAAO,OAAO,OAAO,CAAC,aAAa,CAAC,UAAU,IAAI,QAAQ,CAAC;AAC7D;AASA,eAAe,cAA6B;AAC1C,QAAM,UAAU,QAAQ;AAAA,IACtB,CAAC,QAAQ,QAAQ,QAAQ,MAAM,EAAE;AAAA,MAC/B,CAAC,WACC,IAAI,QAAc,CAAC,YAAY;AAC7B,YAAI,OAAO,mBAAmB,EAAG,SAAQ;AAAA,YACpC,QAAO,MAAM,IAAI,MAAM,QAAQ,CAAC;AAAA,MACvC,CAAC;AAAA,IACL;AAAA,EACF;AACA,QAAM,WAAW,IAAI,QAAc,CAAC,YAAY;AAC9C,eAAW,SAAS,GAAI,EAAE,MAAM;AAAA,EAClC,CAAC;AACD,QAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;AACxC;AAyBA,SAAS,eAAe,MAAoB;AAC1C,UAAQ,WAAW;AACnB,aAAW,MAAM;AACf,UAAM,OAAO,YAAY;AACzB,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,IAAI;AACjD;AAAA,QACE;AAAA,UACE;AAAA,UACA;AAAA,UACA,iBAAiB,KAAK,MAAM,KAAK,KAAK;AAAA,UACtC;AAAA,UACA;AAAA,UACA,iBAAiB,IAAI;AAAA,QACvB,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AACA,SAAK,YAAY,EAAE,KAAK,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,EAClD,GAAG,QAAQ,EAAE,MAAM;AACrB;AAEA,IAAI,gBAAgB,GAAG;AACrB,OAAK,EAAE;AAAA,IACL,CAAC,SAAS,eAAe,IAAI;AAAA,IAC7B,CAAC,UAAmB;AAClB,iBAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,qBAAe,CAAC;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coverageReport,
|
|
3
|
+
renderCheckOverviewPlain,
|
|
4
|
+
renderCoveragePlain,
|
|
5
|
+
renderDriftPlain,
|
|
6
|
+
renderFailuresPlain,
|
|
7
|
+
renderNextStepsPlain,
|
|
8
|
+
renderNoVerdictPlain,
|
|
9
|
+
renderSectionsPlain,
|
|
10
|
+
scenarioBaseline
|
|
11
|
+
} from "./chunk-L7GHSC2Z.js";
|
|
12
|
+
import {
|
|
13
|
+
displayPath,
|
|
14
|
+
scenarioStats
|
|
15
|
+
} from "./chunk-VX6GBEP3.js";
|
|
16
|
+
import {
|
|
17
|
+
SCENARIO_MANIFEST_FILE,
|
|
18
|
+
annotate,
|
|
19
|
+
baselinePath,
|
|
20
|
+
diff,
|
|
21
|
+
joinCoverage,
|
|
22
|
+
mountScenarios,
|
|
23
|
+
readBaseline,
|
|
24
|
+
readInventory,
|
|
25
|
+
readScenarioManifest
|
|
26
|
+
} from "./chunk-IALBMW3R.js";
|
|
27
|
+
import {
|
|
28
|
+
UsageError,
|
|
29
|
+
write,
|
|
30
|
+
writeError
|
|
31
|
+
} from "./chunk-3AJ343NA.js";
|
|
32
|
+
import {
|
|
33
|
+
ALLOWLIST_FILE,
|
|
34
|
+
UNREAD_ALLOWLIST_FILE,
|
|
35
|
+
coverageExitCode
|
|
36
|
+
} from "./chunk-UGCLJ5JX.js";
|
|
37
|
+
|
|
38
|
+
// src/commands/check.ts
|
|
39
|
+
import { existsSync, readdirSync } from "fs";
|
|
40
|
+
async function runCheck(options) {
|
|
41
|
+
if (options.depth === "static") {
|
|
42
|
+
throw new UsageError(
|
|
43
|
+
"check --depth static has nothing to compare \u2014 a baseline is a projection, and at this depth nothing is mounted. Use --depth runtime for drift alone, or full for both."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const analysis = {
|
|
47
|
+
configPath: options.configPath,
|
|
48
|
+
depth: options.depth,
|
|
49
|
+
...options.scenario ? { scenario: options.scenario } : {},
|
|
50
|
+
...options.scope ? { scope: options.scope } : {},
|
|
51
|
+
...options.tsconfig ? { tsconfig: options.tsconfig } : {},
|
|
52
|
+
...options.baselineDir ? { baselineDir: options.baselineDir } : {}
|
|
53
|
+
};
|
|
54
|
+
const inventory = readInventory(analysis);
|
|
55
|
+
const runtime = await mountScenarios(analysis);
|
|
56
|
+
if (!runtime) throw new UsageError("check needs a mount, and this depth performs none");
|
|
57
|
+
const drifted = [];
|
|
58
|
+
for (const result of runtime.results) {
|
|
59
|
+
const path = baselinePath(runtime.baselineDir, result.scenario);
|
|
60
|
+
const expected = readBaseline(path);
|
|
61
|
+
if (expected === void 0) {
|
|
62
|
+
drifted.push({ scenario: result.scenario, missingBaseline: true, entries: [] });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const actual = scenarioBaseline(result);
|
|
66
|
+
const entries = annotate(diff(expected, actual), actual, expected);
|
|
67
|
+
if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });
|
|
68
|
+
}
|
|
69
|
+
const committedScenarios = readScenarioManifest(runtime.baselineDir);
|
|
70
|
+
const declared = [...runtime.declaredScenarios].sort();
|
|
71
|
+
const scenarioManifestMismatch = committedScenarios === void 0 || JSON.stringify(committedScenarios) !== JSON.stringify(declared);
|
|
72
|
+
const reserved = /* @__PURE__ */ new Set([SCENARIO_MANIFEST_FILE, ALLOWLIST_FILE, UNREAD_ALLOWLIST_FILE]);
|
|
73
|
+
const staleBaselineFiles = (existsSync(runtime.baselineDir) ? readdirSync(runtime.baselineDir, { withFileTypes: true }) : []).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && !reserved.has(entry.name)).map((entry) => entry.name.slice(0, -5)).filter((scenario) => !runtime.declaredScenarios.includes(scenario)).sort();
|
|
74
|
+
const rejected = runtime.results.filter((result) => result.rejections.length > 0).map((result) => ({ scenario: result.scenario, rejections: result.rejections }));
|
|
75
|
+
const coverage = joinCoverage(inventory, runtime, analysis);
|
|
76
|
+
const coverageFailed = coverage !== void 0 && coverageExitCode(coverage, { allowUnresolved: options.allowUnresolved === true }) !== 0;
|
|
77
|
+
const couldNotRun = runtime.failures.length > 0;
|
|
78
|
+
const ok = drifted.length === 0 && !coverageFailed && !couldNotRun && rejected.length === 0 && !scenarioManifestMismatch && staleBaselineFiles.length === 0;
|
|
79
|
+
if (options.json) {
|
|
80
|
+
write(
|
|
81
|
+
JSON.stringify(
|
|
82
|
+
{
|
|
83
|
+
ok,
|
|
84
|
+
drifted,
|
|
85
|
+
failures: runtime.failures,
|
|
86
|
+
rejected,
|
|
87
|
+
scenarioManifest: {
|
|
88
|
+
expected: declared,
|
|
89
|
+
committed: committedScenarios ?? null,
|
|
90
|
+
staleBaselines: staleBaselineFiles
|
|
91
|
+
},
|
|
92
|
+
coverage: coverageReport(coverage)
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
return couldNotRun ? 2 : ok ? 0 : 1;
|
|
99
|
+
}
|
|
100
|
+
const missing = drifted.filter((entry) => entry.missingBaseline);
|
|
101
|
+
const changed = drifted.filter((entry) => !entry.missingBaseline);
|
|
102
|
+
const baselineOf = (scenario) => {
|
|
103
|
+
const entry = drifted.find((candidate) => candidate.scenario === scenario);
|
|
104
|
+
if (!entry) return "current";
|
|
105
|
+
if (entry.missingBaseline) return "missing";
|
|
106
|
+
return `drift (${entry.entries.length})`;
|
|
107
|
+
};
|
|
108
|
+
const stats = runtime.scenarios.map((scenario) => {
|
|
109
|
+
const result = runtime.results.find((candidate) => candidate.scenario === scenario);
|
|
110
|
+
return result ? { ...scenarioStats(result), baseline: baselineOf(scenario) } : {
|
|
111
|
+
scenario,
|
|
112
|
+
callable: 0,
|
|
113
|
+
disabled: 0,
|
|
114
|
+
hidden: 0,
|
|
115
|
+
rejected: 0,
|
|
116
|
+
failed: true,
|
|
117
|
+
...runtime.failures.find((failure) => failure.scenario === scenario)?.message ? {
|
|
118
|
+
failure: runtime.failures.find((failure) => failure.scenario === scenario).message
|
|
119
|
+
} : {}
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
const overview = renderCheckOverviewPlain({
|
|
123
|
+
status: couldNotRun ? "ERROR" : ok ? "PASS" : "FAIL",
|
|
124
|
+
...coverage ? { coverage } : {},
|
|
125
|
+
unresolvedAllowed: options.allowUnresolved === true,
|
|
126
|
+
baselineCurrent: Math.max(0, runtime.results.length - drifted.length),
|
|
127
|
+
baselineTotal: runtime.scenarios.length,
|
|
128
|
+
scenarioManifestOk: !scenarioManifestMismatch && staleBaselineFiles.length === 0,
|
|
129
|
+
rejected: rejected.reduce((sum, entry) => sum + entry.rejections.length, 0),
|
|
130
|
+
mountFailures: runtime.failures.length,
|
|
131
|
+
scenarios: runtime.scenarios,
|
|
132
|
+
context: {
|
|
133
|
+
configPath: options.configPath,
|
|
134
|
+
depth: options.depth,
|
|
135
|
+
...runtime.scope ? { scope: runtime.scope } : {}
|
|
136
|
+
},
|
|
137
|
+
stats
|
|
138
|
+
});
|
|
139
|
+
if (ok) write(overview);
|
|
140
|
+
else writeError(overview);
|
|
141
|
+
if (coverage) {
|
|
142
|
+
const rendered = renderCoveragePlain(coverage, {
|
|
143
|
+
compact: true,
|
|
144
|
+
...options.detail ? { detail: true } : {}
|
|
145
|
+
});
|
|
146
|
+
if (rendered) {
|
|
147
|
+
if (coverageFailed) writeError(`
|
|
148
|
+
${rendered}`);
|
|
149
|
+
else write(`
|
|
150
|
+
${rendered}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const sections = [];
|
|
154
|
+
const steps = [];
|
|
155
|
+
if (coverage && coverage.unreached.length > 0) {
|
|
156
|
+
steps.push(
|
|
157
|
+
`mount the ${coverage.unreached.length} unreached capabilit${coverage.unreached.length === 1 ? "y" : "ies"} from a scenario, or record the decision in ${displayPath(coverage.allowlistPath)}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (coverage && coverage.unresolved.length > 0 && options.allowUnresolved !== true) {
|
|
161
|
+
steps.push(
|
|
162
|
+
`make the ${coverage.unresolved.length} unread call site${coverage.unresolved.length === 1 ? "" : "s"} readable, or paste each printed key into ${displayPath(coverage.unreadAllowlistPath)}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (rejected.length > 0) {
|
|
166
|
+
const total = rejected.reduce((sum, entry) => sum + entry.rejections.length, 0);
|
|
167
|
+
sections.push({
|
|
168
|
+
title: "REJECTED REGISTRATIONS",
|
|
169
|
+
gloss: "the runtime refused authored surface during the mount",
|
|
170
|
+
count: total,
|
|
171
|
+
lines: rejected.flatMap(
|
|
172
|
+
(entry) => entry.rejections.map(
|
|
173
|
+
(rejection) => `${entry.scenario}: ${rejection.componentType}@${rejection.instanceId} (${rejection.reason})`
|
|
174
|
+
)
|
|
175
|
+
),
|
|
176
|
+
hint: "a dead handle registers nothing \u2014 give the second registration its own instanceId, or remove the duplicated component type"
|
|
177
|
+
});
|
|
178
|
+
steps.push(
|
|
179
|
+
`resolve ${total} rejected registration${total === 1 ? "" : "s"} \u2014 the capabilities behind them reach no agent`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (scenarioManifestMismatch || staleBaselineFiles.length > 0) {
|
|
183
|
+
sections.push({
|
|
184
|
+
title: "SCENARIO DRIFT",
|
|
185
|
+
gloss: "the committed baselines do not match the config",
|
|
186
|
+
count: 0,
|
|
187
|
+
lines: [
|
|
188
|
+
`config: ${declared.join(", ")}`,
|
|
189
|
+
`manifest: ${committedScenarios?.join(", ") ?? "missing"}`,
|
|
190
|
+
...staleBaselineFiles.length > 0 ? [`stale: ${staleBaselineFiles.join(", ")}`] : []
|
|
191
|
+
],
|
|
192
|
+
hint: "run `agent-surface snapshot`, commit the manifest, and delete any baseline for a scenario the config no longer declares"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (missing.length > 0) {
|
|
196
|
+
sections.push({
|
|
197
|
+
title: "NO BASELINE",
|
|
198
|
+
gloss: "nothing to compare against, which is not the same as a match",
|
|
199
|
+
count: missing.length,
|
|
200
|
+
lines: missing.map(
|
|
201
|
+
(entry) => `${entry.scenario}: ${displayPath(
|
|
202
|
+
baselinePath(runtime.baselineDir, entry.scenario)
|
|
203
|
+
)} does not exist`
|
|
204
|
+
),
|
|
205
|
+
hint: "run `agent-surface snapshot` and commit the files it writes"
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (changed.length > 0) {
|
|
209
|
+
sections.push({
|
|
210
|
+
title: "DRIFT",
|
|
211
|
+
gloss: "the surface changed against its baseline",
|
|
212
|
+
count: changed.length,
|
|
213
|
+
lines: changed.flatMap((entry) => renderDriftPlain(entry.scenario, entry.entries)),
|
|
214
|
+
hint: "review the change, then `agent-surface snapshot` to accept it" + (runtime.scope ? ". A scope filters the projection while baselines are written from whatever scope wrote them, so re-check without --scope before believing this one" : "")
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const baselinesStale = missing.length > 0 || changed.length > 0 || scenarioManifestMismatch || staleBaselineFiles.length > 0;
|
|
218
|
+
if (baselinesStale) {
|
|
219
|
+
steps.push(
|
|
220
|
+
"`agent-surface snapshot`, then commit .agent-surface/ \u2014 this accepts the surface above as reviewed"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (sections.length > 0) writeError(`
|
|
224
|
+
${renderSectionsPlain(sections)}`);
|
|
225
|
+
if (couldNotRun) {
|
|
226
|
+
writeError(`
|
|
227
|
+
${renderFailuresPlain(runtime.failures)}`);
|
|
228
|
+
if (inventory) writeError(`
|
|
229
|
+
${renderNoVerdictPlain(runtime.failures)}`);
|
|
230
|
+
steps.unshift(
|
|
231
|
+
`fix the mount for ${runtime.failures.map((failure) => failure.scenario).join(", ")} \u2014 every count above is missing whatever ${runtime.failures.length === 1 ? "it" : "they"} would have surfaced`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (!ok && steps.length > 0) writeError(`
|
|
235
|
+
${renderNextStepsPlain(steps)}`);
|
|
236
|
+
return couldNotRun ? 2 : ok ? 0 : 1;
|
|
237
|
+
}
|
|
238
|
+
export {
|
|
239
|
+
runCheck
|
|
240
|
+
};
|
|
241
|
+
//# sourceMappingURL=check-TJIX7NDE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport {\n UsageError,\n joinCoverage,\n mountScenarios,\n readInventory,\n type AnalysisOptions,\n type Depth,\n} from \"../analysis.js\";\nimport {\n annotate,\n baselinePath,\n diff,\n readBaseline,\n readScenarioManifest,\n SCENARIO_MANIFEST_FILE,\n type DiffEntry,\n} from \"../baseline.js\";\nimport { ALLOWLIST_FILE, coverageExitCode, UNREAD_ALLOWLIST_FILE } from \"../coverage.js\";\nimport {\n renderCoveragePlain,\n renderCheckOverviewPlain,\n renderDriftPlain,\n renderFailuresPlain,\n renderNextStepsPlain,\n renderNoVerdictPlain,\n renderSectionsPlain,\n} from \"../render/plain.js\";\nimport {\n displayPath,\n scenarioStats,\n type FindingSection,\n type ScenarioStats,\n} from \"../render/summary.js\";\nimport { write, writeError } from \"../output.js\";\nimport { coverageReport, scenarioBaseline } from \"../report.js\";\n\nexport interface CheckOptions {\n configPath: string;\n depth: Depth;\n scenario?: string;\n scope?: string[];\n tsconfig?: string;\n baselineDir?: string;\n allowUnresolved?: boolean;\n json?: boolean;\n plain?: boolean;\n detail?: boolean;\n}\n\ninterface ScenarioDrift {\n scenario: string;\n missingBaseline?: boolean;\n entries: DiffEntry[];\n}\n\ninterface RejectedScenario {\n scenario: string;\n rejections: Array<{ componentType: string; instanceId: string; reason: string }>;\n}\n\n/**\n * The gate. The only command in this package that fails on a finding, which is\n * why every finding has to reach it.\n *\n * It used to fail on exactly one class — the projection drifting from its\n * baseline — and print a line telling you that capabilities no scenario mounts\n * were a different command's question. A gate that names the check it is not\n * performing is a gate with a hole in it, and in CI the hole was silent: a\n * whole unreached route sat behind a green tick.\n *\n * So it now fails on every incomplete or changed report:\n *\n * - **drift** — the surface changed against its committed baseline;\n * - **a missing baseline** — nothing to compare, which is not the same as a match;\n * - **an unreached capability** — authored, and no scenario mounts it;\n * - **an unread call site** — the catalog is incomplete, so the third check\n * above is computed over a denominator that is only a floor;\n * - **a rejected registration** — authored surface was refused;\n * - **scenario drift** — config, manifest and baseline files disagree.\n *\n * `.agent-surface/coverage-allow.json` ratchets the third; `--allow-unresolved`\n * accepts the fourth. Both are deliberate, committed decisions rather than\n * flags that quietly widen the gate.\n *\n * The report is read top-down: the verdict, what it was computed over, one row\n * per class of finding whether or not it fired, one row per scenario, then the\n * findings themselves and the commands that clear them. Output is always plain,\n * with no rendering framework in its path at all: this is a report that gets\n * pasted into a pull request and read out of a CI log, and neither of those is\n * a terminal.\n */\nexport async function runCheck(options: CheckOptions): Promise<number> {\n if (options.depth === \"static\") {\n throw new UsageError(\n \"check --depth static has nothing to compare — a baseline is a projection, and \" +\n \"at this depth nothing is mounted. Use --depth runtime for drift alone, or full for both.\",\n );\n }\n\n const analysis: AnalysisOptions = {\n configPath: options.configPath,\n depth: options.depth,\n ...(options.scenario ? { scenario: options.scenario } : {}),\n ...(options.scope ? { scope: options.scope } : {}),\n ...(options.tsconfig ? { tsconfig: options.tsconfig } : {}),\n ...(options.baselineDir ? { baselineDir: options.baselineDir } : {}),\n };\n\n const inventory = readInventory(analysis);\n // No streaming here, unlike `inspect`. A report is read top-down and has to\n // lead with its findings, which means every finding has to exist first.\n const runtime = await mountScenarios(analysis);\n if (!runtime) throw new UsageError(\"check needs a mount, and this depth performs none\");\n\n const drifted: ScenarioDrift[] = [];\n for (const result of runtime.results) {\n const path = baselinePath(runtime.baselineDir, result.scenario);\n const expected = readBaseline(path);\n if (expected === undefined) {\n drifted.push({ scenario: result.scenario, missingBaseline: true, entries: [] });\n continue;\n }\n const actual = scenarioBaseline(result);\n const entries = annotate(diff(expected, actual), actual, expected);\n if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });\n }\n\n const committedScenarios = readScenarioManifest(runtime.baselineDir);\n const declared = [...runtime.declaredScenarios].sort();\n const scenarioManifestMismatch =\n committedScenarios === undefined ||\n JSON.stringify(committedScenarios) !== JSON.stringify(declared);\n const reserved = new Set([SCENARIO_MANIFEST_FILE, ALLOWLIST_FILE, UNREAD_ALLOWLIST_FILE]);\n const staleBaselineFiles = (\n existsSync(runtime.baselineDir) ? readdirSync(runtime.baselineDir, { withFileTypes: true }) : []\n )\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\") && !reserved.has(entry.name))\n .map((entry) => entry.name.slice(0, -5))\n .filter((scenario) => !runtime.declaredScenarios.includes(scenario))\n .sort();\n\n const rejected: RejectedScenario[] = runtime.results\n .filter((result) => result.rejections.length > 0)\n .map((result) => ({ scenario: result.scenario, rejections: result.rejections }));\n\n const coverage = joinCoverage(inventory, runtime, analysis);\n const coverageFailed =\n coverage !== undefined &&\n coverageExitCode(coverage, { allowUnresolved: options.allowUnresolved === true }) !== 0;\n const couldNotRun = runtime.failures.length > 0;\n const ok =\n drifted.length === 0 &&\n !coverageFailed &&\n !couldNotRun &&\n rejected.length === 0 &&\n !scenarioManifestMismatch &&\n staleBaselineFiles.length === 0;\n\n if (options.json) {\n write(\n JSON.stringify(\n {\n ok,\n drifted,\n failures: runtime.failures,\n rejected,\n scenarioManifest: {\n expected: declared,\n committed: committedScenarios ?? null,\n staleBaselines: staleBaselineFiles,\n },\n coverage: coverageReport(coverage),\n },\n null,\n 2,\n ),\n );\n return couldNotRun ? 2 : ok ? 0 : 1;\n }\n\n // \"No baseline\" is not drift, and filing it under a heading that says the\n // surface changed would be a claim about a comparison that never happened.\n const missing = drifted.filter((entry) => entry.missingBaseline);\n const changed = drifted.filter((entry) => !entry.missingBaseline);\n\n const baselineOf = (scenario: string): string => {\n const entry = drifted.find((candidate) => candidate.scenario === scenario);\n if (!entry) return \"current\";\n if (entry.missingBaseline) return \"missing\";\n return `drift (${entry.entries.length})`;\n };\n // One row per scenario the run attempted, in config order — including the\n // ones that threw, which otherwise appear only at the bottom of the report.\n const stats: ScenarioStats[] = runtime.scenarios.map((scenario) => {\n const result = runtime.results.find((candidate) => candidate.scenario === scenario);\n return result\n ? { ...scenarioStats(result), baseline: baselineOf(scenario) }\n : {\n scenario,\n callable: 0,\n disabled: 0,\n hidden: 0,\n rejected: 0,\n failed: true,\n ...(runtime.failures.find((failure) => failure.scenario === scenario)?.message\n ? {\n failure: runtime.failures.find((failure) => failure.scenario === scenario)!.message,\n }\n : {}),\n };\n });\n\n const overview = renderCheckOverviewPlain({\n status: couldNotRun ? \"ERROR\" : ok ? \"PASS\" : \"FAIL\",\n ...(coverage ? { coverage } : {}),\n unresolvedAllowed: options.allowUnresolved === true,\n baselineCurrent: Math.max(0, runtime.results.length - drifted.length),\n baselineTotal: runtime.scenarios.length,\n scenarioManifestOk: !scenarioManifestMismatch && staleBaselineFiles.length === 0,\n rejected: rejected.reduce((sum, entry) => sum + entry.rejections.length, 0),\n mountFailures: runtime.failures.length,\n scenarios: runtime.scenarios,\n context: {\n configPath: options.configPath,\n depth: options.depth,\n ...(runtime.scope ? { scope: runtime.scope } : {}),\n },\n stats,\n });\n if (ok) write(overview);\n else writeError(overview);\n\n // The gap leads, because it is the finding this command could not previously\n // make at all. Drift follows, because it is the one it always could.\n if (coverage) {\n const rendered = renderCoveragePlain(coverage, {\n compact: true,\n ...(options.detail ? { detail: true } : {}),\n });\n if (rendered) {\n if (coverageFailed) writeError(`\\n${rendered}`);\n else write(`\\n${rendered}`);\n }\n }\n\n const sections: FindingSection[] = [];\n const steps: string[] = [];\n if (coverage && coverage.unreached.length > 0) {\n steps.push(\n `mount the ${coverage.unreached.length} unreached capabilit${\n coverage.unreached.length === 1 ? \"y\" : \"ies\"\n } from a scenario, or record the decision in ${displayPath(coverage.allowlistPath)}`,\n );\n }\n if (coverage && coverage.unresolved.length > 0 && options.allowUnresolved !== true) {\n steps.push(\n `make the ${coverage.unresolved.length} unread call site${\n coverage.unresolved.length === 1 ? \"\" : \"s\"\n } readable, or paste each printed key into ${displayPath(coverage.unreadAllowlistPath)}`,\n );\n }\n\n if (rejected.length > 0) {\n const total = rejected.reduce((sum, entry) => sum + entry.rejections.length, 0);\n sections.push({\n title: \"REJECTED REGISTRATIONS\",\n gloss: \"the runtime refused authored surface during the mount\",\n count: total,\n lines: rejected.flatMap((entry) =>\n entry.rejections.map(\n (rejection) =>\n `${entry.scenario}: ${rejection.componentType}@${rejection.instanceId} (${rejection.reason})`,\n ),\n ),\n hint:\n \"a dead handle registers nothing — give the second registration its own instanceId, \" +\n \"or remove the duplicated component type\",\n });\n steps.push(\n `resolve ${total} rejected registration${total === 1 ? \"\" : \"s\"} — the capabilities ` +\n \"behind them reach no agent\",\n );\n }\n\n if (scenarioManifestMismatch || staleBaselineFiles.length > 0) {\n sections.push({\n title: \"SCENARIO DRIFT\",\n gloss: \"the committed baselines do not match the config\",\n count: 0,\n lines: [\n `config: ${declared.join(\", \")}`,\n `manifest: ${committedScenarios?.join(\", \") ?? \"missing\"}`,\n ...(staleBaselineFiles.length > 0\n ? [`stale: ${staleBaselineFiles.join(\", \")}`]\n : []),\n ],\n hint:\n \"run `agent-surface snapshot`, commit the manifest, and delete any baseline for a \" +\n \"scenario the config no longer declares\",\n });\n }\n\n if (missing.length > 0) {\n sections.push({\n title: \"NO BASELINE\",\n gloss: \"nothing to compare against, which is not the same as a match\",\n count: missing.length,\n lines: missing.map(\n (entry) =>\n `${entry.scenario}: ${displayPath(\n baselinePath(runtime.baselineDir, entry.scenario),\n )} does not exist`,\n ),\n hint: \"run `agent-surface snapshot` and commit the files it writes\",\n });\n }\n\n if (changed.length > 0) {\n sections.push({\n title: \"DRIFT\",\n gloss: \"the surface changed against its baseline\",\n count: changed.length,\n lines: changed.flatMap((entry) => renderDriftPlain(entry.scenario, entry.entries)),\n hint:\n \"review the change, then `agent-surface snapshot` to accept it\" +\n (runtime.scope\n ? \". A scope filters the projection while baselines are written from whatever \" +\n \"scope wrote them, so re-check without --scope before believing this one\"\n : \"\"),\n });\n }\n\n const baselinesStale =\n missing.length > 0 ||\n changed.length > 0 ||\n scenarioManifestMismatch ||\n staleBaselineFiles.length > 0;\n if (baselinesStale) {\n steps.push(\n \"`agent-surface snapshot`, then commit .agent-surface/ — this accepts the surface \" +\n \"above as reviewed\",\n );\n }\n\n if (sections.length > 0) writeError(`\\n${renderSectionsPlain(sections)}`);\n\n if (couldNotRun) {\n writeError(`\\n${renderFailuresPlain(runtime.failures)}`);\n if (inventory) writeError(`\\n${renderNoVerdictPlain(runtime.failures)}`);\n // First, and above every other remedy: nothing else in this report can be\n // trusted while a scenario the config declares never ran.\n steps.unshift(\n `fix the mount for ${runtime.failures\n .map((failure) => failure.scenario)\n .join(\", \")} — every count above is missing whatever ${\n runtime.failures.length === 1 ? \"it\" : \"they\"\n } would have surfaced`,\n );\n }\n\n // Last, and only when something failed: the tail of a CI log is what a reader\n // sees first, and a list of findings without the commands that clear them\n // leaves the reader to derive those from six different sections.\n if (!ok && steps.length > 0) writeError(`\\n${renderNextStepsPlain(steps)}`);\n\n return couldNotRun ? 2 : ok ? 0 : 1;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -141,7 +141,7 @@ var cached;
|
|
|
141
141
|
async function loadInk() {
|
|
142
142
|
if (cached !== void 0) return cached;
|
|
143
143
|
try {
|
|
144
|
-
cached = await import("./ink-
|
|
144
|
+
cached = await import("./ink-QR7X7TAC.js");
|
|
145
145
|
} catch {
|
|
146
146
|
cached = null;
|
|
147
147
|
}
|
|
@@ -153,6 +153,14 @@ async function paint(element) {
|
|
|
153
153
|
instance.unmount();
|
|
154
154
|
await instance.waitUntilExit();
|
|
155
155
|
}
|
|
156
|
+
async function transient(element) {
|
|
157
|
+
const { render } = await import("ink");
|
|
158
|
+
const instance = render(element);
|
|
159
|
+
return () => {
|
|
160
|
+
instance.clear();
|
|
161
|
+
instance.unmount();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
156
164
|
|
|
157
165
|
export {
|
|
158
166
|
DEPTHS,
|
|
@@ -164,6 +172,7 @@ export {
|
|
|
164
172
|
write,
|
|
165
173
|
writeError,
|
|
166
174
|
loadInk,
|
|
167
|
-
paint
|
|
175
|
+
paint,
|
|
176
|
+
transient
|
|
168
177
|
};
|
|
169
|
-
//# sourceMappingURL=chunk-
|
|
178
|
+
//# sourceMappingURL=chunk-3AJ343NA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contract.ts","../src/load.ts","../src/output.ts"],"sourcesContent":["/**\n * The vocabulary every layer shares, and nothing else.\n *\n * It is its own module because `bin.ts` needs both of these before it has\n * decided which command to run, and everything else in this package pulls in\n * either the TypeScript compiler or Vite the moment it is imported. A `--help`\n * that boots a TypeScript program to print a paragraph is a `--help` nobody\n * runs twice.\n */\n\nexport const DEPTHS = [\"static\", \"runtime\", \"full\"] as const;\n\n/**\n * How much of the surface a command is asked to compute.\n *\n * A presentation surface has two sources of truth and every command needs some\n * mix of both — the **catalog** this codebase authors, which is static, and the\n * **projection** a mounted scenario surfaces, which is not. Splitting those\n * across separate commands is what let a green `check` sit on top of a route no\n * scenario visits, so the split lives here instead.\n *\n * `static` reads the TypeScript program and mounts nothing — no Vite server, no\n * jsdom, no scenarios. It is the only depth that survives an app which will not\n * mount, and the only one that needs no scenarios to exist yet.\n *\n * `runtime` mounts and skips the program read, for a repository whose tsconfig\n * is wide enough that booting it costs more than the answer is worth.\n *\n * `full` does both and joins them, which is the only depth that can answer\n * *did we author something no scenario reaches*. It is the default because a\n * tool that has to be asked for the complete answer mostly gives the\n * incomplete one.\n */\nexport type Depth = (typeof DEPTHS)[number];\n\nexport function isDepth(value: unknown): value is Depth {\n return typeof value === \"string\" && (DEPTHS as readonly string[]).includes(value);\n}\n\n/**\n * The caller asked for something impossible — as opposed to the app being\n * broken, which is what a mount failure is. Both exit `2`: CI has to tell \"the\n * surface changed\" apart from \"the tool never ran\", and these are both the\n * second one.\n */\nexport class UsageError extends Error {}\n","import { existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createServer, type ViteDevServer } from \"vite\";\nimport { ViteNodeServer } from \"vite-node/server\";\nimport { ViteNodeRunner } from \"vite-node/client\";\nimport { installSourcemapsSupport } from \"vite-node/source-map\";\nimport type { CollectOptions, CollectResult } from \"./collect.js\";\nimport type { SurfaceConfig } from \"./config.js\";\n\nconst CONFIG_NAMES = [\n \"agent-surface.config.tsx\",\n \"agent-surface.config.ts\",\n \"agent-surface.config.mjs\",\n \"agent-surface.config.js\",\n];\n\n/** Walks up from `from` looking for an `agent-surface.config.*`. */\nexport function findConfig(from: string = process.cwd()): string | undefined {\n let dir = resolve(from);\n for (;;) {\n for (const name of CONFIG_NAMES) {\n const candidate = join(dir, name);\n if (existsSync(candidate)) return candidate;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/** `dist/collect.js` when installed; `src/collect.ts` when run from source. */\nfunction collectorPath(): string {\n for (const ext of [\"js\", \"ts\"]) {\n const candidate = fileURLToPath(new URL(`./collect.${ext}`, import.meta.url));\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\"could not locate the agent-surface collector module\");\n}\n\nexport interface SurfaceRunner {\n config: SurfaceConfig;\n scenarioNames: string[];\n collect(options: CollectOptions): Promise<CollectResult>;\n close(): Promise<void>;\n}\n\n/**\n * Boots a Vite dev server on the app's own config, so the config file and the\n * app modules it imports are transformed and resolved exactly as the app\n * resolves them — its aliases, its plugins, its TSX.\n */\nexport async function createSurfaceRunner(configPath: string): Promise<SurfaceRunner> {\n const absoluteConfig = isAbsolute(configPath) ? configPath : resolve(configPath);\n if (!existsSync(absoluteConfig)) {\n throw new Error(`config not found: ${absoluteConfig}`);\n }\n const root = dirname(absoluteConfig);\n\n let server: ViteDevServer;\n try {\n server = await createServer({\n root,\n logLevel: \"error\",\n // `serve` so plugins behave as they do in dev; nothing is ever served.\n server: { middlewareMode: true, watch: null, fs: { strict: false } },\n optimizeDeps: { noDiscovery: true, include: [] },\n resolve: {\n // Both halves of the graph must agree on these. React because two\n // copies break hooks; core because `explainSurface` finds the registry\n // through a Symbol, which is per-module-instance (see collect.ts).\n dedupe: [\n \"react\",\n \"react-dom\",\n \"@agent-surface/core\",\n \"@agent-surface/react\",\n \"@agent-surface/testing\",\n ],\n },\n });\n } catch (error) {\n throw new Error(\n `could not start Vite for ${root}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n try {\n await server.pluginContainer.buildStart({});\n } catch {\n // Vite keeps moving this; a plugin that needs buildStart will say so itself.\n }\n\n const nodeServer = new ViteNodeServer(server);\n installSourcemapsSupport({ getSourceMap: (source) => nodeServer.getSourceMap(source) });\n\n const runner = new ViteNodeRunner({\n root: server.config.root,\n base: server.config.base,\n fetchModule: (id) => nodeServer.fetchModule(id),\n resolveId: (id, importer) => nodeServer.resolveId(id, importer),\n });\n\n const close = async (): Promise<void> => {\n await server.close();\n };\n\n try {\n const configModule = (await runner.executeFile(absoluteConfig)) as {\n default?: SurfaceConfig;\n };\n const config = configModule.default;\n if (!config || typeof config.mount !== \"function\") {\n throw new Error(\n `${absoluteConfig} must \\`export default defineSurface({ mount, scenarios })\\``,\n );\n }\n const scenarioNames = Object.keys(config.scenarios ?? {});\n if (scenarioNames.length === 0) {\n throw new Error(`${absoluteConfig} defines no scenarios`);\n }\n\n // Same runner ⇒ same module graph ⇒ the collector shares React and core\n // with the app tree it is about to mount.\n const collector = (await runner.executeFile(collectorPath())) as {\n collect(config: SurfaceConfig, options: CollectOptions): Promise<CollectResult>;\n };\n\n return {\n config,\n scenarioNames,\n collect: async (options) => {\n // Scoped to the mount, never process-wide: `act()` needs it, and Ink\n // renders its own React tree afterwards — with the flag still set,\n // every frame of the CLI's own UI prints React's \"not wrapped in\n // act(...)\" warning at the user.\n const globals = globalThis as Record<string, unknown>;\n const previous = globals[\"IS_REACT_ACT_ENVIRONMENT\"];\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = true;\n try {\n return await collector.collect(config, options);\n } finally {\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = previous;\n }\n },\n close,\n };\n } catch (error) {\n await close();\n throw error;\n }\n}\n","import type { ReactElement } from \"react\";\n\nexport interface OutputFlags {\n plain?: boolean;\n json?: boolean;\n}\n\n/**\n * Terminal-aware only when there is a terminal. Piped output, `--plain`, `CI`\n * and `NO_COLOR` all fall back to plain text — a CLI whose output changes shape\n * when redirected is unusable in a build log.\n */\nexport function isPlain(flags: OutputFlags): boolean {\n if (flags.json) return true;\n if (flags.plain) return true;\n if (process.env[\"CI\"]) return true;\n if (process.env[\"NO_COLOR\"]) return true;\n if (process.stdout.isTTY !== true) return true;\n // A TTY that cannot report its width (some CI ptys, `script` on macOS) makes\n // Ink lay out at zero columns and emit one character per line. Plain text is\n // the only honest rendering for a terminal whose size is unknown.\n return !process.stdout.columns;\n}\n\nexport function write(text: string): void {\n process.stdout.write(`${text}\\n`);\n}\n\nexport function writeError(text: string): void {\n process.stderr.write(`${text}\\n`);\n}\n\ntype InkModule = typeof import(\"./render/ink.js\");\n\nlet cached: InkModule | null | undefined;\n\n/**\n * Loads the Ink renderer, or returns `null` when it cannot run here.\n *\n * Two reasons this is lazy rather than a top-level import. It keeps `--plain`\n * and `--json` from paying for a terminal UI they never draw — and Ink drives\n * React through `react-reconciler`, which reads React 19 internals, so a host\n * that pins React 18 globally cannot load it at all. Neither is a reason to\n * fail a command that was about to print text.\n */\nexport async function loadInk(): Promise<InkModule | null> {\n if (cached !== undefined) return cached;\n try {\n cached = await import(\"./render/ink.js\");\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** Paints an Ink element once and returns when the frame has been flushed. */\nexport async function paint(element: ReactElement): Promise<void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n instance.unmount();\n await instance.waitUntilExit();\n}\n\n/** A live Ink frame (spinner) that is cleared before the real output lands. */\nexport async function transient(element: ReactElement): Promise<() => void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n return () => {\n instance.clear();\n instance.unmount();\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/contract.ts","../src/load.ts","../src/output.ts"],"sourcesContent":["/**\n * The vocabulary every layer shares, and nothing else.\n *\n * It is its own module because `bin.ts` needs both of these before it has\n * decided which command to run, and everything else in this package pulls in\n * either the TypeScript compiler or Vite the moment it is imported. A `--help`\n * that boots a TypeScript program to print a paragraph is a `--help` nobody\n * runs twice.\n */\n\nexport const DEPTHS = [\"static\", \"runtime\", \"full\"] as const;\n\n/**\n * How much of the surface a command is asked to compute.\n *\n * A presentation surface has two sources of truth and every command needs some\n * mix of both — the **catalog** this codebase authors, which is static, and the\n * **projection** a mounted scenario surfaces, which is not. Splitting those\n * across separate commands is what let a green `check` sit on top of a route no\n * scenario visits, so the split lives here instead.\n *\n * `static` reads the TypeScript program and mounts nothing — no Vite server, no\n * jsdom, no scenarios. It is the only depth that survives an app which will not\n * mount, and the only one that needs no scenarios to exist yet.\n *\n * `runtime` mounts and skips the program read, for a repository whose tsconfig\n * is wide enough that booting it costs more than the answer is worth.\n *\n * `full` does both and joins them, which is the only depth that can answer\n * *did we author something no scenario reaches*. It is the default because a\n * tool that has to be asked for the complete answer mostly gives the\n * incomplete one.\n */\nexport type Depth = (typeof DEPTHS)[number];\n\nexport function isDepth(value: unknown): value is Depth {\n return typeof value === \"string\" && (DEPTHS as readonly string[]).includes(value);\n}\n\n/**\n * The caller asked for something impossible — as opposed to the app being\n * broken, which is what a mount failure is. Both exit `2`: CI has to tell \"the\n * surface changed\" apart from \"the tool never ran\", and these are both the\n * second one.\n */\nexport class UsageError extends Error {}\n","import { existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createServer, type ViteDevServer } from \"vite\";\nimport { ViteNodeServer } from \"vite-node/server\";\nimport { ViteNodeRunner } from \"vite-node/client\";\nimport { installSourcemapsSupport } from \"vite-node/source-map\";\nimport type { CollectOptions, CollectResult } from \"./collect.js\";\nimport type { SurfaceConfig } from \"./config.js\";\n\nconst CONFIG_NAMES = [\n \"agent-surface.config.tsx\",\n \"agent-surface.config.ts\",\n \"agent-surface.config.mjs\",\n \"agent-surface.config.js\",\n];\n\n/** Walks up from `from` looking for an `agent-surface.config.*`. */\nexport function findConfig(from: string = process.cwd()): string | undefined {\n let dir = resolve(from);\n for (;;) {\n for (const name of CONFIG_NAMES) {\n const candidate = join(dir, name);\n if (existsSync(candidate)) return candidate;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/** `dist/collect.js` when installed; `src/collect.ts` when run from source. */\nfunction collectorPath(): string {\n for (const ext of [\"js\", \"ts\"]) {\n const candidate = fileURLToPath(new URL(`./collect.${ext}`, import.meta.url));\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\"could not locate the agent-surface collector module\");\n}\n\nexport interface SurfaceRunner {\n config: SurfaceConfig;\n scenarioNames: string[];\n collect(options: CollectOptions): Promise<CollectResult>;\n close(): Promise<void>;\n}\n\n/**\n * Boots a Vite dev server on the app's own config, so the config file and the\n * app modules it imports are transformed and resolved exactly as the app\n * resolves them — its aliases, its plugins, its TSX.\n */\nexport async function createSurfaceRunner(configPath: string): Promise<SurfaceRunner> {\n const absoluteConfig = isAbsolute(configPath) ? configPath : resolve(configPath);\n if (!existsSync(absoluteConfig)) {\n throw new Error(`config not found: ${absoluteConfig}`);\n }\n const root = dirname(absoluteConfig);\n\n let server: ViteDevServer;\n try {\n server = await createServer({\n root,\n logLevel: \"error\",\n // `serve` so plugins behave as they do in dev; nothing is ever served.\n server: { middlewareMode: true, watch: null, fs: { strict: false } },\n optimizeDeps: { noDiscovery: true, include: [] },\n resolve: {\n // Both halves of the graph must agree on these. React because two\n // copies break hooks; core because `explainSurface` finds the registry\n // through a Symbol, which is per-module-instance (see collect.ts).\n dedupe: [\n \"react\",\n \"react-dom\",\n \"@agent-surface/core\",\n \"@agent-surface/react\",\n \"@agent-surface/testing\",\n ],\n },\n });\n } catch (error) {\n throw new Error(\n `could not start Vite for ${root}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n try {\n await server.pluginContainer.buildStart({});\n } catch {\n // Vite keeps moving this; a plugin that needs buildStart will say so itself.\n }\n\n const nodeServer = new ViteNodeServer(server);\n installSourcemapsSupport({ getSourceMap: (source) => nodeServer.getSourceMap(source) });\n\n const runner = new ViteNodeRunner({\n root: server.config.root,\n base: server.config.base,\n fetchModule: (id) => nodeServer.fetchModule(id),\n resolveId: (id, importer) => nodeServer.resolveId(id, importer),\n });\n\n const close = async (): Promise<void> => {\n await server.close();\n };\n\n try {\n const configModule = (await runner.executeFile(absoluteConfig)) as {\n default?: SurfaceConfig;\n };\n const config = configModule.default;\n if (!config || typeof config.mount !== \"function\") {\n throw new Error(\n `${absoluteConfig} must \\`export default defineSurface({ mount, scenarios })\\``,\n );\n }\n const scenarioNames = Object.keys(config.scenarios ?? {});\n if (scenarioNames.length === 0) {\n throw new Error(`${absoluteConfig} defines no scenarios`);\n }\n\n // Same runner ⇒ same module graph ⇒ the collector shares React and core\n // with the app tree it is about to mount.\n const collector = (await runner.executeFile(collectorPath())) as {\n collect(config: SurfaceConfig, options: CollectOptions): Promise<CollectResult>;\n };\n\n return {\n config,\n scenarioNames,\n collect: async (options) => {\n // Scoped to the mount, never process-wide: `act()` needs it, and Ink\n // renders its own React tree afterwards — with the flag still set,\n // every frame of the CLI's own UI prints React's \"not wrapped in\n // act(...)\" warning at the user.\n const globals = globalThis as Record<string, unknown>;\n const previous = globals[\"IS_REACT_ACT_ENVIRONMENT\"];\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = true;\n try {\n return await collector.collect(config, options);\n } finally {\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = previous;\n }\n },\n close,\n };\n } catch (error) {\n await close();\n throw error;\n }\n}\n","import type { ReactElement } from \"react\";\n\nexport interface OutputFlags {\n plain?: boolean;\n json?: boolean;\n}\n\n/**\n * Terminal-aware only when there is a terminal. Piped output, `--plain`, `CI`\n * and `NO_COLOR` all fall back to plain text — a CLI whose output changes shape\n * when redirected is unusable in a build log.\n */\nexport function isPlain(flags: OutputFlags): boolean {\n if (flags.json) return true;\n if (flags.plain) return true;\n if (process.env[\"CI\"]) return true;\n if (process.env[\"NO_COLOR\"]) return true;\n if (process.stdout.isTTY !== true) return true;\n // A TTY that cannot report its width (some CI ptys, `script` on macOS) makes\n // Ink lay out at zero columns and emit one character per line. Plain text is\n // the only honest rendering for a terminal whose size is unknown.\n return !process.stdout.columns;\n}\n\nexport function write(text: string): void {\n process.stdout.write(`${text}\\n`);\n}\n\nexport function writeError(text: string): void {\n process.stderr.write(`${text}\\n`);\n}\n\ntype InkModule = typeof import(\"./render/ink.js\");\n\nlet cached: InkModule | null | undefined;\n\n/**\n * Loads the Ink renderer, or returns `null` when it cannot run here.\n *\n * Two reasons this is lazy rather than a top-level import. It keeps `--plain`\n * and `--json` from paying for a terminal UI they never draw — and Ink drives\n * React through `react-reconciler`, which reads React 19 internals, so a host\n * that pins React 18 globally cannot load it at all. Neither is a reason to\n * fail a command that was about to print text.\n */\nexport async function loadInk(): Promise<InkModule | null> {\n if (cached !== undefined) return cached;\n try {\n cached = await import(\"./render/ink.js\");\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** Paints an Ink element once and returns when the frame has been flushed. */\nexport async function paint(element: ReactElement): Promise<void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n instance.unmount();\n await instance.waitUntilExit();\n}\n\n/** A live Ink frame (spinner) that is cleared before the real output lands. */\nexport async function transient(element: ReactElement): Promise<() => void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n return () => {\n instance.clear();\n instance.unmount();\n };\n}\n"],"mappings":"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","names":[]}
|