@agent-surface/cli 0.8.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/bin.js +174 -0
- package/dist/bin.js.map +1 -0
- package/dist/check-7Z2VNN5R.js +79 -0
- package/dist/check-7Z2VNN5R.js.map +1 -0
- package/dist/chunk-A2G4QLX5.js +29 -0
- package/dist/chunk-A2G4QLX5.js.map +1 -0
- package/dist/chunk-KZUR4CAU.js +84 -0
- package/dist/chunk-KZUR4CAU.js.map +1 -0
- package/dist/chunk-ODUIFFPM.js +104 -0
- package/dist/chunk-ODUIFFPM.js.map +1 -0
- package/dist/chunk-S2LM3N6D.js +167 -0
- package/dist/chunk-S2LM3N6D.js.map +1 -0
- package/dist/collect.js +35 -0
- package/dist/collect.js.map +1 -0
- package/dist/config-DqK8YqyQ.d.ts +52 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/ink-GCWDO4ML.js +122 -0
- package/dist/ink-GCWDO4ML.js.map +1 -0
- package/dist/inspect-3AXX3VVK.js +202 -0
- package/dist/inspect-3AXX3VVK.js.map +1 -0
- package/dist/snapshot-3D55FL63.js +36 -0
- package/dist/snapshot-3D55FL63.js.map +1 -0
- package/dist/vitest.d.ts +30 -0
- package/dist/vitest.js +9 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wiseair S.r.l.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @agent-surface/cli
|
|
2
|
+
|
|
3
|
+
Inspect and check the agent surface your app exposes — from a terminal, and in CI. Part of [agent-surface](https://github.com/Wiseair-srl/agent-surface).
|
|
4
|
+
|
|
5
|
+
Docs: https://agent-surface-docs.vercel.app/20-cli
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D @agent-surface/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
A presentation surface only exists once components mount — it is a projection of *what is mounted × route × host context × consumer × policy × live `when()`*. So there is nothing static to read, and the CLI mounts your app. It does not re-implement it: the config points at the composition root you already have.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// agent-surface.config.tsx
|
|
19
|
+
import { defineSurface } from "@agent-surface/cli";
|
|
20
|
+
import { createApp } from "./src/agent/setup.js"; // already exists
|
|
21
|
+
import { App } from "./src/app/App.js"; // already exists
|
|
22
|
+
|
|
23
|
+
export default defineSurface({
|
|
24
|
+
mount: ({ user }) => {
|
|
25
|
+
const app = createApp({ environment: "test", user });
|
|
26
|
+
return { registry: app.registry, ui: <App app={app} />, app };
|
|
27
|
+
},
|
|
28
|
+
scenarios: {
|
|
29
|
+
admin: { user: { id: "u_admin", permissions: ["devices:write"] } },
|
|
30
|
+
anonymous: { user: null },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Loading goes through vite-node on your own `vite.config.*`, so your aliases, plugins and TSX resolve exactly as they do in dev.
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
agent-surface inspect [scenario] # what an agent can see right now
|
|
41
|
+
agent-surface snapshot [scenario] # write/refresh the committed baseline
|
|
42
|
+
agent-surface check [scenario] # exit non-zero when the surface drifts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
scenario admin route /devices
|
|
47
|
+
9 callable, 2 visible-disabled
|
|
48
|
+
|
|
49
|
+
devices.drawer (3)
|
|
50
|
+
+ open [local-state, reversible]
|
|
51
|
+
Open the detail drawer for a device
|
|
52
|
+
~ close [local-state, reversible]
|
|
53
|
+
Close the detail drawer
|
|
54
|
+
reason: The drawer is not open
|
|
55
|
+
|
|
56
|
+
authoritative (domain) (1)
|
|
57
|
+
~ devices.disable [destructive, confirmation:required, deviceIds bound+locked]
|
|
58
|
+
Disable the given devices
|
|
59
|
+
reason: Select at least one device first
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Why is my capability missing?
|
|
63
|
+
|
|
64
|
+
`snapshot()` bakes policy outcomes: a `hide` removes the capability *and* the reason, because the existence of a hidden capability is itself information. Correct at the agent boundary, useless when you are the developer. `--explain` answers it:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
agent-surface inspect anonymous --explain
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
0 callable, 0 visible-disabled, 11 hidden
|
|
72
|
+
|
|
73
|
+
hidden by policy (absent from the snapshot) (11)
|
|
74
|
+
- set [devices.filters@default]
|
|
75
|
+
Update one or both filters; omitted fields are unchanged.
|
|
76
|
+
policy authenticated (registry, discovery/authorize): hide
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every policy in the chain, in the order it runs, with its own vote, the layer it came from, and whether its `onDiscovery` threw. Availability is reported apart from the policy votes — *authority hides, state discloses*, and the two failures must never look alike.
|
|
80
|
+
|
|
81
|
+
### The scenarios are not a fixture
|
|
82
|
+
|
|
83
|
+
The same definitions drive your test suite, so "admin on /devices" exists once rather than twice:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import config from "../agent-surface.config.js";
|
|
87
|
+
import { mountScenario } from "@agent-surface/cli/vitest";
|
|
88
|
+
|
|
89
|
+
const { surface, app } = await mountScenario(config, "admin");
|
|
90
|
+
expect(surface).toExpose("view:devices.filters.set");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
Requires `@testing-library/react` and `react-dom` (peers) — it mounts your real tree in jsdom. Anything needing a real browser is out of reach by construction. Output falls back to plain text when piped, or under `--plain`, `CI` and `NO_COLOR`.
|
|
96
|
+
|
|
97
|
+
Full specification: [docs/20](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/20-cli.md).
|
|
98
|
+
|
|
99
|
+
MIT © Wiseair S.r.l.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findConfig,
|
|
4
|
+
write,
|
|
5
|
+
writeError
|
|
6
|
+
} from "./chunk-S2LM3N6D.js";
|
|
7
|
+
|
|
8
|
+
// src/bin.ts
|
|
9
|
+
import { realpathSync } from "fs";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { parseArgs } from "util";
|
|
12
|
+
|
|
13
|
+
// src/dom.ts
|
|
14
|
+
import { JSDOM } from "jsdom";
|
|
15
|
+
function installDom(url = "http://localhost/") {
|
|
16
|
+
const globals = globalThis;
|
|
17
|
+
if (typeof globals["document"] !== "undefined") return noop;
|
|
18
|
+
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
|
19
|
+
url,
|
|
20
|
+
pretendToBeVisual: true
|
|
21
|
+
});
|
|
22
|
+
const { window } = dom;
|
|
23
|
+
for (const key of Object.getOwnPropertyNames(window)) {
|
|
24
|
+
if (key.startsWith("_")) continue;
|
|
25
|
+
if (key in globals) continue;
|
|
26
|
+
const descriptor = Object.getOwnPropertyDescriptor(window, key);
|
|
27
|
+
if (!descriptor) continue;
|
|
28
|
+
Object.defineProperty(globals, key, descriptor);
|
|
29
|
+
}
|
|
30
|
+
for (const key of ["window", "document", "navigator"]) {
|
|
31
|
+
if (!(key in globals)) {
|
|
32
|
+
Object.defineProperty(globals, key, { value: window[key], configurable: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return noop;
|
|
36
|
+
}
|
|
37
|
+
function noop() {
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/bin.ts
|
|
41
|
+
var USAGE = `agent-surface \u2014 inspect and check the agent surface your app exposes
|
|
42
|
+
|
|
43
|
+
Usage
|
|
44
|
+
agent-surface inspect [scenario] what an agent can see right now
|
|
45
|
+
agent-surface snapshot [scenario] write/refresh the committed baseline
|
|
46
|
+
agent-surface check [scenario] fail if the surface drifted from the baseline
|
|
47
|
+
|
|
48
|
+
Options
|
|
49
|
+
--config <path> path to agent-surface.config.* (default: nearest, searching upward)
|
|
50
|
+
--baseline-dir where baselines live (default: .agent-surface next to the config)
|
|
51
|
+
--scope <prefix> restrict to a component-type prefix (repeatable)
|
|
52
|
+
--explain name the policies behind every decision, hidden ones included
|
|
53
|
+
--schemas include input/output JSON Schemas
|
|
54
|
+
--json emit data instead of a rendered view
|
|
55
|
+
--plain force plain text (implied when piped, or under CI / NO_COLOR)
|
|
56
|
+
-h, --help show this
|
|
57
|
+
-v, --version print the version
|
|
58
|
+
`;
|
|
59
|
+
async function main(argv = process.argv.slice(2)) {
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = parseArgs({
|
|
63
|
+
args: argv,
|
|
64
|
+
allowPositionals: true,
|
|
65
|
+
options: {
|
|
66
|
+
config: { type: "string" },
|
|
67
|
+
"baseline-dir": { type: "string" },
|
|
68
|
+
scope: { type: "string", multiple: true },
|
|
69
|
+
explain: { type: "boolean", default: false },
|
|
70
|
+
schemas: { type: "boolean", default: false },
|
|
71
|
+
json: { type: "boolean", default: false },
|
|
72
|
+
plain: { type: "boolean", default: false },
|
|
73
|
+
help: { type: "boolean", short: "h", default: false },
|
|
74
|
+
version: { type: "boolean", short: "v", default: false }
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
writeError(error instanceof Error ? error.message : String(error));
|
|
79
|
+
writeError(USAGE);
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
const { values, positionals } = parsed;
|
|
83
|
+
if (values.help) {
|
|
84
|
+
write(USAGE);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (values.version) {
|
|
88
|
+
write(await readVersion());
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const [command, scenario] = positionals;
|
|
92
|
+
if (!command) {
|
|
93
|
+
write(USAGE);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
if (!["inspect", "snapshot", "check"].includes(command)) {
|
|
97
|
+
writeError(`unknown command "${command}"`);
|
|
98
|
+
writeError(USAGE);
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const configPath = values.config ?? findConfig();
|
|
102
|
+
if (!configPath) {
|
|
103
|
+
writeError(
|
|
104
|
+
"no agent-surface.config.* found (searched upward from the working directory).\nCreate one that points at your app's composition root \u2014 see https://agent-surface-docs.vercel.app/20-cli"
|
|
105
|
+
);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const uninstallDom = installDom();
|
|
109
|
+
try {
|
|
110
|
+
const shared = {
|
|
111
|
+
configPath,
|
|
112
|
+
...scenario ? { scenario } : {},
|
|
113
|
+
...values.scope ? { scope: values.scope } : {},
|
|
114
|
+
...values.json ? { json: true } : {},
|
|
115
|
+
...values.plain ? { plain: true } : {},
|
|
116
|
+
...values["baseline-dir"] ? { baselineDir: values["baseline-dir"] } : {}
|
|
117
|
+
};
|
|
118
|
+
if (command === "inspect") {
|
|
119
|
+
const { runInspect } = await import("./inspect-3AXX3VVK.js");
|
|
120
|
+
return await runInspect({
|
|
121
|
+
...shared,
|
|
122
|
+
...values.explain ? { explain: true } : {},
|
|
123
|
+
...values.schemas ? { schemas: true } : {}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (command === "snapshot") {
|
|
127
|
+
const { runSnapshot } = await import("./snapshot-3D55FL63.js");
|
|
128
|
+
return await runSnapshot(shared);
|
|
129
|
+
}
|
|
130
|
+
const { runCheck } = await import("./check-7Z2VNN5R.js");
|
|
131
|
+
return await runCheck(shared);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
writeError(error instanceof Error ? error.message : String(error));
|
|
134
|
+
if (error instanceof Error && error.stack && process.env["AGENT_SURFACE_DEBUG"]) {
|
|
135
|
+
writeError(error.stack);
|
|
136
|
+
}
|
|
137
|
+
return 1;
|
|
138
|
+
} finally {
|
|
139
|
+
uninstallDom();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function readVersion() {
|
|
143
|
+
try {
|
|
144
|
+
const { readFileSync } = await import("fs");
|
|
145
|
+
const path = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
146
|
+
return JSON.parse(readFileSync(path, "utf8")).version;
|
|
147
|
+
} catch {
|
|
148
|
+
return "unknown";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function invokedAsBinary() {
|
|
152
|
+
const entry = process.argv[1];
|
|
153
|
+
if (!entry) return false;
|
|
154
|
+
try {
|
|
155
|
+
return fileURLToPath(import.meta.url) === realpathSync(entry);
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (invokedAsBinary()) {
|
|
161
|
+
main().then(
|
|
162
|
+
(code) => {
|
|
163
|
+
process.exitCode = code;
|
|
164
|
+
},
|
|
165
|
+
(error) => {
|
|
166
|
+
writeError(error instanceof Error ? error.message : String(error));
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
main
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +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\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":";;;;;;;;AACA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;;;ACH1B,SAAS,aAAa;AAWf,SAAS,WAAW,MAAM,qBAAiC;AAChE,QAAM,UAAU;AAChB,MAAI,OAAO,QAAQ,UAAU,MAAM,YAAa,QAAO;AAEvD,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;AAEA,SAAO;AACT;AAaA,SAAS,OAAa;AAAC;;;AD5CvB,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBd,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,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,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,CAAC,WAAW,YAAY,OAAO,EAAE,SAAS,OAAO,GAAG;AACvD,eAAW,oBAAoB,OAAO,GAAG;AACzC,eAAW,KAAK;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,OAAO,UAAU,WAAW;AAC/C,MAAI,CAAC,YAAY;AACf;AAAA,MACE;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAIA,QAAM,eAAe,WAAW;AAChC,MAAI;AACF,UAAM,SAAS;AAAA,MACb;AAAA,MACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9C,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,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,MAAM;AAAA,EAC9B,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;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa;AAAA,EACf;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;AAEA,IAAI,gBAAgB,GAAG;AACrB,OAAK,EAAE;AAAA,IACL,CAAC,SAAS;AACR,cAAQ,WAAW;AAAA,IACrB;AAAA,IACA,CAAC,UAAmB;AAClB,iBAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,cAAQ,WAAW;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderDiffPlain
|
|
3
|
+
} from "./chunk-KZUR4CAU.js";
|
|
4
|
+
import {
|
|
5
|
+
createSurfaceRunner,
|
|
6
|
+
isPlain,
|
|
7
|
+
loadInk,
|
|
8
|
+
paint,
|
|
9
|
+
write,
|
|
10
|
+
writeError
|
|
11
|
+
} from "./chunk-S2LM3N6D.js";
|
|
12
|
+
import {
|
|
13
|
+
annotate,
|
|
14
|
+
baselineDirFor,
|
|
15
|
+
baselinePath,
|
|
16
|
+
diff,
|
|
17
|
+
normalize,
|
|
18
|
+
readBaseline
|
|
19
|
+
} from "./chunk-ODUIFFPM.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/check.tsx
|
|
22
|
+
import { relative } from "path";
|
|
23
|
+
import { jsx } from "react/jsx-runtime";
|
|
24
|
+
async function runCheck(options) {
|
|
25
|
+
const runner = await createSurfaceRunner(options.configPath);
|
|
26
|
+
try {
|
|
27
|
+
const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;
|
|
28
|
+
const dir = baselineDirFor(options.configPath, options.baselineDir ?? runner.config.baselineDir);
|
|
29
|
+
const drifted = [];
|
|
30
|
+
for (const scenario of scenarios) {
|
|
31
|
+
const result = await runner.collect({
|
|
32
|
+
scenario,
|
|
33
|
+
...options.scope ? { scope: options.scope } : {}
|
|
34
|
+
});
|
|
35
|
+
const path = baselinePath(dir, scenario);
|
|
36
|
+
const expected = readBaseline(path);
|
|
37
|
+
if (expected === void 0) {
|
|
38
|
+
drifted.push({ scenario, missingBaseline: true, entries: [] });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const actual = normalize(result.snapshot);
|
|
42
|
+
const entries = annotate(diff(expected, actual), actual, expected);
|
|
43
|
+
if (entries.length > 0) drifted.push({ scenario, entries });
|
|
44
|
+
}
|
|
45
|
+
if (options.json) {
|
|
46
|
+
write(JSON.stringify({ ok: drifted.length === 0, drifted }, null, 2));
|
|
47
|
+
return drifted.length === 0 ? 0 : 1;
|
|
48
|
+
}
|
|
49
|
+
if (drifted.length === 0) {
|
|
50
|
+
write(`surface matches the baseline (${scenarios.length} scenario${scenarios.length === 1 ? "" : "s"})`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const ink = isPlain(options) ? null : await loadInk();
|
|
54
|
+
for (const entry of drifted) {
|
|
55
|
+
if (entry.missingBaseline) {
|
|
56
|
+
writeError(
|
|
57
|
+
`${entry.scenario}: no baseline at ${relative(
|
|
58
|
+
process.cwd(),
|
|
59
|
+
baselinePath(dir, entry.scenario)
|
|
60
|
+
)} \u2014 run \`agent-surface snapshot\` and commit it`
|
|
61
|
+
);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ink) await paint(/* @__PURE__ */ jsx(ink.Drift, { scenario: entry.scenario, entries: entry.entries }));
|
|
65
|
+
else write(renderDiffPlain(entry.scenario, entry.entries));
|
|
66
|
+
}
|
|
67
|
+
writeError(
|
|
68
|
+
`
|
|
69
|
+
surface drift in ${drifted.length} scenario${drifted.length === 1 ? "" : "s"} \u2014 review the change, then \`agent-surface snapshot\` to accept it`
|
|
70
|
+
);
|
|
71
|
+
return 1;
|
|
72
|
+
} finally {
|
|
73
|
+
await runner.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
runCheck
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=check-7Z2VNN5R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.tsx"],"sourcesContent":["import { relative } from \"node:path\";\nimport { createSurfaceRunner } from \"../load.js\";\nimport {\n annotate,\n baselineDirFor,\n baselinePath,\n diff,\n normalize,\n readBaseline,\n type DiffEntry,\n} from \"../baseline.js\";\nimport { renderDiffPlain } from \"../render/plain.js\";\nimport { isPlain, loadInk, paint, write, writeError } from \"../output.js\";\n\nexport interface CheckOptions {\n configPath: string;\n scenario?: string;\n scope?: string[];\n baselineDir?: string;\n json?: boolean;\n plain?: boolean;\n}\n\ninterface ScenarioDrift {\n scenario: string;\n missingBaseline?: boolean;\n entries: DiffEntry[];\n}\n\n/**\n * Compares every scenario against its committed baseline. Exit code is the\n * point: 0 when the surface is what the repo says it is, 1 when it drifted —\n * so CI fails on an unreviewed change to what agents can see.\n */\nexport async function runCheck(options: CheckOptions): Promise<number> {\n const runner = await createSurfaceRunner(options.configPath);\n try {\n const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;\n const dir = baselineDirFor(options.configPath, options.baselineDir ?? runner.config.baselineDir);\n const drifted: ScenarioDrift[] = [];\n\n for (const scenario of scenarios) {\n const result = await runner.collect({\n scenario,\n ...(options.scope ? { scope: options.scope } : {}),\n });\n const path = baselinePath(dir, scenario);\n const expected = readBaseline(path);\n\n if (expected === undefined) {\n drifted.push({ scenario, missingBaseline: true, entries: [] });\n continue;\n }\n const actual = normalize(result.snapshot);\n const entries = annotate(diff(expected, actual), actual, expected);\n if (entries.length > 0) drifted.push({ scenario, entries });\n }\n\n if (options.json) {\n write(JSON.stringify({ ok: drifted.length === 0, drifted }, null, 2));\n return drifted.length === 0 ? 0 : 1;\n }\n\n if (drifted.length === 0) {\n write(`surface matches the baseline (${scenarios.length} scenario${scenarios.length === 1 ? \"\" : \"s\"})`);\n return 0;\n }\n\n const ink = isPlain(options) ? null : await loadInk();\n for (const entry of drifted) {\n if (entry.missingBaseline) {\n writeError(\n `${entry.scenario}: no baseline at ${relative(\n process.cwd(),\n baselinePath(dir, entry.scenario),\n )} — run \\`agent-surface snapshot\\` and commit it`,\n );\n continue;\n }\n if (ink) await paint(<ink.Drift scenario={entry.scenario} entries={entry.entries} />);\n else write(renderDiffPlain(entry.scenario, entry.entries));\n }\n writeError(\n `\\nsurface drift in ${drifted.length} scenario${drifted.length === 1 ? \"\" : \"s\"} — review the change, then \\`agent-surface snapshot\\` to accept it`,\n );\n return 1;\n } finally {\n await runner.close();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AA+EE;AA7C3B,eAAsB,SAAS,SAAwC;AACrE,QAAM,SAAS,MAAM,oBAAoB,QAAQ,UAAU;AAC3D,MAAI;AACF,UAAM,YAAY,QAAQ,WAAW,CAAC,QAAQ,QAAQ,IAAI,OAAO;AACjE,UAAM,MAAM,eAAe,QAAQ,YAAY,QAAQ,eAAe,OAAO,OAAO,WAAW;AAC/F,UAAM,UAA2B,CAAC;AAElC,eAAW,YAAY,WAAW;AAChC,YAAM,SAAS,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,QACA,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAClD,CAAC;AACD,YAAM,OAAO,aAAa,KAAK,QAAQ;AACvC,YAAM,WAAW,aAAa,IAAI;AAElC,UAAI,aAAa,QAAW;AAC1B,gBAAQ,KAAK,EAAE,UAAU,iBAAiB,MAAM,SAAS,CAAC,EAAE,CAAC;AAC7D;AAAA,MACF;AACA,YAAM,SAAS,UAAU,OAAO,QAAQ;AACxC,YAAM,UAAU,SAAS,KAAK,UAAU,MAAM,GAAG,QAAQ,QAAQ;AACjE,UAAI,QAAQ,SAAS,EAAG,SAAQ,KAAK,EAAE,UAAU,QAAQ,CAAC;AAAA,IAC5D;AAEA,QAAI,QAAQ,MAAM;AAChB,YAAM,KAAK,UAAU,EAAE,IAAI,QAAQ,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC;AACpE,aAAO,QAAQ,WAAW,IAAI,IAAI;AAAA,IACpC;AAEA,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,iCAAiC,UAAU,MAAM,YAAY,UAAU,WAAW,IAAI,KAAK,GAAG,GAAG;AACvG,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ;AACpD,eAAW,SAAS,SAAS;AAC3B,UAAI,MAAM,iBAAiB;AACzB;AAAA,UACE,GAAG,MAAM,QAAQ,oBAAoB;AAAA,YACnC,QAAQ,IAAI;AAAA,YACZ,aAAa,KAAK,MAAM,QAAQ;AAAA,UAClC,CAAC;AAAA,QACH;AACA;AAAA,MACF;AACA,UAAI,IAAK,OAAM,MAAM,oBAAC,IAAI,OAAJ,EAAU,UAAU,MAAM,UAAU,SAAS,MAAM,SAAS,CAAE;AAAA,UAC/E,OAAM,gBAAgB,MAAM,UAAU,MAAM,OAAO,CAAC;AAAA,IAC3D;AACA;AAAA,MACE;AAAA,mBAAsB,QAAQ,MAAM,YAAY,QAAQ,WAAW,IAAI,KAAK,GAAG;AAAA,IACjF;AACA,WAAO;AAAA,EACT,UAAE;AACA,UAAM,OAAO,MAAM;AAAA,EACrB;AACF;","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/mount.ts
|
|
2
|
+
import { act } from "@testing-library/react";
|
|
3
|
+
import { renderAgentSurface } from "@agent-surface/testing/react";
|
|
4
|
+
var DEFAULT_CLI_CONSUMER = { id: "cli", kind: "test" };
|
|
5
|
+
async function mountScenario(config, scenario, options = {}) {
|
|
6
|
+
const props = config.scenarios[scenario];
|
|
7
|
+
if (!props) {
|
|
8
|
+
const known = Object.keys(config.scenarios);
|
|
9
|
+
throw new Error(
|
|
10
|
+
`unknown scenario "${scenario}" \u2014 this config defines ${known.length > 0 ? known.map((name) => `"${name}"`).join(", ") : "none"}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const consumer = options.consumer ?? config.consumer ?? DEFAULT_CLI_CONSUMER;
|
|
14
|
+
const mounted = await config.mount({ ...props, scenario });
|
|
15
|
+
const surface = await renderAgentSurface(mounted.ui, {
|
|
16
|
+
registry: mounted.registry,
|
|
17
|
+
consumer
|
|
18
|
+
});
|
|
19
|
+
await act(async () => {
|
|
20
|
+
});
|
|
21
|
+
await config.settle?.(mounted);
|
|
22
|
+
return { scenario, surface, mounted, app: mounted.app, consumer };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_CLI_CONSUMER,
|
|
27
|
+
mountScenario
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=chunk-A2G4QLX5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mount.ts"],"sourcesContent":["import { act } from \"@testing-library/react\";\nimport { renderAgentSurface, type RenderedAgentSurface } from \"@agent-surface/testing/react\";\nimport type { AgentConsumer } from \"@agent-surface/core\";\nimport type { MountResult, ScenarioProps, SurfaceConfig } from \"./config.js\";\n\nexport const DEFAULT_CLI_CONSUMER: AgentConsumer = { id: \"cli\", kind: \"test\" };\n\nexport interface MountScenarioOptions {\n consumer?: AgentConsumer;\n}\n\nexport interface MountedScenario<TApp> {\n scenario: string;\n surface: RenderedAgentSurface;\n mounted: MountResult<TApp>;\n /**\n * Whatever `mount()` returned as `app`. Typed as `TApp` rather than\n * `TApp | undefined` because a config that never sets it infers `TApp` as\n * `unknown`, and forcing a `!` on every test that *does* set it is worse\n * than trusting the config's own return type.\n */\n app: TApp;\n consumer: AgentConsumer;\n}\n\n/**\n * The one mounting path. `agent-surface inspect` and the Vitest helper both\n * come through here, so a scenario cannot behave one way in CI and another in\n * the terminal — which is the entire reason scenarios live in one file.\n */\nexport async function mountScenario<TScenario extends ScenarioProps, TApp>(\n config: SurfaceConfig<TScenario, TApp>,\n scenario: string,\n options: MountScenarioOptions = {},\n): Promise<MountedScenario<TApp>> {\n const props = config.scenarios[scenario];\n if (!props) {\n const known = Object.keys(config.scenarios);\n throw new Error(\n `unknown scenario \"${scenario}\" — this config defines ${\n known.length > 0 ? known.map((name) => `\"${name}\"`).join(\", \") : \"none\"\n }`,\n );\n }\n\n const consumer = options.consumer ?? config.consumer ?? DEFAULT_CLI_CONSUMER;\n const mounted = await config.mount({ ...props, scenario });\n const surface = await renderAgentSurface(mounted.ui, {\n registry: mounted.registry,\n consumer,\n });\n\n // Mount effects have flushed, but whatever the first render *started* has\n // not settled — the initial fetch that fills a table, typically. This flush\n // drains pending microtasks; `settle` covers anything slower.\n await act(async () => {});\n await config.settle?.(mounted);\n\n return { scenario, surface, mounted, app: mounted.app as TApp, consumer };\n}\n"],"mappings":";AAAA,SAAS,WAAW;AACpB,SAAS,0BAAqD;AAIvD,IAAM,uBAAsC,EAAE,IAAI,OAAO,MAAM,OAAO;AAyB7E,eAAsB,cACpB,QACA,UACA,UAAgC,CAAC,GACD;AAChC,QAAM,QAAQ,OAAO,UAAU,QAAQ;AACvC,MAAI,CAAC,OAAO;AACV,UAAM,QAAQ,OAAO,KAAK,OAAO,SAAS;AAC1C,UAAM,IAAI;AAAA,MACR,qBAAqB,QAAQ,gCAC3B,MAAM,SAAS,IAAI,MAAM,IAAI,CAAC,SAAS,IAAI,IAAI,GAAG,EAAE,KAAK,IAAI,IAAI,MACnE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,QAAQ,YAAY,OAAO,YAAY;AACxD,QAAM,UAAU,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,SAAS,CAAC;AACzD,QAAM,UAAU,MAAM,mBAAmB,QAAQ,IAAI;AAAA,IACnD,UAAU,QAAQ;AAAA,IAClB;AAAA,EACF,CAAC;AAKD,QAAM,IAAI,YAAY;AAAA,EAAC,CAAC;AACxB,QAAM,OAAO,SAAS,OAAO;AAE7B,SAAO,EAAE,UAAU,SAAS,SAAS,KAAK,QAAQ,KAAa,SAAS;AAC1E;","names":[]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatValue
|
|
3
|
+
} from "./chunk-ODUIFFPM.js";
|
|
4
|
+
|
|
5
|
+
// src/render/plain.ts
|
|
6
|
+
var MARK = { expose: "+", disable: "~", hide: "-" };
|
|
7
|
+
function renderRow(row, lines) {
|
|
8
|
+
const tags = row.tags.length > 0 ? ` [${row.tags.join(", ")}]` : "";
|
|
9
|
+
lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);
|
|
10
|
+
lines.push(` ${row.description}`);
|
|
11
|
+
if (row.reason) lines.push(` reason: ${row.reason}`);
|
|
12
|
+
if (row.policies) {
|
|
13
|
+
if (row.policies.length === 0) {
|
|
14
|
+
lines.push(" policies: none");
|
|
15
|
+
} else {
|
|
16
|
+
for (const policy of row.policies) {
|
|
17
|
+
const vote = policy.discovery ? policy.discovery.decision === "disable" ? `disable \u2014 ${policy.discovery.reason}` : policy.discovery.decision : "no discovery hook";
|
|
18
|
+
const phases = policy.phases.length > 0 ? policy.phases.join("/") : "\u2014";
|
|
19
|
+
const flags = [
|
|
20
|
+
policy.threw ? "THREW" : "",
|
|
21
|
+
policy.confirmationEscalation ? "escalates-confirmation" : ""
|
|
22
|
+
].filter(Boolean).join(", ");
|
|
23
|
+
lines.push(
|
|
24
|
+
` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${flags ? ` [${flags}]` : ""}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (row.availability && !row.availability.available) {
|
|
29
|
+
lines.push(
|
|
30
|
+
` availability: unavailable${row.availability.reason ? ` \u2014 ${row.availability.reason}` : ""}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (row.schemas) {
|
|
35
|
+
if (row.schemas.input !== void 0) {
|
|
36
|
+
lines.push(` input: ${JSON.stringify(row.schemas.input)}`);
|
|
37
|
+
}
|
|
38
|
+
if (row.schemas.output !== void 0) {
|
|
39
|
+
lines.push(` output: ${JSON.stringify(row.schemas.output)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function renderSurfacePlain(view) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
lines.push(`scenario ${view.scenario}${view.route ? ` route ${view.route}` : ""}`);
|
|
46
|
+
lines.push(
|
|
47
|
+
`${view.counts.callable} callable, ${view.counts.disabled} visible-disabled${view.explained ? `, ${view.counts.hidden} hidden` : ""}`
|
|
48
|
+
);
|
|
49
|
+
const populated = view.groups.filter((group) => group.rows.length > 0);
|
|
50
|
+
if (populated.length === 0) {
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push("Nothing is registered for this scenario \u2014 the agent has no surface here.");
|
|
53
|
+
if (!view.explained) {
|
|
54
|
+
lines.push("Re-run with --explain to see whether a policy hid it.");
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
for (const group of populated) {
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(`${group.heading} (${group.rows.length})`);
|
|
61
|
+
for (const row of group.rows) renderRow(row, lines);
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
function renderDiffPlain(scenario, entries) {
|
|
66
|
+
const lines = [`${scenario}: ${entries.length} change${entries.length === 1 ? "" : "s"}`];
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;
|
|
69
|
+
if (entry.kind === "added") lines.push(` + ${where} ${formatValue(entry.after)}`);
|
|
70
|
+
else if (entry.kind === "removed") lines.push(` - ${where} ${formatValue(entry.before)}`);
|
|
71
|
+
else {
|
|
72
|
+
lines.push(` ~ ${where}`);
|
|
73
|
+
lines.push(` before: ${formatValue(entry.before)}`);
|
|
74
|
+
lines.push(` after: ${formatValue(entry.after)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
renderSurfacePlain,
|
|
82
|
+
renderDiffPlain
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=chunk-KZUR4CAU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/render/plain.ts"],"sourcesContent":["import type { CapabilityRow, SurfaceView } from \"./model.js\";\nimport type { DiffEntry } from \"../baseline.js\";\nimport { formatValue } from \"../baseline.js\";\n\n/**\n * The no-colour, no-cursor rendering used when stdout is piped or when\n * `--plain`, `CI` or `NO_COLOR` is set. Same view model as the Ink UI, so the\n * two cannot disagree about what the surface contains.\n */\n\nconst MARK = { expose: \"+\", disable: \"~\", hide: \"-\" } as const;\n\nfunction renderRow(row: CapabilityRow, lines: string[]): void {\n const tags = row.tags.length > 0 ? ` [${row.tags.join(\", \")}]` : \"\";\n lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);\n lines.push(` ${row.description}`);\n if (row.reason) lines.push(` reason: ${row.reason}`);\n\n if (row.policies) {\n if (row.policies.length === 0) {\n lines.push(\" policies: none\");\n } else {\n for (const policy of row.policies) {\n const vote = policy.discovery\n ? policy.discovery.decision === \"disable\"\n ? `disable — ${policy.discovery.reason}`\n : policy.discovery.decision\n : \"no discovery hook\";\n const phases = policy.phases.length > 0 ? policy.phases.join(\"/\") : \"—\";\n const flags = [\n policy.threw ? \"THREW\" : \"\",\n policy.confirmationEscalation ? \"escalates-confirmation\" : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n lines.push(\n ` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${\n flags ? ` [${flags}]` : \"\"\n }`,\n );\n }\n }\n if (row.availability && !row.availability.available) {\n lines.push(\n ` availability: unavailable${\n row.availability.reason ? ` — ${row.availability.reason}` : \"\"\n }`,\n );\n }\n }\n\n if (row.schemas) {\n if (row.schemas.input !== undefined) {\n lines.push(` input: ${JSON.stringify(row.schemas.input)}`);\n }\n if (row.schemas.output !== undefined) {\n lines.push(` output: ${JSON.stringify(row.schemas.output)}`);\n }\n }\n}\n\nexport function renderSurfacePlain(view: SurfaceView): string {\n const lines: string[] = [];\n lines.push(`scenario ${view.scenario}${view.route ? ` route ${view.route}` : \"\"}`);\n lines.push(\n `${view.counts.callable} callable, ${view.counts.disabled} visible-disabled${\n view.explained ? `, ${view.counts.hidden} hidden` : \"\"\n }`,\n );\n\n const populated = view.groups.filter((group) => group.rows.length > 0);\n if (populated.length === 0) {\n lines.push(\"\");\n lines.push(\"Nothing is registered for this scenario — the agent has no surface here.\");\n if (!view.explained) {\n lines.push(\"Re-run with --explain to see whether a policy hid it.\");\n }\n return lines.join(\"\\n\");\n }\n\n for (const group of populated) {\n lines.push(\"\");\n lines.push(`${group.heading} (${group.rows.length})`);\n for (const row of group.rows) renderRow(row, lines);\n }\n return lines.join(\"\\n\");\n}\n\nexport function renderDiffPlain(scenario: string, entries: DiffEntry[]): string {\n const lines = [`${scenario}: ${entries.length} change${entries.length === 1 ? \"\" : \"s\"}`];\n for (const entry of entries) {\n const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;\n if (entry.kind === \"added\") lines.push(` + ${where} ${formatValue(entry.after)}`);\n else if (entry.kind === \"removed\") lines.push(` - ${where} ${formatValue(entry.before)}`);\n else {\n lines.push(` ~ ${where}`);\n lines.push(` before: ${formatValue(entry.before)}`);\n lines.push(` after: ${formatValue(entry.after)}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/baseline.ts
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { serializeSurfaceSnapshot } from "@agent-surface/testing";
|
|
5
|
+
var DEFAULT_BASELINE_DIR = ".agent-surface";
|
|
6
|
+
function baselineDirFor(configPath, configured) {
|
|
7
|
+
return resolve(dirname(configPath), configured ?? DEFAULT_BASELINE_DIR);
|
|
8
|
+
}
|
|
9
|
+
function baselinePath(dir, scenario) {
|
|
10
|
+
return join(dir, `${scenario}.json`);
|
|
11
|
+
}
|
|
12
|
+
function normalize(snapshot) {
|
|
13
|
+
return serializeSurfaceSnapshot(snapshot);
|
|
14
|
+
}
|
|
15
|
+
function readBaseline(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeBaseline(path, value) {
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
24
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
25
|
+
`, "utf8");
|
|
26
|
+
}
|
|
27
|
+
var PATH_SEGMENT = /([^.[\]]+)|\[(\d+)\]/g;
|
|
28
|
+
function subjectFor(document, path) {
|
|
29
|
+
let node = document;
|
|
30
|
+
let subject;
|
|
31
|
+
for (const match of path.matchAll(PATH_SEGMENT)) {
|
|
32
|
+
if (typeof node !== "object" || node === null) return subject;
|
|
33
|
+
const record = node;
|
|
34
|
+
const candidate = record["capabilityId"] ?? record["procedureId"];
|
|
35
|
+
if (typeof candidate === "string") subject = candidate;
|
|
36
|
+
const key = match[1] ?? match[2];
|
|
37
|
+
if (key === void 0) return subject;
|
|
38
|
+
node = record[key];
|
|
39
|
+
}
|
|
40
|
+
if (typeof node === "object" && node !== null) {
|
|
41
|
+
const record = node;
|
|
42
|
+
const candidate = record["capabilityId"] ?? record["procedureId"];
|
|
43
|
+
if (typeof candidate === "string") subject = candidate;
|
|
44
|
+
}
|
|
45
|
+
return subject;
|
|
46
|
+
}
|
|
47
|
+
function annotate(entries, after, before) {
|
|
48
|
+
return entries.map((entry) => {
|
|
49
|
+
const subject = subjectFor(after, entry.path) ?? subjectFor(before, entry.path);
|
|
50
|
+
return subject ? { ...entry, subject } : entry;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function diff(before, after, path = "") {
|
|
54
|
+
if (Object.is(before, after)) return [];
|
|
55
|
+
const bothArrays = Array.isArray(before) && Array.isArray(after);
|
|
56
|
+
const bothObjects = !bothArrays && typeof before === "object" && typeof after === "object" && before !== null && after !== null;
|
|
57
|
+
if (bothArrays) {
|
|
58
|
+
const entries = [];
|
|
59
|
+
const max = Math.max(before.length, after.length);
|
|
60
|
+
for (let i = 0; i < max; i++) {
|
|
61
|
+
const at = `${path}[${i}]`;
|
|
62
|
+
if (i >= before.length) entries.push({ path: at, kind: "added", after: after[i] });
|
|
63
|
+
else if (i >= after.length) entries.push({ path: at, kind: "removed", before: before[i] });
|
|
64
|
+
else entries.push(...diff(before[i], after[i], at));
|
|
65
|
+
}
|
|
66
|
+
return entries;
|
|
67
|
+
}
|
|
68
|
+
if (bothObjects) {
|
|
69
|
+
const entries = [];
|
|
70
|
+
const beforeRecord = before;
|
|
71
|
+
const afterRecord = after;
|
|
72
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]);
|
|
73
|
+
for (const key of [...keys].sort()) {
|
|
74
|
+
const at = path ? `${path}.${key}` : key;
|
|
75
|
+
if (!(key in beforeRecord)) {
|
|
76
|
+
entries.push({ path: at, kind: "added", after: afterRecord[key] });
|
|
77
|
+
} else if (!(key in afterRecord)) {
|
|
78
|
+
entries.push({ path: at, kind: "removed", before: beforeRecord[key] });
|
|
79
|
+
} else {
|
|
80
|
+
entries.push(...diff(beforeRecord[key], afterRecord[key], at));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
if (JSON.stringify(before) === JSON.stringify(after)) return [];
|
|
86
|
+
return [{ path: path || "<root>", kind: "changed", before, after }];
|
|
87
|
+
}
|
|
88
|
+
function formatValue(value) {
|
|
89
|
+
if (value === void 0) return "\u2014";
|
|
90
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
91
|
+
return text.length > 120 ? `${text.slice(0, 117)}\u2026` : text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
baselineDirFor,
|
|
96
|
+
baselinePath,
|
|
97
|
+
normalize,
|
|
98
|
+
readBaseline,
|
|
99
|
+
writeBaseline,
|
|
100
|
+
annotate,
|
|
101
|
+
diff,
|
|
102
|
+
formatValue
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=chunk-ODUIFFPM.js.map
|