@agent-surface/cli 0.9.0 → 0.9.1
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/dist/bin.js +37 -11
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
package/dist/bin.js
CHANGED
|
@@ -14,7 +14,7 @@ import { parseArgs } from "util";
|
|
|
14
14
|
import { JSDOM } from "jsdom";
|
|
15
15
|
function installDom(url = "http://localhost/") {
|
|
16
16
|
const globals = globalThis;
|
|
17
|
-
if (typeof globals["document"] !== "undefined") return
|
|
17
|
+
if (typeof globals["document"] !== "undefined") return;
|
|
18
18
|
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
|
19
19
|
url,
|
|
20
20
|
pretendToBeVisual: true
|
|
@@ -32,9 +32,6 @@ function installDom(url = "http://localhost/") {
|
|
|
32
32
|
Object.defineProperty(globals, key, { value: window[key], configurable: true });
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return noop;
|
|
36
|
-
}
|
|
37
|
-
function noop() {
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
// src/bin.ts
|
|
@@ -107,7 +104,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
107
104
|
);
|
|
108
105
|
return 2;
|
|
109
106
|
}
|
|
110
|
-
|
|
107
|
+
installDom();
|
|
111
108
|
try {
|
|
112
109
|
const shared = {
|
|
113
110
|
configPath,
|
|
@@ -137,8 +134,6 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
137
134
|
writeError(error.stack);
|
|
138
135
|
}
|
|
139
136
|
return 1;
|
|
140
|
-
} finally {
|
|
141
|
-
uninstallDom();
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
139
|
async function readVersion() {
|
|
@@ -159,14 +154,45 @@ function invokedAsBinary() {
|
|
|
159
154
|
return false;
|
|
160
155
|
}
|
|
161
156
|
}
|
|
157
|
+
var GRACE_MS = 1e3;
|
|
158
|
+
var OWN_STDIO = /* @__PURE__ */ new Set(["TTYWrap", "PipeWrap", "FileWrap"]);
|
|
159
|
+
function heldHandles() {
|
|
160
|
+
const active = process.getActiveResourcesInfo?.() ?? [];
|
|
161
|
+
return active.filter((resource) => !OWN_STDIO.has(resource));
|
|
162
|
+
}
|
|
163
|
+
async function flushOutput() {
|
|
164
|
+
const drained = Promise.all(
|
|
165
|
+
[process.stdout, process.stderr].map(
|
|
166
|
+
(stream) => new Promise((resolve) => {
|
|
167
|
+
if (stream.writableLength === 0) resolve();
|
|
168
|
+
else stream.write("", () => resolve());
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
const deadline = new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, 2e3).unref();
|
|
174
|
+
});
|
|
175
|
+
await Promise.race([drained, deadline]);
|
|
176
|
+
}
|
|
177
|
+
function exitWhenWedged(code) {
|
|
178
|
+
process.exitCode = code;
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
const held = heldHandles();
|
|
181
|
+
if (held.length > 0) {
|
|
182
|
+
const kinds = [...new Set(held)].sort().join(", ");
|
|
183
|
+
writeError(
|
|
184
|
+
`agent-surface: the output above is complete, but ${held.length} handle(s) are still open (${kinds}) \u2014 something started during the mount is still running, so this command would have waited instead of exiting. Common causes: a polling interval, a websocket, or a data layer whose cache timer outlives the render. Exiting ${code}.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
void flushOutput().then(() => process.exit(code));
|
|
188
|
+
}, GRACE_MS).unref();
|
|
189
|
+
}
|
|
162
190
|
if (invokedAsBinary()) {
|
|
163
191
|
main().then(
|
|
164
|
-
(code) =>
|
|
165
|
-
process.exitCode = code;
|
|
166
|
-
},
|
|
192
|
+
(code) => exitWhenWedged(code),
|
|
167
193
|
(error) => {
|
|
168
194
|
writeError(error instanceof Error ? error.message : String(error));
|
|
169
|
-
|
|
195
|
+
exitWhenWedged(1);
|
|
170
196
|
}
|
|
171
197
|
);
|
|
172
198
|
}
|
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 { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — inspect and check the agent surface your app exposes\n\nUsage\n agent-surface inspect [scenario] what an agent can see right now\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail if the surface drifted from the baseline\n\nEvery command covers all scenarios in the config unless you name one.\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 --explain name the policies behind every decision, hidden ones included\n --schemas include input/output JSON Schemas\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`;\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 explain: { type: \"boolean\", default: false },\n schemas: { 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 (![\"inspect\", \"snapshot\", \"check\"].includes(command)) {\n writeError(`unknown command \"${command}\"`);\n writeError(USAGE);\n return 2;\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 \"Create one that points at your app's composition root — 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 these\n // globals at import time — so this must happen before any app module loads.\n const uninstallDom = installDom();\n try {\n const shared = {\n configPath,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\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.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(shared);\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 return 1;\n } finally {\n uninstallDom();\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\nif (invokedAsBinary()) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n process.exitCode = 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: the app tree runs inside the vite-node graph, which\n * shares this realm's globals.\n */\nexport function installDom(url = \"http://localhost/\"): () => void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return noop;\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 return noop;\n}\n\n/**\n * Teardown is deliberately a no-op, 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, so there is nothing to reclaim; only in-process callers (the test\n * suite) run more than one command, and those are exactly the ones this\n * protects.\n */\nfunction noop(): void {}\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 { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — inspect and check the agent surface your app exposes\n\nUsage\n agent-surface inspect [scenario] what an agent can see right now\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail if the surface drifted from the baseline\n\nEvery command covers all scenarios in the config unless you name one.\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 --explain name the policies behind every decision, hidden ones included\n --schemas include input/output JSON Schemas\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`;\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 explain: { type: \"boolean\", default: false },\n schemas: { 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 (![\"inspect\", \"snapshot\", \"check\"].includes(command)) {\n writeError(`unknown command \"${command}\"`);\n writeError(USAGE);\n return 2;\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 \"Create one that points at your app's composition root — 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 these\n // globals at import time — so this must happen before any app module loads.\n // It stays installed for the life of the process, on purpose (see dom.ts).\n installDom();\n try {\n const shared = {\n configPath,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\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.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(shared);\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 return 1;\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-surface/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Inspect and check the agent surface your app exposes — in the terminal and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"react": "^19.1.1",
|
|
31
31
|
"vite": "^7.1.3",
|
|
32
32
|
"vite-node": "^3.2.4",
|
|
33
|
-
"@agent-surface/core": "^0.9.
|
|
34
|
-
"@agent-surface/
|
|
35
|
-
"@agent-surface/
|
|
33
|
+
"@agent-surface/core": "^0.9.1",
|
|
34
|
+
"@agent-surface/testing": "^0.9.1",
|
|
35
|
+
"@agent-surface/react": "^0.9.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@testing-library/react": ">=14",
|