@agent-surface/cli 0.15.0 → 0.16.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 CHANGED
@@ -1,187 +1,11 @@
1
- # @agent-surface/cli
1
+ # `@agent-surface/cli`
2
2
 
3
- The agent surface your app exposes — from a terminal, and in CI. It answers three questions: *what can an agent do here right now*, *did that change without anyone noticing*, and *did we author something no scenario ever reaches*. Part of [agent-surface](https://github.com/Wiseair-srl/agent-surface).
4
-
5
- It is a developer tool: nothing it prints ever reaches a model.
6
-
7
- Docs: https://agent-surface-docs.vercel.app/20-cli
8
-
9
- ## Install
3
+ Compiles, snapshots and diffs the production capability contract.
10
4
 
11
5
  ```bash
12
- pnpm add -D @agent-surface/cli
13
- ```
14
-
15
- ## Configure
16
-
17
- 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.
18
-
19
- ```tsx
20
- // agent-surface.config.tsx
21
- import { defineSurface } from "@agent-surface/cli";
22
- import { agentManifest, createApp } from "./src/agent/setup.js"; // already exists
23
- import { App } from "./src/app/App.js"; // already exists
24
-
25
- export default defineSurface({
26
- manifest: agentManifest,
27
- mount: ({ user }) => {
28
- const app = createApp({ environment: "test", user });
29
- return { registry: app.registry, ui: <App app={app} />, app };
30
- },
31
- scenarios: {
32
- admin: { user: { id: "u_admin", permissions: ["devices:write"] } },
33
- anonymous: { user: null },
34
- },
35
- });
36
- ```
37
-
38
- Loading goes through vite-node on your own `vite.config.*`, so your aliases, plugins and TSX resolve exactly as they do in dev.
39
-
40
- ## Use
41
-
42
- ```bash
43
- agent-surface init # read the codebase, then scaffold a config
44
- agent-surface inspect [scenario] # what an agent can reach, and what it cannot
45
- agent-surface snapshot [scenario] # write/refresh the committed baseline
46
- agent-surface check [scenario] # fail drift, gaps, rejections, stale scenarios
47
- ```
48
-
49
- Every command covers all scenarios in the config unless you name one. `inspect` prints each in turn:
50
-
51
- ```text
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
73
-
74
- scenario admin route /devices
75
- 9 callable, 2 visible-disabled, 0 hidden · 1 destructive, 1 confirmation-gated
76
-
77
- CAPABILITY KIND EFFECT STATE FLAGS
78
- app.navigation.goTo action navigation callable reversible
79
- devices.drawer.open action local-state callable reversible
80
- devices.drawer.close action local-state disabled reversible
81
- ⤷ The drawer is not open
82
- devices.table.sort action local-state callable idempotent · reversible
83
- devices.disable procedure destructive disabled confirmation:required · deviceIds bound+locked
84
- ⤷ Select at least one device first
6
+ agent-surface inspect
7
+ agent-surface snapshot
8
+ agent-surface check --base origin/main --format github
85
9
  ```
86
10
 
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".
88
-
89
- `--detail` shows full capability, origin, and diagnostic detail; `--explain` and `--schemas` imply it.
90
-
91
- ### Depth
92
-
93
- A presentation surface has two sources of truth, and `--depth` says how much of each to compute:
94
-
95
- | | reads the TypeScript program | mounts the scenarios | answers |
96
- |---|---|---|---|
97
- | `--depth static` | ✔ | — | what this codebase authors |
98
- | `--depth runtime` | — | ✔ | what a mounted scenario surfaces |
99
- | `--depth full` *(default)* | ✔ | ✔ | both, **and the difference between them** |
100
-
101
- `full` is the default because a tool that has to be asked for the complete answer mostly gives the incomplete one. `static` needs no scenarios and survives an app that will not mount; `runtime` skips the program boot on a repository wide enough to feel it.
102
-
103
- ### Why is my capability missing?
104
-
105
- `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:
106
-
107
- ```bash
108
- agent-surface inspect anonymous --explain
109
- ```
110
-
111
- ```text
112
- scenario anonymous route /devices
113
- 0 callable, 0 visible-disabled, 11 hidden
114
-
115
- hidden by policy (absent from the snapshot) (11)
116
- - set [devices.filters@default]
117
- Update one or both filters; omitted fields are unchanged.
118
- policy authenticated (registry, discovery/authorize): hide
119
- ```
120
-
121
- The hidden capabilities themselves print without the flag — signed out, that scenario shows eleven rows marked `hidden` rather than a bare `0 callable`. `--explain` adds the attribution: 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.
122
-
123
- ### What did we author that no scenario reaches?
124
-
125
- A route nobody visits never registers, so it is in no snapshot and drifts against no baseline — invisible to a mount by construction. The static half reads the TypeScript program instead, and the difference is a first-class finding in **every** command:
126
-
127
- ```text
128
- UNREACHED — authored, and no scenario mounts it (1)
129
- CAPABILITY ORIGIN
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
- ```
134
-
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
153
- ```
154
-
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.
156
-
157
- If any scenario fails to mount there is **no verdict at all**: that scenario reached nothing, so everything it would have surfaced would read as unreached. The failed scenarios are named instead, and the static half still prints.
158
-
159
- ### Exit codes
160
-
161
- **0** clean · **1** a finding · **2** the command could not run.
162
-
163
- `inspect` never exits `1` — it reports, and `check` gates. CI has to be able to tell "the surface changed" from "the tool never loaded the app", because the second one passing silently is how a gate rots.
164
-
165
- > Upgrading from 0.10? `capabilities` is now `inspect --depth static`, and `coverage` is folded into `inspect` and `check`. Both were removed rather than aliased; naming either still prints where its answer went.
166
-
167
- ### The scenarios are not a fixture
168
-
169
- The same definitions drive your test suite, so "admin on /devices" exists once rather than twice:
170
-
171
- ```ts
172
- import config from "../agent-surface.config.js";
173
- import { mountScenario } from "@agent-surface/cli/vitest";
174
-
175
- const { surface, app } = await mountScenario(config, "admin");
176
- expect(surface).toExpose("view:devices.filters.set");
177
- ```
178
-
179
- ## Notes
180
-
181
- Requires `@testing-library/react` and `react-dom` (peers) — anything that mounts does so in jsdom, so anything needing a real browser is out of reach by construction. `--depth static` and `init` mount nothing at all, and install no DOM.
182
-
183
- Output falls back to plain text when piped, or under `--plain`, `CI` and `NO_COLOR`. stdout carries the command's output and nothing else; diagnostics go to stderr.
184
-
185
- Full specification: [docs/20](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/20-cli.md).
186
-
187
- MIT © Wiseair S.r.l.
11
+ No scenarios, app mounts, static heuristics or allowlists. See [CLI docs](../../docs/20-cli.md).
package/dist/bin.js CHANGED
@@ -1,99 +1,167 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- DEPTHS,
4
- findConfig,
5
- isDepth,
6
- renderReportPlain,
7
- write,
8
- writeError
9
- } from "./chunk-GYYWHZPM.js";
10
- import "./chunk-NFK3XWWH.js";
3
+ DEFAULT_SNAPSHOT,
4
+ readBaseManifest,
5
+ readManifest,
6
+ renderReport,
7
+ writeManifest
8
+ } from "./chunk-X6RGGBQ4.js";
9
+ import {
10
+ diffContracts
11
+ } from "./chunk-IA7FUP4R.js";
11
12
 
12
13
  // src/bin.ts
13
14
  import { realpathSync } from "fs";
14
15
  import { fileURLToPath } from "url";
15
16
  import { parseArgs } from "util";
16
17
 
17
- // src/dom.ts
18
- import { JSDOM } from "jsdom";
19
- var DOM_EVENT_GLOBALS = ["Event", "CustomEvent"];
20
- function alignEventRealm(globals, window) {
21
- for (const key of DOM_EVENT_GLOBALS) {
22
- if (typeof window[key] !== "function") continue;
23
- Object.defineProperty(globals, key, {
24
- value: window[key],
25
- writable: true,
26
- configurable: true
27
- });
18
+ // src/commands.ts
19
+ import { resolve } from "path";
20
+ import {
21
+ compileCapabilityContract,
22
+ computeManifestHash
23
+ } from "@agent-surface/compiler";
24
+ function mergeManifests(manifests) {
25
+ const entries = /* @__PURE__ */ new Map();
26
+ for (const manifest of manifests) {
27
+ for (const entry of manifest.capabilities) {
28
+ const key = `${entry.declarationId}\0${entry.capabilityId}`;
29
+ const existing = entries.get(key);
30
+ if (!existing) entries.set(key, { ...entry, targets: [...entry.targets] });
31
+ else {
32
+ if (existing.contractHash !== entry.contractHash) {
33
+ throw new Error(`contract ${entry.declarationId} / ${entry.capabilityId} differs across targets`);
34
+ }
35
+ existing.targets = [.../* @__PURE__ */ new Set([...existing.targets, ...entry.targets])].sort();
36
+ }
37
+ }
28
38
  }
39
+ const payload = {
40
+ formatVersion: 3,
41
+ compilerVersion: manifests[0]?.compilerVersion ?? "unknown",
42
+ targets: [...new Set(manifests.flatMap((manifest) => manifest.targets))].sort(),
43
+ capabilities: [...entries.values()].sort(
44
+ (a, b) => `${a.declarationId}\0${a.capabilityId}`.localeCompare(`${b.declarationId}\0${b.capabilityId}`)
45
+ ),
46
+ externalContracts: manifests.flatMap((manifest) => manifest.externalContracts).filter((entry, index, all) => all.findIndex((candidate) => candidate.digest === entry.digest) === index).sort((a, b) => a.source.localeCompare(b.source)),
47
+ completeness: { status: "proven" }
48
+ };
49
+ return { ...payload, hash: computeManifestHash(payload) };
29
50
  }
30
- function installDom(url = "http://localhost/") {
31
- const globals = globalThis;
32
- if (typeof globals["document"] !== "undefined") {
33
- const installedWindow = globals["window"];
34
- if (installedWindow && typeof installedWindow === "object") {
35
- alignEventRealm(globals, installedWindow);
36
- }
51
+ async function compile(options) {
52
+ const targets = options.targets.length > 0 ? options.targets : ["web-production"];
53
+ const manifests = await Promise.all(
54
+ targets.map(
55
+ (target) => compileCapabilityContract({
56
+ root: options.root,
57
+ ...options.configFile ? { configFile: options.configFile } : {},
58
+ target
59
+ })
60
+ )
61
+ );
62
+ return mergeManifests(manifests);
63
+ }
64
+ function shouldFail(policy, changes) {
65
+ if (policy === "none") return false;
66
+ if (policy === "all") return changes.length > 0;
67
+ return changes.some((change) => change.classification === policy);
68
+ }
69
+ async function present(report, options) {
70
+ const interactive = options.format === "human" && !options.plain && process.stdout.isTTY === true && !process.env["CI"] && !process.env["NO_COLOR"];
71
+ if (interactive) {
72
+ const { renderInk } = await import("./ink-E6DATMUQ.js");
73
+ await renderInk(report);
37
74
  return;
38
75
  }
39
- const dom = new JSDOM("<!doctype html><html><body></body></html>", {
40
- url,
41
- pretendToBeVisual: true
42
- });
43
- const { window } = dom;
44
- for (const key of Object.getOwnPropertyNames(window)) {
45
- if (key.startsWith("_")) continue;
46
- if (key in globals) continue;
47
- const descriptor = Object.getOwnPropertyDescriptor(window, key);
48
- if (!descriptor) continue;
49
- Object.defineProperty(globals, key, descriptor);
50
- }
51
- for (const key of ["window", "document", "navigator"]) {
52
- if (!(key in globals)) {
53
- Object.defineProperty(globals, key, { value: window[key], configurable: true });
54
- }
55
- }
56
- alignEventRealm(globals, window);
76
+ process.stdout.write(renderReport(report, options.format));
77
+ }
78
+ async function runInspect(options) {
79
+ const manifest = await compile(options);
80
+ const snapshotPath = resolve(options.root, options.snapshot ?? DEFAULT_SNAPSHOT);
81
+ const snapshot = readManifest(snapshotPath);
82
+ const integrityChanges = diffContracts(snapshot, manifest);
83
+ const report = {
84
+ command: "inspect",
85
+ status: "view",
86
+ manifest,
87
+ snapshotPath,
88
+ integrity: {
89
+ status: !snapshot ? "missing" : snapshot.hash === manifest.hash ? "current" : "stale",
90
+ changes: integrityChanges
91
+ },
92
+ ...options.base ? {
93
+ pullRequest: {
94
+ base: options.base,
95
+ changes: diffContracts(readBaseManifest(options.base, snapshotPath, options.root), manifest)
96
+ }
97
+ } : {}
98
+ };
99
+ await present(report, options);
100
+ return 0;
101
+ }
102
+ async function runSnapshot(options) {
103
+ const manifest = await compile(options);
104
+ const snapshotPath = resolve(options.root, options.snapshot ?? DEFAULT_SNAPSHOT);
105
+ writeManifest(snapshotPath, manifest);
106
+ await present({ command: "snapshot", status: "written", manifest, snapshotPath }, options);
107
+ return 0;
108
+ }
109
+ async function runCheck(options) {
110
+ const manifest = await compile(options);
111
+ const snapshotPath = resolve(options.root, options.snapshot ?? DEFAULT_SNAPSHOT);
112
+ const snapshot = readManifest(snapshotPath);
113
+ const integrityChanges = diffContracts(snapshot, manifest);
114
+ const integrityCurrent = snapshot?.hash === manifest.hash;
115
+ const pr = options.base ? {
116
+ base: options.base,
117
+ changes: diffContracts(readBaseManifest(options.base, snapshotPath, options.root), snapshot ?? manifest)
118
+ } : void 0;
119
+ const failed = !integrityCurrent || (pr ? shouldFail(options.policy, pr.changes) : false);
120
+ const report = {
121
+ command: "check",
122
+ status: failed ? "fail" : "pass",
123
+ manifest,
124
+ snapshotPath,
125
+ integrity: {
126
+ status: !snapshot ? "missing" : integrityCurrent ? "current" : "stale",
127
+ changes: integrityChanges
128
+ },
129
+ ...pr ? { pullRequest: pr } : {}
130
+ };
131
+ await present(report, options);
132
+ return failed ? 1 : 0;
57
133
  }
58
134
 
59
135
  // src/bin.ts
60
- var USAGE = `agent-surface \u2014 the agent surface your app exposes
136
+ var USAGE = `agent-surface \u2014 compiled production capability contract
61
137
 
62
138
  Usage
63
- agent-surface init read the codebase, then scaffold a config
64
- agent-surface inspect [scenario] what an agent can reach, and what it cannot
65
- agent-surface snapshot [scenario] write/refresh the committed baseline
66
- agent-surface check [scenario] fail on drift, or on a capability no scenario reaches
67
-
68
- Every command covers all scenarios in the config unless you name one.
69
-
70
- Depth
71
- --depth full read the source AND mount the scenarios, and report the gap (default)
72
- --depth static read the source only \u2014 no Vite, no jsdom, no mount, no scenarios needed
73
- --depth runtime mount only \u2014 skip the TypeScript program on a repo wide enough to feel it
139
+ agent-surface inspect
140
+ agent-surface snapshot
141
+ agent-surface check --base origin/main --format github
74
142
 
75
143
  Options
76
- --config <path> path to agent-surface.config.* (default: nearest, searching upward)
77
- --baseline-dir where baselines live (default: .agent-surface next to the config)
78
- --scope <prefix> restrict to a component-type prefix (repeatable)
79
- --detail full capability, origin, and diagnostic detail
80
- --explain name the policies behind every decision (implies --detail)
81
- --schemas include input/output JSON Schemas (implies --detail)
82
- --tsconfig <path> tsconfig the source read uses (default: nearest to the config)
83
- --allow-unresolved check: do not fail on a call site that could not be read
84
- --yes init: write without asking
85
- --json emit data instead of a rendered view
86
- --plain force plain text (implied when piped, or under CI / NO_COLOR)
87
- -h, --help show this
88
- -v, --version print the version
144
+ --root <path> application root (default: cwd)
145
+ --config <path> Vite config, relative to root
146
+ --snapshot <path> committed contract (default: .agent-surface/contract.json)
147
+ --target <name> production build target; repeatable
148
+ --base <git-ref> render committed PR drift against Git base
149
+ --policy <mode> all | widening | narrowing | neutral | none (default: all)
150
+ --format <format> human | json | github | markdown (default: human)
151
+ --json alias for --format json
152
+ --plain disable Ink terminal rendering
153
+ -h, --help
154
+ -v, --version
89
155
 
90
- Exit codes are the contract: 0 clean, 1 a finding, 2 the command could not run.
156
+ Exit: 0 clean/viewed, 1 deterministic drift/policy finding, 2 completeness failure.
91
157
  `;
92
- var COMMANDS = ["init", "inspect", "snapshot", "check"];
93
- var RETIRED = {
94
- capabilities: "agent-surface inspect --depth static",
95
- coverage: "agent-surface inspect (or `check`, which now fails on the gap)"
96
- };
158
+ var COMMANDS = /* @__PURE__ */ new Set(["inspect", "snapshot", "check"]);
159
+ var FORMATS = /* @__PURE__ */ new Set(["human", "json", "github", "markdown"]);
160
+ var POLICIES = /* @__PURE__ */ new Set(["all", "widening", "narrowing", "neutral", "none"]);
161
+ function write(text, error = false) {
162
+ (error ? process.stderr : process.stdout).write(text.endsWith("\n") ? text : `${text}
163
+ `);
164
+ }
97
165
  async function main(argv = process.argv.slice(2)) {
98
166
  let parsed;
99
167
  try {
@@ -101,16 +169,13 @@ async function main(argv = process.argv.slice(2)) {
101
169
  args: argv,
102
170
  allowPositionals: true,
103
171
  options: {
172
+ root: { type: "string", default: process.cwd() },
104
173
  config: { type: "string" },
105
- "baseline-dir": { type: "string" },
106
- scope: { type: "string", multiple: true },
107
- depth: { type: "string", default: "full" },
108
- detail: { type: "boolean", default: false },
109
- explain: { type: "boolean", default: false },
110
- schemas: { type: "boolean", default: false },
111
- tsconfig: { type: "string" },
112
- "allow-unresolved": { type: "boolean", default: false },
113
- yes: { type: "boolean", default: false },
174
+ snapshot: { type: "string" },
175
+ target: { type: "string", multiple: true },
176
+ base: { type: "string" },
177
+ policy: { type: "string", default: "all" },
178
+ format: { type: "string", default: "human" },
114
179
  json: { type: "boolean", default: false },
115
180
  plain: { type: "boolean", default: false },
116
181
  help: { type: "boolean", short: "h", default: false },
@@ -118,8 +183,8 @@ async function main(argv = process.argv.slice(2)) {
118
183
  }
119
184
  });
120
185
  } catch (error) {
121
- writeError(error instanceof Error ? error.message : String(error));
122
- writeError(USAGE);
186
+ write(error instanceof Error ? error.message : String(error), true);
187
+ write(USAGE, true);
123
188
  return 2;
124
189
  }
125
190
  const { values, positionals } = parsed;
@@ -131,87 +196,47 @@ async function main(argv = process.argv.slice(2)) {
131
196
  write(await readVersion());
132
197
  return 0;
133
198
  }
134
- const [command, scenario] = positionals;
135
- if (!command) {
136
- write(USAGE);
199
+ const [command, ...extra] = positionals;
200
+ if (!command || !COMMANDS.has(command) || extra.length > 0) {
201
+ write(command === "init" ? "init removed: configure the Vite compiler plugin" : `invalid command ${command ?? ""}`, true);
202
+ write(USAGE, true);
137
203
  return 2;
138
204
  }
139
- if (!COMMANDS.includes(command)) {
140
- const moved = RETIRED[command];
141
- writeError(
142
- moved ? `"${command}" was removed in 0.11 \u2014 its answer is now \`${moved}\`. The static catalog and the live projection are one command, so the gap between them is reported rather than left for whoever remembers to look.` : `unknown command "${command}"`
143
- );
144
- if (!moved) writeError(USAGE);
205
+ const format = values.json ? "json" : values.format;
206
+ if (!FORMATS.has(format)) {
207
+ write(`--format must be human, json, github, or markdown`, true);
145
208
  return 2;
146
209
  }
147
- if (!isDepth(values.depth)) {
148
- writeError(`--depth must be one of ${DEPTHS.join(", ")} \u2014 got "${values.depth}"`);
210
+ if (!POLICIES.has(values.policy)) {
211
+ write(`--policy must be all, widening, narrowing, neutral, or none`, true);
149
212
  return 2;
150
213
  }
151
- const depth = values.depth;
214
+ const options = {
215
+ root: values.root,
216
+ ...values.config ? { configFile: values.config } : {},
217
+ ...values.snapshot ? { snapshot: values.snapshot } : {},
218
+ targets: values.target ?? [],
219
+ ...values.base ? { base: values.base } : {},
220
+ format,
221
+ policy: values.policy,
222
+ ...values.plain ? { plain: true } : {}
223
+ };
152
224
  try {
153
- if (command === "init") {
154
- const { runInit } = await import("./init-BVZR6CRS.js");
155
- return await runInit({
156
- cwd: process.cwd(),
157
- ...values.tsconfig ? { tsconfig: values.tsconfig } : {},
158
- ...values.yes ? { yes: true } : {},
159
- ...values.plain ? { plain: true } : {}
160
- });
161
- }
162
- const configPath = values.config ?? findConfig();
163
- if (!configPath) {
164
- writeError(
165
- "no agent-surface.config.* found (searched upward from the working directory).\nRun `agent-surface init`, or see https://agent-surface-docs.vercel.app/20-cli"
166
- );
167
- return 2;
168
- }
169
- if (depth !== "static") installDom();
170
- const shared = {
171
- configPath,
172
- depth,
173
- ...scenario ? { scenario } : {},
174
- ...values.scope ? { scope: values.scope } : {},
175
- ...values.tsconfig ? { tsconfig: values.tsconfig } : {},
176
- ...values.json ? { json: true } : {},
177
- ...values.plain ? { plain: true } : {},
178
- ...values["baseline-dir"] ? { baselineDir: values["baseline-dir"] } : {}
179
- };
180
- if (command === "inspect") {
181
- const { runInspect } = await import("./inspect-3YFPCYQJ.js");
182
- return await runInspect({
183
- ...shared,
184
- ...values.detail ? { detail: true } : {},
185
- ...values.explain ? { explain: true } : {},
186
- ...values.schemas ? { schemas: true } : {}
187
- });
188
- }
189
- if (command === "snapshot") {
190
- const { runSnapshot } = await import("./snapshot-5QKLZKZI.js");
191
- return await runSnapshot(shared);
192
- }
193
- const { runCheck } = await import("./check-VWRYNYLN.js");
194
- return await runCheck({
195
- ...shared,
196
- ...values.detail ? { detail: true } : {},
197
- ...values["allow-unresolved"] ? { allowUnresolved: true } : {}
198
- });
225
+ if (command === "inspect") return await runInspect(options);
226
+ if (command === "snapshot") return await runSnapshot(options);
227
+ return await runCheck(options);
199
228
  } catch (error) {
200
- writeError(error instanceof Error ? error.message : String(error));
229
+ write(error instanceof Error ? error.message : String(error), true);
201
230
  if (error instanceof Error && error.stack && process.env["AGENT_SURFACE_DEBUG"]) {
202
- writeError(error.stack);
231
+ write(error.stack, true);
203
232
  }
204
233
  return 2;
205
234
  }
206
235
  }
207
236
  async function readVersion() {
208
- try {
209
- const { readFileSync } = await import("fs");
210
- const path = fileURLToPath(new URL("../package.json", import.meta.url));
211
- return JSON.parse(readFileSync(path, "utf8")).version;
212
- } catch {
213
- return "unknown";
214
- }
237
+ const { readFileSync } = await import("fs");
238
+ const path = fileURLToPath(new URL("../package.json", import.meta.url));
239
+ return JSON.parse(readFileSync(path, "utf8")).version;
215
240
  }
216
241
  function invokedAsBinary() {
217
242
  const entry = process.argv[1];
@@ -222,61 +247,10 @@ function invokedAsBinary() {
222
247
  return false;
223
248
  }
224
249
  }
225
- var GRACE_MS = 1e3;
226
- var OWN_STDIO = /* @__PURE__ */ new Set(["TTYWrap", "PipeWrap", "FileWrap"]);
227
- function heldHandles() {
228
- const active = process.getActiveResourcesInfo?.() ?? [];
229
- return active.filter((resource) => !OWN_STDIO.has(resource));
230
- }
231
- async function flushOutput() {
232
- const drained = Promise.all(
233
- [process.stdout, process.stderr].map(
234
- (stream) => new Promise((resolve) => {
235
- if (stream.writableLength === 0) resolve();
236
- else stream.write("", () => resolve());
237
- })
238
- )
239
- );
240
- const deadline = new Promise((resolve) => {
241
- setTimeout(resolve, 2e3).unref();
242
- });
243
- await Promise.race([drained, deadline]);
244
- }
245
- function exitWhenWedged(code) {
246
- process.exitCode = code;
247
- setTimeout(() => {
248
- const held = heldHandles();
249
- if (held.length > 0) {
250
- const kinds = [...new Set(held)].sort().join(", ");
251
- writeError(
252
- `
253
- ${renderReportPlain([
254
- {
255
- title: "PROCESS CLEANUP WARN",
256
- rows: [
257
- { label: "Open handles", text: `${held.length} (${kinds})` },
258
- { label: "Impact", text: "report complete; exit code unchanged; process exit forced" },
259
- {
260
- label: "Likely cause",
261
- text: "polling, websocket, or a cache timer created during mount"
262
- },
263
- { label: "Exit", text: String(code) }
264
- ]
265
- }
266
- ])}`
267
- );
268
- }
269
- void flushOutput().then(() => process.exit(code));
270
- }, GRACE_MS).unref();
271
- }
272
250
  if (invokedAsBinary()) {
273
- main().then(
274
- (code) => exitWhenWedged(code),
275
- (error) => {
276
- writeError(error instanceof Error ? error.message : String(error));
277
- exitWhenWedged(1);
278
- }
279
- );
251
+ void main().then((code) => {
252
+ process.exitCode = code;
253
+ });
280
254
  }
281
255
  export {
282
256
  main