@agent-surface/cli 0.10.0 → 0.11.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/README.md +64 -23
- package/dist/bin.js +63 -41
- package/dist/bin.js.map +1 -1
- package/dist/check-XU3FYNGJ.js +122 -0
- package/dist/check-XU3FYNGJ.js.map +1 -0
- package/dist/chunk-2FG527AM.js +740 -0
- package/dist/chunk-2FG527AM.js.map +1 -0
- package/dist/chunk-AFLVTBI6.js +316 -0
- package/dist/chunk-AFLVTBI6.js.map +1 -0
- package/dist/chunk-DYDSJM7R.js +170 -0
- package/dist/chunk-DYDSJM7R.js.map +1 -0
- package/dist/{chunk-FYEXHWGG.js → chunk-QIVOZAWX.js} +52 -2
- package/dist/chunk-QIVOZAWX.js.map +1 -0
- package/dist/index.d.ts +36 -2
- package/dist/init-ODFEGU3P.js +141 -0
- package/dist/init-ODFEGU3P.js.map +1 -0
- package/dist/{ink-HBPOQTRS.js → ink-P23VKP4H.js} +102 -33
- package/dist/ink-P23VKP4H.js.map +1 -0
- package/dist/inspect-6XKULUH2.js +108 -0
- package/dist/inspect-6XKULUH2.js.map +1 -0
- package/dist/snapshot-YGEDJVTG.js +59 -0
- package/dist/snapshot-YGEDJVTG.js.map +1 -0
- package/package.json +4 -4
- package/dist/capabilities-OLFYMHCL.js +0 -37
- package/dist/capabilities-OLFYMHCL.js.map +0 -1
- package/dist/check-IN4XKAND.js +0 -84
- package/dist/check-IN4XKAND.js.map +0 -1
- package/dist/chunk-4AEQKM2X.js +0 -498
- package/dist/chunk-4AEQKM2X.js.map +0 -1
- package/dist/chunk-A27Y7ALQ.js +0 -51
- package/dist/chunk-A27Y7ALQ.js.map +0 -1
- package/dist/chunk-FYEXHWGG.js.map +0 -1
- package/dist/chunk-ODUIFFPM.js +0 -104
- package/dist/chunk-ODUIFFPM.js.map +0 -1
- package/dist/coverage-HCHLJTDD.js +0 -133
- package/dist/coverage-HCHLJTDD.js.map +0 -1
- package/dist/ink-HBPOQTRS.js.map +0 -1
- package/dist/inspect-NJNB6CAS.js +0 -213
- package/dist/inspect-NJNB6CAS.js.map +0 -1
- package/dist/snapshot-JQAB73OV.js +0 -38
- package/dist/snapshot-JQAB73OV.js.map +0 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/render/model.ts
|
|
2
|
+
function flatRows(view) {
|
|
3
|
+
return view.groups.flatMap((group) => group.rows);
|
|
4
|
+
}
|
|
5
|
+
function pathOf(capabilityId) {
|
|
6
|
+
return capabilityId.replace(/^(view|domain):/, "");
|
|
7
|
+
}
|
|
8
|
+
function leafOf(capabilityId) {
|
|
9
|
+
const withoutPlane = pathOf(capabilityId);
|
|
10
|
+
const dot = withoutPlane.lastIndexOf(".");
|
|
11
|
+
return dot === -1 ? withoutPlane : withoutPlane.slice(dot + 1);
|
|
12
|
+
}
|
|
13
|
+
function actionFlags(action) {
|
|
14
|
+
const flags = [];
|
|
15
|
+
if (action.idempotent) flags.push("idempotent");
|
|
16
|
+
if (action.reversible) flags.push("reversible");
|
|
17
|
+
if (action.confirmation !== "never") flags.push(`confirmation:${action.confirmation}`);
|
|
18
|
+
return flags;
|
|
19
|
+
}
|
|
20
|
+
function procedureFlags(procedure) {
|
|
21
|
+
const flags = [];
|
|
22
|
+
if (procedure.confirmation !== "never") flags.push(`confirmation:${procedure.confirmation}`);
|
|
23
|
+
for (const field of procedure.boundFields) {
|
|
24
|
+
flags.push(`${field.path} bound${field.locked ? "+locked" : ""}`);
|
|
25
|
+
}
|
|
26
|
+
return flags;
|
|
27
|
+
}
|
|
28
|
+
function explanationIndex(explanation) {
|
|
29
|
+
const index = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const capability of explanation.capabilities) {
|
|
31
|
+
index.set(`${capability.capabilityId}\0${capability.registrationId}`, capability);
|
|
32
|
+
}
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
35
|
+
function buildView(result, options = {}) {
|
|
36
|
+
const { snapshot, explanation } = result;
|
|
37
|
+
const index = explanationIndex(explanation);
|
|
38
|
+
const groups = [];
|
|
39
|
+
const counts = { callable: 0, disabled: 0, hidden: 0 };
|
|
40
|
+
const enrich = (row, capabilityId, registrationId) => {
|
|
41
|
+
const explained = index.get(`${capabilityId}\0${registrationId}`);
|
|
42
|
+
if (options.explain && explained) {
|
|
43
|
+
row.policies = explained.policies;
|
|
44
|
+
row.availability = explained.availability;
|
|
45
|
+
}
|
|
46
|
+
return row;
|
|
47
|
+
};
|
|
48
|
+
for (const component of snapshot.components) {
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const observation of component.observations) {
|
|
51
|
+
rows.push(
|
|
52
|
+
enrich(
|
|
53
|
+
rowFor(observation, "observation", void 0, [], options, {
|
|
54
|
+
input: void 0,
|
|
55
|
+
output: observation.outputSchema
|
|
56
|
+
}),
|
|
57
|
+
observation.capabilityId,
|
|
58
|
+
component.registrationId
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
for (const action of component.actions) {
|
|
63
|
+
rows.push(
|
|
64
|
+
enrich(
|
|
65
|
+
rowFor(action, "action", action.effect, actionFlags(action), options, {
|
|
66
|
+
input: action.inputSchema,
|
|
67
|
+
output: action.outputSchema
|
|
68
|
+
}),
|
|
69
|
+
action.capabilityId,
|
|
70
|
+
component.registrationId
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
groups.push({
|
|
75
|
+
heading: component.instanceId === "default" ? component.type : `${component.type}@${component.instanceId}`,
|
|
76
|
+
rows
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (snapshot.procedures.length > 0) {
|
|
80
|
+
groups.push({
|
|
81
|
+
heading: "authoritative (domain)",
|
|
82
|
+
rows: snapshot.procedures.map(
|
|
83
|
+
(procedure) => enrich(
|
|
84
|
+
{
|
|
85
|
+
capabilityId: procedure.procedureId,
|
|
86
|
+
name: procedure.procedureId.replace(/^domain:/, ""),
|
|
87
|
+
path: pathOf(procedure.procedureId),
|
|
88
|
+
kind: "procedure",
|
|
89
|
+
plane: "domain",
|
|
90
|
+
outcome: procedure.available ? "expose" : "disable",
|
|
91
|
+
description: procedure.description,
|
|
92
|
+
...procedure.unavailableReason ? { reason: procedure.unavailableReason } : {},
|
|
93
|
+
effect: procedure.effect,
|
|
94
|
+
flags: procedureFlags(procedure),
|
|
95
|
+
tags: [procedure.effect, ...procedureFlags(procedure)],
|
|
96
|
+
...options.schemas ? { schemas: { input: procedure.inputSchema, output: procedure.outputSchema } } : {}
|
|
97
|
+
},
|
|
98
|
+
procedure.procedureId,
|
|
99
|
+
procedure.registrationId
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const hidden = explanation.capabilities.filter((c) => c.outcome === "hide");
|
|
105
|
+
if (hidden.length > 0) {
|
|
106
|
+
groups.push({
|
|
107
|
+
heading: "hidden by policy (absent from the snapshot)",
|
|
108
|
+
rows: hidden.map((capability) => ({
|
|
109
|
+
capabilityId: capability.capabilityId,
|
|
110
|
+
name: leafOf(capability.capabilityId),
|
|
111
|
+
path: pathOf(capability.capabilityId),
|
|
112
|
+
kind: capability.kind,
|
|
113
|
+
plane: capability.plane,
|
|
114
|
+
outcome: "hide",
|
|
115
|
+
description: capability.description,
|
|
116
|
+
// No reason line, deliberately. The reason a hidden capability carries
|
|
117
|
+
// is its *availability* reason — "The drawer is not open" — and printing
|
|
118
|
+
// that under a row marked `hidden` says the UI declined when authority
|
|
119
|
+
// did. Authority hides, state discloses (D11/D12), and the two must
|
|
120
|
+
// never look alike. Why it was hidden is a policy question, which is
|
|
121
|
+
// what `--explain` answers.
|
|
122
|
+
//
|
|
123
|
+
// A hidden capability has no snapshot entry, so there is no effect to
|
|
124
|
+
// report — the table prints an em dash rather than inventing one. The
|
|
125
|
+
// capability path already carries the component type; only a non-default
|
|
126
|
+
// instance adds anything.
|
|
127
|
+
flags: capability.component.instanceId === "default" ? [] : [`@${capability.component.instanceId}`],
|
|
128
|
+
tags: [`${capability.component.type}@${capability.component.instanceId}`],
|
|
129
|
+
...options.explain ? { policies: capability.policies, availability: capability.availability } : {}
|
|
130
|
+
}))
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const capability of explanation.capabilities) {
|
|
134
|
+
if (capability.outcome === "expose") counts.callable += 1;
|
|
135
|
+
else if (capability.outcome === "disable") counts.disabled += 1;
|
|
136
|
+
else counts.hidden += 1;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
scenario: result.scenario,
|
|
140
|
+
...snapshot.route?.path ? { route: snapshot.route.path } : {},
|
|
141
|
+
...result.scope ? { scope: result.scope } : {},
|
|
142
|
+
groups,
|
|
143
|
+
counts,
|
|
144
|
+
rejections: result.rejections ?? [],
|
|
145
|
+
explained: options.explain === true
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function rowFor(descriptor, kind, effect, flags, options, schemas) {
|
|
149
|
+
return {
|
|
150
|
+
capabilityId: descriptor.capabilityId,
|
|
151
|
+
name: descriptor.name,
|
|
152
|
+
path: pathOf(descriptor.capabilityId),
|
|
153
|
+
kind,
|
|
154
|
+
plane: "view",
|
|
155
|
+
outcome: descriptor.available ? "expose" : "disable",
|
|
156
|
+
description: descriptor.description,
|
|
157
|
+
...descriptor.unavailableReason ? { reason: descriptor.unavailableReason } : {},
|
|
158
|
+
...effect ? { effect } : {},
|
|
159
|
+
flags,
|
|
160
|
+
// The grouped detail view prints one combined list, the way it always has.
|
|
161
|
+
tags: effect ? [effect, ...flags] : [kind, ...flags],
|
|
162
|
+
...options.schemas ? { schemas } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
flatRows,
|
|
168
|
+
buildView
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=chunk-DYDSJM7R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/render/model.ts"],"sourcesContent":["import type {\n AgentActionDescriptor,\n AgentObservationDescriptor,\n AgentProcedureDescriptor,\n AgentSurfaceSnapshot,\n} from \"@agent-surface/core\";\nimport type { CapabilityExplanation, SurfaceExplanation } from \"@agent-surface/core/explain\";\nimport type { CollectResult, RegistrationRejection } from \"../collect.js\";\n\n/**\n * One view model, two renderers. The Ink UI and the plain-text fallback both\n * consume this, so `--plain` can never drift into showing something different\n * from what a TTY shows.\n */\nexport interface CapabilityRow {\n capabilityId: string;\n /** Leaf name — the group heading already carries the rest of the id. */\n name: string;\n /**\n * The id minus its plane prefix. The table is flat, so its first column has\n * to carry the whole path; the grouped detail view uses `name`.\n */\n path: string;\n kind: \"observation\" | \"action\" | \"procedure\";\n plane: \"view\" | \"domain\";\n outcome: \"expose\" | \"disable\" | \"hide\";\n description: string;\n reason?: string;\n /** The effect, alone, for the table's own column. Observations have none. */\n effect?: string;\n /** What is left of `tags` once the effect has its own column. */\n flags: string[];\n /** Effect and flags together — what the grouped detail view prints. */\n tags: string[];\n policies?: CapabilityExplanation[\"policies\"];\n availability?: CapabilityExplanation[\"availability\"];\n schemas?: { input?: unknown; output?: unknown };\n}\n\nexport interface CapabilityGroup {\n heading: string;\n rows: CapabilityRow[];\n}\n\nexport interface SurfaceView {\n scenario: string;\n route?: string;\n /**\n * The scope the counts below were computed under (`AS-CLI-007`). A scope\n * filters the snapshot *and* the explanation, so without it on screen the\n * header reads as a statement about the whole surface when it is a statement\n * about one prefix of it.\n */\n scope?: string[];\n groups: CapabilityGroup[];\n counts: { callable: number; disabled: number; hidden: number };\n /** Refused during the mount — absent from both projections (`AS-CLI-006`). */\n rejections: RegistrationRejection[];\n explained: boolean;\n}\n\nexport interface ViewOptions {\n explain?: boolean;\n schemas?: boolean;\n}\n\n/**\n * The table is flat — `groups` exist for the grouped detail view, which is what\n * `--detail`, `--explain` and `--schemas` render. Flattening here rather than in\n * each renderer keeps the two views over one order.\n */\nexport function flatRows(view: SurfaceView): CapabilityRow[] {\n return view.groups.flatMap((group) => group.rows);\n}\n\nfunction pathOf(capabilityId: string): string {\n return capabilityId.replace(/^(view|domain):/, \"\");\n}\n\nfunction leafOf(capabilityId: string): string {\n const withoutPlane = pathOf(capabilityId);\n const dot = withoutPlane.lastIndexOf(\".\");\n return dot === -1 ? withoutPlane : withoutPlane.slice(dot + 1);\n}\n\n/**\n * The effect gets its own table column; everything else is a flag. An\n * observation reads state and has no effect at all, which the table shows as\n * an em dash rather than inventing one.\n */\nfunction actionFlags(action: AgentActionDescriptor): string[] {\n const flags: string[] = [];\n if (action.idempotent) flags.push(\"idempotent\");\n if (action.reversible) flags.push(\"reversible\");\n if (action.confirmation !== \"never\") flags.push(`confirmation:${action.confirmation}`);\n return flags;\n}\n\nfunction procedureFlags(procedure: AgentProcedureDescriptor): string[] {\n const flags: string[] = [];\n if (procedure.confirmation !== \"never\") flags.push(`confirmation:${procedure.confirmation}`);\n for (const field of procedure.boundFields) {\n flags.push(`${field.path} bound${field.locked ? \"+locked\" : \"\"}`);\n }\n return flags;\n}\n\nfunction explanationIndex(explanation: SurfaceExplanation): Map<string, CapabilityExplanation> {\n const index = new Map<string, CapabilityExplanation>();\n for (const capability of explanation.capabilities) {\n // Keyed by id + registration so two instances of one component stay apart.\n index.set(`${capability.capabilityId}\\u0000${capability.registrationId}`, capability);\n }\n return index;\n}\n\nexport function buildView(result: CollectResult, options: ViewOptions = {}): SurfaceView {\n const { snapshot, explanation } = result;\n const index = explanationIndex(explanation);\n const groups: CapabilityGroup[] = [];\n const counts = { callable: 0, disabled: 0, hidden: 0 };\n\n const enrich = (\n row: CapabilityRow,\n capabilityId: string,\n registrationId: string,\n ): CapabilityRow => {\n const explained = index.get(`${capabilityId}\\u0000${registrationId}`);\n if (options.explain && explained) {\n row.policies = explained.policies;\n row.availability = explained.availability;\n }\n return row;\n };\n\n for (const component of snapshot.components) {\n const rows: CapabilityRow[] = [];\n\n for (const observation of component.observations) {\n rows.push(\n enrich(\n rowFor(observation, \"observation\", undefined, [], options, {\n input: undefined,\n output: observation.outputSchema,\n }),\n observation.capabilityId,\n component.registrationId,\n ),\n );\n }\n for (const action of component.actions) {\n rows.push(\n enrich(\n rowFor(action, \"action\", action.effect, actionFlags(action), options, {\n input: action.inputSchema,\n output: action.outputSchema,\n }),\n action.capabilityId,\n component.registrationId,\n ),\n );\n }\n\n groups.push({\n heading:\n component.instanceId === \"default\"\n ? component.type\n : `${component.type}@${component.instanceId}`,\n rows,\n });\n }\n\n if (snapshot.procedures.length > 0) {\n groups.push({\n heading: \"authoritative (domain)\",\n rows: snapshot.procedures.map((procedure) =>\n enrich(\n {\n capabilityId: procedure.procedureId,\n name: procedure.procedureId.replace(/^domain:/, \"\"),\n path: pathOf(procedure.procedureId),\n kind: \"procedure\",\n plane: \"domain\",\n outcome: procedure.available ? \"expose\" : \"disable\",\n description: procedure.description,\n ...(procedure.unavailableReason ? { reason: procedure.unavailableReason } : {}),\n effect: procedure.effect,\n flags: procedureFlags(procedure),\n tags: [procedure.effect, ...procedureFlags(procedure)],\n ...(options.schemas\n ? { schemas: { input: procedure.inputSchema, output: procedure.outputSchema } }\n : {}),\n },\n procedure.procedureId,\n procedure.registrationId,\n ),\n ),\n });\n }\n\n // Hidden capabilities exist only in the explanation — that is the whole point\n // of it. They get their own group so nobody mistakes them for callable.\n //\n // Unconditional, not behind `--explain`, for the reason `AS-CLI-007` moved\n // the hidden *count* out from behind it: signed out, the example app rendered\n // `0 callable, 0 visible-disabled` over eleven perfectly good capabilities\n // that authority had hidden, and a reader who did not know to re-run with a\n // flag read that as an app which annotated nothing. The explanation is\n // collected on every run regardless, so this costs nothing. The policy\n // *attribution* still needs `--explain`; only the rows moved.\n const hidden = explanation.capabilities.filter((c) => c.outcome === \"hide\");\n if (hidden.length > 0) {\n groups.push({\n heading: \"hidden by policy (absent from the snapshot)\",\n rows: hidden.map((capability) => ({\n capabilityId: capability.capabilityId,\n name: leafOf(capability.capabilityId),\n path: pathOf(capability.capabilityId),\n kind: capability.kind,\n plane: capability.plane,\n outcome: \"hide\" as const,\n description: capability.description,\n // No reason line, deliberately. The reason a hidden capability carries\n // is its *availability* reason — \"The drawer is not open\" — and printing\n // that under a row marked `hidden` says the UI declined when authority\n // did. Authority hides, state discloses (D11/D12), and the two must\n // never look alike. Why it was hidden is a policy question, which is\n // what `--explain` answers.\n //\n // A hidden capability has no snapshot entry, so there is no effect to\n // report — the table prints an em dash rather than inventing one. The\n // capability path already carries the component type; only a non-default\n // instance adds anything.\n flags:\n capability.component.instanceId === \"default\"\n ? []\n : [`@${capability.component.instanceId}`],\n tags: [`${capability.component.type}@${capability.component.instanceId}`],\n ...(options.explain\n ? { policies: capability.policies, availability: capability.availability }\n : {}),\n })),\n });\n }\n\n for (const capability of explanation.capabilities) {\n if (capability.outcome === \"expose\") counts.callable += 1;\n else if (capability.outcome === \"disable\") counts.disabled += 1;\n else counts.hidden += 1;\n }\n\n return {\n scenario: result.scenario,\n ...(snapshot.route?.path ? { route: snapshot.route.path } : {}),\n ...(result.scope ? { scope: result.scope } : {}),\n groups,\n counts,\n rejections: result.rejections ?? [],\n explained: options.explain === true,\n };\n}\n\nfunction rowFor(\n descriptor: AgentObservationDescriptor | AgentActionDescriptor,\n kind: \"observation\" | \"action\",\n effect: string | undefined,\n flags: string[],\n options: ViewOptions,\n schemas: { input?: unknown; output?: unknown },\n): CapabilityRow {\n return {\n capabilityId: descriptor.capabilityId,\n name: descriptor.name,\n path: pathOf(descriptor.capabilityId),\n kind,\n plane: \"view\",\n outcome: descriptor.available ? \"expose\" : \"disable\",\n description: descriptor.description,\n ...(descriptor.unavailableReason ? { reason: descriptor.unavailableReason } : {}),\n ...(effect ? { effect } : {}),\n flags,\n // The grouped detail view prints one combined list, the way it always has.\n tags: effect ? [effect, ...flags] : [kind, ...flags],\n ...(options.schemas ? { schemas } : {}),\n };\n}\n"],"mappings":";AAuEO,SAAS,SAAS,MAAoC;AAC3D,SAAO,KAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,IAAI;AAClD;AAEA,SAAS,OAAO,cAA8B;AAC5C,SAAO,aAAa,QAAQ,mBAAmB,EAAE;AACnD;AAEA,SAAS,OAAO,cAA8B;AAC5C,QAAM,eAAe,OAAO,YAAY;AACxC,QAAM,MAAM,aAAa,YAAY,GAAG;AACxC,SAAO,QAAQ,KAAK,eAAe,aAAa,MAAM,MAAM,CAAC;AAC/D;AAOA,SAAS,YAAY,QAAyC;AAC5D,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAO,WAAY,OAAM,KAAK,YAAY;AAC9C,MAAI,OAAO,WAAY,OAAM,KAAK,YAAY;AAC9C,MAAI,OAAO,iBAAiB,QAAS,OAAM,KAAK,gBAAgB,OAAO,YAAY,EAAE;AACrF,SAAO;AACT;AAEA,SAAS,eAAe,WAA+C;AACrE,QAAM,QAAkB,CAAC;AACzB,MAAI,UAAU,iBAAiB,QAAS,OAAM,KAAK,gBAAgB,UAAU,YAAY,EAAE;AAC3F,aAAW,SAAS,UAAU,aAAa;AACzC,UAAM,KAAK,GAAG,MAAM,IAAI,SAAS,MAAM,SAAS,YAAY,EAAE,EAAE;AAAA,EAClE;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,aAAqE;AAC7F,QAAM,QAAQ,oBAAI,IAAmC;AACrD,aAAW,cAAc,YAAY,cAAc;AAEjD,UAAM,IAAI,GAAG,WAAW,YAAY,KAAS,WAAW,cAAc,IAAI,UAAU;AAAA,EACtF;AACA,SAAO;AACT;AAEO,SAAS,UAAU,QAAuB,UAAuB,CAAC,GAAgB;AACvF,QAAM,EAAE,UAAU,YAAY,IAAI;AAClC,QAAM,QAAQ,iBAAiB,WAAW;AAC1C,QAAM,SAA4B,CAAC;AACnC,QAAM,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,EAAE;AAErD,QAAM,SAAS,CACb,KACA,cACA,mBACkB;AAClB,UAAM,YAAY,MAAM,IAAI,GAAG,YAAY,KAAS,cAAc,EAAE;AACpE,QAAI,QAAQ,WAAW,WAAW;AAChC,UAAI,WAAW,UAAU;AACzB,UAAI,eAAe,UAAU;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAEA,aAAW,aAAa,SAAS,YAAY;AAC3C,UAAM,OAAwB,CAAC;AAE/B,eAAW,eAAe,UAAU,cAAc;AAChD,WAAK;AAAA,QACH;AAAA,UACE,OAAO,aAAa,eAAe,QAAW,CAAC,GAAG,SAAS;AAAA,YACzD,OAAO;AAAA,YACP,QAAQ,YAAY;AAAA,UACtB,CAAC;AAAA,UACD,YAAY;AAAA,UACZ,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AACA,eAAW,UAAU,UAAU,SAAS;AACtC,WAAK;AAAA,QACH;AAAA,UACE,OAAO,QAAQ,UAAU,OAAO,QAAQ,YAAY,MAAM,GAAG,SAAS;AAAA,YACpE,OAAO,OAAO;AAAA,YACd,QAAQ,OAAO;AAAA,UACjB,CAAC;AAAA,UACD,OAAO;AAAA,UACP,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAEA,WAAO,KAAK;AAAA,MACV,SACE,UAAU,eAAe,YACrB,UAAU,OACV,GAAG,UAAU,IAAI,IAAI,UAAU,UAAU;AAAA,MAC/C;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,SAAS,WAAW,SAAS,GAAG;AAClC,WAAO,KAAK;AAAA,MACV,SAAS;AAAA,MACT,MAAM,SAAS,WAAW;AAAA,QAAI,CAAC,cAC7B;AAAA,UACE;AAAA,YACE,cAAc,UAAU;AAAA,YACxB,MAAM,UAAU,YAAY,QAAQ,YAAY,EAAE;AAAA,YAClD,MAAM,OAAO,UAAU,WAAW;AAAA,YAClC,MAAM;AAAA,YACN,OAAO;AAAA,YACP,SAAS,UAAU,YAAY,WAAW;AAAA,YAC1C,aAAa,UAAU;AAAA,YACvB,GAAI,UAAU,oBAAoB,EAAE,QAAQ,UAAU,kBAAkB,IAAI,CAAC;AAAA,YAC7E,QAAQ,UAAU;AAAA,YAClB,OAAO,eAAe,SAAS;AAAA,YAC/B,MAAM,CAAC,UAAU,QAAQ,GAAG,eAAe,SAAS,CAAC;AAAA,YACrD,GAAI,QAAQ,UACR,EAAE,SAAS,EAAE,OAAO,UAAU,aAAa,QAAQ,UAAU,aAAa,EAAE,IAC5E,CAAC;AAAA,UACP;AAAA,UACA,UAAU;AAAA,UACV,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAYA,QAAM,SAAS,YAAY,aAAa,OAAO,CAAC,MAAM,EAAE,YAAY,MAAM;AAC1E,MAAI,OAAO,SAAS,GAAG;AACrB,WAAO,KAAK;AAAA,MACV,SAAS;AAAA,MACT,MAAM,OAAO,IAAI,CAAC,gBAAgB;AAAA,QAChC,cAAc,WAAW;AAAA,QACzB,MAAM,OAAO,WAAW,YAAY;AAAA,QACpC,MAAM,OAAO,WAAW,YAAY;AAAA,QACpC,MAAM,WAAW;AAAA,QACjB,OAAO,WAAW;AAAA,QAClB,SAAS;AAAA,QACT,aAAa,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYxB,OACE,WAAW,UAAU,eAAe,YAChC,CAAC,IACD,CAAC,IAAI,WAAW,UAAU,UAAU,EAAE;AAAA,QAC5C,MAAM,CAAC,GAAG,WAAW,UAAU,IAAI,IAAI,WAAW,UAAU,UAAU,EAAE;AAAA,QACxE,GAAI,QAAQ,UACR,EAAE,UAAU,WAAW,UAAU,cAAc,WAAW,aAAa,IACvE,CAAC;AAAA,MACP,EAAE;AAAA,IACJ,CAAC;AAAA,EACH;AAEA,aAAW,cAAc,YAAY,cAAc;AACjD,QAAI,WAAW,YAAY,SAAU,QAAO,YAAY;AAAA,aAC/C,WAAW,YAAY,UAAW,QAAO,YAAY;AAAA,QACzD,QAAO,UAAU;AAAA,EACxB;AAEA,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,GAAI,SAAS,OAAO,OAAO,EAAE,OAAO,SAAS,MAAM,KAAK,IAAI,CAAC;AAAA,IAC7D,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,IAC9C;AAAA,IACA;AAAA,IACA,YAAY,OAAO,cAAc,CAAC;AAAA,IAClC,WAAW,QAAQ,YAAY;AAAA,EACjC;AACF;AAEA,SAAS,OACP,YACA,MACA,QACA,OACA,SACA,SACe;AACf,SAAO;AAAA,IACL,cAAc,WAAW;AAAA,IACzB,MAAM,WAAW;AAAA,IACjB,MAAM,OAAO,WAAW,YAAY;AAAA,IACpC;AAAA,IACA,OAAO;AAAA,IACP,SAAS,WAAW,YAAY,WAAW;AAAA,IAC3C,aAAa,WAAW;AAAA,IACxB,GAAI,WAAW,oBAAoB,EAAE,QAAQ,WAAW,kBAAkB,IAAI,CAAC;AAAA,IAC/E,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B;AAAA;AAAA,IAEA,MAAM,SAAS,CAAC,QAAQ,GAAG,KAAK,IAAI,CAAC,MAAM,GAAG,KAAK;AAAA,IACnD,GAAI,QAAQ,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvC;AACF;","names":[]}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
var DEPTHS = ["static", "runtime", "full"];
|
|
3
|
+
function isDepth(value) {
|
|
4
|
+
return typeof value === "string" && DEPTHS.includes(value);
|
|
5
|
+
}
|
|
6
|
+
var UsageError = class extends Error {
|
|
7
|
+
};
|
|
8
|
+
|
|
1
9
|
// src/load.ts
|
|
2
10
|
import { existsSync } from "fs";
|
|
3
11
|
import { dirname, isAbsolute, join, resolve } from "path";
|
|
@@ -112,8 +120,50 @@ async function createSurfaceRunner(configPath) {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
// src/output.ts
|
|
124
|
+
function isPlain(flags) {
|
|
125
|
+
if (flags.json) return true;
|
|
126
|
+
if (flags.plain) return true;
|
|
127
|
+
if (process.env["CI"]) return true;
|
|
128
|
+
if (process.env["NO_COLOR"]) return true;
|
|
129
|
+
if (process.stdout.isTTY !== true) return true;
|
|
130
|
+
return !process.stdout.columns;
|
|
131
|
+
}
|
|
132
|
+
function write(text) {
|
|
133
|
+
process.stdout.write(`${text}
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
function writeError(text) {
|
|
137
|
+
process.stderr.write(`${text}
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
var cached;
|
|
141
|
+
async function loadInk() {
|
|
142
|
+
if (cached !== void 0) return cached;
|
|
143
|
+
try {
|
|
144
|
+
cached = await import("./ink-P23VKP4H.js");
|
|
145
|
+
} catch {
|
|
146
|
+
cached = null;
|
|
147
|
+
}
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
async function paint(element) {
|
|
151
|
+
const { render } = await import("ink");
|
|
152
|
+
const instance = render(element);
|
|
153
|
+
instance.unmount();
|
|
154
|
+
await instance.waitUntilExit();
|
|
155
|
+
}
|
|
156
|
+
|
|
115
157
|
export {
|
|
158
|
+
DEPTHS,
|
|
159
|
+
isDepth,
|
|
160
|
+
UsageError,
|
|
116
161
|
findConfig,
|
|
117
|
-
createSurfaceRunner
|
|
162
|
+
createSurfaceRunner,
|
|
163
|
+
isPlain,
|
|
164
|
+
write,
|
|
165
|
+
writeError,
|
|
166
|
+
loadInk,
|
|
167
|
+
paint
|
|
118
168
|
};
|
|
119
|
-
//# sourceMappingURL=chunk-
|
|
169
|
+
//# sourceMappingURL=chunk-QIVOZAWX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contract.ts","../src/load.ts","../src/output.ts"],"sourcesContent":["/**\n * The vocabulary every layer shares, and nothing else.\n *\n * It is its own module because `bin.ts` needs both of these before it has\n * decided which command to run, and everything else in this package pulls in\n * either the TypeScript compiler or Vite the moment it is imported. A `--help`\n * that boots a TypeScript program to print a paragraph is a `--help` nobody\n * runs twice.\n */\n\nexport const DEPTHS = [\"static\", \"runtime\", \"full\"] as const;\n\n/**\n * How much of the surface a command is asked to compute.\n *\n * A presentation surface has two sources of truth and every command needs some\n * mix of both — the **catalog** this codebase authors, which is static, and the\n * **projection** a mounted scenario surfaces, which is not. Splitting those\n * across separate commands is what let a green `check` sit on top of a route no\n * scenario visits, so the split lives here instead.\n *\n * `static` reads the TypeScript program and mounts nothing — no Vite server, no\n * jsdom, no scenarios. It is the only depth that survives an app which will not\n * mount, and the only one that needs no scenarios to exist yet.\n *\n * `runtime` mounts and skips the program read, for a repository whose tsconfig\n * is wide enough that booting it costs more than the answer is worth.\n *\n * `full` does both and joins them, which is the only depth that can answer\n * *did we author something no scenario reaches*. It is the default because a\n * tool that has to be asked for the complete answer mostly gives the\n * incomplete one.\n */\nexport type Depth = (typeof DEPTHS)[number];\n\nexport function isDepth(value: unknown): value is Depth {\n return typeof value === \"string\" && (DEPTHS as readonly string[]).includes(value);\n}\n\n/**\n * The caller asked for something impossible — as opposed to the app being\n * broken, which is what a mount failure is. Both exit `2`: CI has to tell \"the\n * surface changed\" apart from \"the tool never ran\", and these are both the\n * second one.\n */\nexport class UsageError extends Error {}\n","import { existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createServer, type ViteDevServer } from \"vite\";\nimport { ViteNodeServer } from \"vite-node/server\";\nimport { ViteNodeRunner } from \"vite-node/client\";\nimport { installSourcemapsSupport } from \"vite-node/source-map\";\nimport type { CollectOptions, CollectResult } from \"./collect.js\";\nimport type { SurfaceConfig } from \"./config.js\";\n\nconst CONFIG_NAMES = [\n \"agent-surface.config.tsx\",\n \"agent-surface.config.ts\",\n \"agent-surface.config.mjs\",\n \"agent-surface.config.js\",\n];\n\n/** Walks up from `from` looking for an `agent-surface.config.*`. */\nexport function findConfig(from: string = process.cwd()): string | undefined {\n let dir = resolve(from);\n for (;;) {\n for (const name of CONFIG_NAMES) {\n const candidate = join(dir, name);\n if (existsSync(candidate)) return candidate;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/** `dist/collect.js` when installed; `src/collect.ts` when run from source. */\nfunction collectorPath(): string {\n for (const ext of [\"js\", \"ts\"]) {\n const candidate = fileURLToPath(new URL(`./collect.${ext}`, import.meta.url));\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\"could not locate the agent-surface collector module\");\n}\n\nexport interface SurfaceRunner {\n config: SurfaceConfig;\n scenarioNames: string[];\n collect(options: CollectOptions): Promise<CollectResult>;\n close(): Promise<void>;\n}\n\n/**\n * Boots a Vite dev server on the app's own config, so the config file and the\n * app modules it imports are transformed and resolved exactly as the app\n * resolves them — its aliases, its plugins, its TSX.\n */\nexport async function createSurfaceRunner(configPath: string): Promise<SurfaceRunner> {\n const absoluteConfig = isAbsolute(configPath) ? configPath : resolve(configPath);\n if (!existsSync(absoluteConfig)) {\n throw new Error(`config not found: ${absoluteConfig}`);\n }\n const root = dirname(absoluteConfig);\n\n let server: ViteDevServer;\n try {\n server = await createServer({\n root,\n logLevel: \"error\",\n // `serve` so plugins behave as they do in dev; nothing is ever served.\n server: { middlewareMode: true, watch: null, fs: { strict: false } },\n optimizeDeps: { noDiscovery: true, include: [] },\n resolve: {\n // Both halves of the graph must agree on these. React because two\n // copies break hooks; core because `explainSurface` finds the registry\n // through a Symbol, which is per-module-instance (see collect.ts).\n dedupe: [\n \"react\",\n \"react-dom\",\n \"@agent-surface/core\",\n \"@agent-surface/react\",\n \"@agent-surface/testing\",\n ],\n },\n });\n } catch (error) {\n throw new Error(\n `could not start Vite for ${root}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n try {\n await server.pluginContainer.buildStart({});\n } catch {\n // Vite keeps moving this; a plugin that needs buildStart will say so itself.\n }\n\n const nodeServer = new ViteNodeServer(server);\n installSourcemapsSupport({ getSourceMap: (source) => nodeServer.getSourceMap(source) });\n\n const runner = new ViteNodeRunner({\n root: server.config.root,\n base: server.config.base,\n fetchModule: (id) => nodeServer.fetchModule(id),\n resolveId: (id, importer) => nodeServer.resolveId(id, importer),\n });\n\n const close = async (): Promise<void> => {\n await server.close();\n };\n\n try {\n const configModule = (await runner.executeFile(absoluteConfig)) as {\n default?: SurfaceConfig;\n };\n const config = configModule.default;\n if (!config || typeof config.mount !== \"function\") {\n throw new Error(\n `${absoluteConfig} must \\`export default defineSurface({ mount, scenarios })\\``,\n );\n }\n const scenarioNames = Object.keys(config.scenarios ?? {});\n if (scenarioNames.length === 0) {\n throw new Error(`${absoluteConfig} defines no scenarios`);\n }\n\n // Same runner ⇒ same module graph ⇒ the collector shares React and core\n // with the app tree it is about to mount.\n const collector = (await runner.executeFile(collectorPath())) as {\n collect(config: SurfaceConfig, options: CollectOptions): Promise<CollectResult>;\n };\n\n return {\n config,\n scenarioNames,\n collect: async (options) => {\n // Scoped to the mount, never process-wide: `act()` needs it, and Ink\n // renders its own React tree afterwards — with the flag still set,\n // every frame of the CLI's own UI prints React's \"not wrapped in\n // act(...)\" warning at the user.\n const globals = globalThis as Record<string, unknown>;\n const previous = globals[\"IS_REACT_ACT_ENVIRONMENT\"];\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = true;\n try {\n return await collector.collect(config, options);\n } finally {\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = previous;\n }\n },\n close,\n };\n } catch (error) {\n await close();\n throw error;\n }\n}\n","import type { ReactElement } from \"react\";\n\nexport interface OutputFlags {\n plain?: boolean;\n json?: boolean;\n}\n\n/**\n * Terminal-aware only when there is a terminal. Piped output, `--plain`, `CI`\n * and `NO_COLOR` all fall back to plain text — a CLI whose output changes shape\n * when redirected is unusable in a build log.\n */\nexport function isPlain(flags: OutputFlags): boolean {\n if (flags.json) return true;\n if (flags.plain) return true;\n if (process.env[\"CI\"]) return true;\n if (process.env[\"NO_COLOR\"]) return true;\n if (process.stdout.isTTY !== true) return true;\n // A TTY that cannot report its width (some CI ptys, `script` on macOS) makes\n // Ink lay out at zero columns and emit one character per line. Plain text is\n // the only honest rendering for a terminal whose size is unknown.\n return !process.stdout.columns;\n}\n\nexport function write(text: string): void {\n process.stdout.write(`${text}\\n`);\n}\n\nexport function writeError(text: string): void {\n process.stderr.write(`${text}\\n`);\n}\n\ntype InkModule = typeof import(\"./render/ink.js\");\n\nlet cached: InkModule | null | undefined;\n\n/**\n * Loads the Ink renderer, or returns `null` when it cannot run here.\n *\n * Two reasons this is lazy rather than a top-level import. It keeps `--plain`\n * and `--json` from paying for a terminal UI they never draw — and Ink drives\n * React through `react-reconciler`, which reads React 19 internals, so a host\n * that pins React 18 globally cannot load it at all. Neither is a reason to\n * fail a command that was about to print text.\n */\nexport async function loadInk(): Promise<InkModule | null> {\n if (cached !== undefined) return cached;\n try {\n cached = await import(\"./render/ink.js\");\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** Paints an Ink element once and returns when the frame has been flushed. */\nexport async function paint(element: ReactElement): Promise<void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n instance.unmount();\n await instance.waitUntilExit();\n}\n\n/** A live Ink frame (spinner) that is cleared before the real output lands. */\nexport async function transient(element: ReactElement): Promise<() => void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n return () => {\n instance.clear();\n instance.unmount();\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -70,7 +70,22 @@ interface AuthoredCapability {
|
|
|
70
70
|
resolution: "static" | "partial" | "unresolved";
|
|
71
71
|
/** Present on `partial`/`unresolved`: what defeated the extractor. */
|
|
72
72
|
note?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Present on `unresolved`: *which* construct defeated the extractor, as a
|
|
75
|
+
* stable code rather than prose.
|
|
76
|
+
*
|
|
77
|
+
* `note` is written for a human and gets reworded — the spread note changed
|
|
78
|
+
* in the same release that introduced it. Anything keyed on that prose would
|
|
79
|
+
* silently invalidate itself on an edit no one thought was behavioural, which
|
|
80
|
+
* is exactly what `unresolved-allow.json` must not do. This is the key.
|
|
81
|
+
*/
|
|
82
|
+
reason?: UnreadReason;
|
|
73
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Why a registration could not be fully read. Stable identifiers: adding one is
|
|
86
|
+
* fine, renaming one invalidates committed allowlists and is a breaking change.
|
|
87
|
+
*/
|
|
88
|
+
type UnreadReason = "dynamic-type" | "dynamic-config" | "dynamic-group" | "spread-members" | "computed-name" | "granular-hook";
|
|
74
89
|
interface CapabilityInventory {
|
|
75
90
|
capabilities: AuthoredCapability[];
|
|
76
91
|
/** Absolute path to the tsconfig whose file list was analyzed. */
|
|
@@ -111,11 +126,25 @@ interface UnreachedCapability {
|
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
128
|
interface CoverageReport {
|
|
114
|
-
/** Distinct capability ids the inventory resolved. */
|
|
129
|
+
/** Distinct capability ids the inventory resolved, within any active scope. */
|
|
115
130
|
authored: number;
|
|
116
131
|
/** How many of them at least one scenario surfaced. */
|
|
117
132
|
reached: number;
|
|
118
133
|
scenarios: string[];
|
|
134
|
+
/**
|
|
135
|
+
* The scope every number here was computed under (`AS-CLI-007`). A scope
|
|
136
|
+
* filters the catalog *and* the mount, so `10 authored` without it on screen
|
|
137
|
+
* reads as a claim about the whole codebase when it is a claim about one
|
|
138
|
+
* prefix of it.
|
|
139
|
+
*/
|
|
140
|
+
scope?: string[];
|
|
141
|
+
/**
|
|
142
|
+
* Allowlist entries outside the active scope, which a scoped run cannot
|
|
143
|
+
* judge: not unreached (nothing looked), not stale (nothing reached them).
|
|
144
|
+
* Counted rather than silently dropped, so a scoped run never reads as a
|
|
145
|
+
* verdict on the whole allowlist.
|
|
146
|
+
*/
|
|
147
|
+
allowlistOutOfScope: number;
|
|
119
148
|
/** Authored, surfaced by no scenario, and not allowlisted — the finding. */
|
|
120
149
|
unreached: UnreachedCapability[];
|
|
121
150
|
/**
|
|
@@ -130,13 +159,18 @@ interface CoverageReport {
|
|
|
130
159
|
* defect, which is the misleading check this whole command rejects.
|
|
131
160
|
*/
|
|
132
161
|
domainReached: string[];
|
|
133
|
-
/** Carried forward from the inventory. */
|
|
162
|
+
/** Carried forward from the inventory, minus anything allowlisted. */
|
|
134
163
|
unresolved: AuthoredCapability[];
|
|
135
164
|
/** Unreached, but listed in the allowlist. */
|
|
136
165
|
allowed: string[];
|
|
137
166
|
/** Listed in the allowlist and reached anyway — the list has rotted. */
|
|
138
167
|
staleAllowlist: string[];
|
|
139
168
|
allowlistPath: string;
|
|
169
|
+
/** Unread, but listed in `unresolved-allow.json`. Keys, not entries. */
|
|
170
|
+
allowedUnread: string[];
|
|
171
|
+
/** Listed there and no longer unread — that list has rotted too. */
|
|
172
|
+
staleUnreadAllowlist: string[];
|
|
173
|
+
unreadAllowlistPath: string;
|
|
140
174
|
}
|
|
141
175
|
|
|
142
176
|
export type { AuthoredCapability, CapabilityInventory, CollectResult, CoverageAllowlist, CoverageReport, RegistrationRejection };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authoredIds,
|
|
3
|
+
extractCapabilities,
|
|
4
|
+
findTsconfig,
|
|
5
|
+
unresolved
|
|
6
|
+
} from "./chunk-2FG527AM.js";
|
|
7
|
+
import {
|
|
8
|
+
UsageError,
|
|
9
|
+
isPlain,
|
|
10
|
+
loadInk,
|
|
11
|
+
write,
|
|
12
|
+
writeError
|
|
13
|
+
} from "./chunk-QIVOZAWX.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/init.tsx
|
|
16
|
+
import { existsSync, writeFileSync } from "fs";
|
|
17
|
+
import { join, relative } from "path";
|
|
18
|
+
import { jsx } from "react/jsx-runtime";
|
|
19
|
+
var CONFIG_NAME = "agent-surface.config.tsx";
|
|
20
|
+
var ENTRY_CANDIDATES = [
|
|
21
|
+
"src/main.tsx",
|
|
22
|
+
"src/main.ts",
|
|
23
|
+
"src/index.tsx",
|
|
24
|
+
"src/App.tsx",
|
|
25
|
+
"src/app/App.tsx",
|
|
26
|
+
"app/root.tsx"
|
|
27
|
+
];
|
|
28
|
+
function scaffold(entry) {
|
|
29
|
+
const importPath = entry ? `./${entry.replace(/\.tsx?$/, ".js")}` : "./src/App.js";
|
|
30
|
+
return `import { defineSurface } from "@agent-surface/cli";
|
|
31
|
+
// TODO: point these at your own composition root \u2014 whatever \`main.tsx\` calls.
|
|
32
|
+
// The config should *reuse* how the app builds itself, not restate it.
|
|
33
|
+
import { App } from "${importPath}";
|
|
34
|
+
|
|
35
|
+
export default defineSurface({
|
|
36
|
+
mount: ({ user }) => {
|
|
37
|
+
// TODO: build the app the way the app builds itself, and hand back the
|
|
38
|
+
// registry it created plus the tree that registers into it.
|
|
39
|
+
const app = createApp({ environment: "test", user });
|
|
40
|
+
return { registry: app.registry, ui: <App app={app} />, app };
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Named prop bundles. Free-form \u2014 a user, a route, a feature flag; the CLI
|
|
44
|
+
// never interprets them. Every scenario you leave out is a surface nothing
|
|
45
|
+
// measures, which is what \`--depth full\` reports as unreached.
|
|
46
|
+
scenarios: {
|
|
47
|
+
default: { user: { id: "u_1", permissions: [] } },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
async function runInit(options) {
|
|
53
|
+
const configPath = join(options.cwd, CONFIG_NAME);
|
|
54
|
+
if (existsSync(configPath)) {
|
|
55
|
+
throw new UsageError(
|
|
56
|
+
`${relative(process.cwd(), configPath)} already exists \u2014 edit it, or delete it and re-run`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const tsconfig = options.tsconfig ?? findTsconfig(options.cwd);
|
|
60
|
+
if (!tsconfig) {
|
|
61
|
+
throw new UsageError(
|
|
62
|
+
`no tsconfig.json found from ${options.cwd} \u2014 agent-surface reads your TypeScript program to find registration call sites, and cannot do that without one`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const inventory = extractCapabilities({ root: options.cwd, tsconfig });
|
|
66
|
+
const ids = authoredIds(inventory);
|
|
67
|
+
const unread = unresolved(inventory);
|
|
68
|
+
const components = new Set(
|
|
69
|
+
[...ids].map((id) => id.replace(/^view:/, "").split(".").slice(0, -1).join("."))
|
|
70
|
+
);
|
|
71
|
+
write(`Read ${inventory.filesAnalyzed} file${inventory.filesAnalyzed === 1 ? "" : "s"} from ${relative(process.cwd(), tsconfig) || "tsconfig.json"}`);
|
|
72
|
+
write("");
|
|
73
|
+
write(` authored capabilities ${ids.size}`);
|
|
74
|
+
write(` components ${components.size}`);
|
|
75
|
+
write(` unread call sites ${unread.length}`);
|
|
76
|
+
if (ids.size === 0) {
|
|
77
|
+
write("");
|
|
78
|
+
write(
|
|
79
|
+
"Nothing is annotated yet \u2014 that is the default, and it is the safe one: a capability exists only where someone wrote one. Start with `useAgentComponent` in a component that owns state worth acting on, then re-run this."
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
write("");
|
|
83
|
+
for (const component of [...components].sort()) write(` ${component}`);
|
|
84
|
+
}
|
|
85
|
+
const entry = ENTRY_CANDIDATES.find((candidate) => existsSync(join(options.cwd, candidate)));
|
|
86
|
+
write("");
|
|
87
|
+
write(`Write ${relative(process.cwd(), configPath)}?`);
|
|
88
|
+
write(
|
|
89
|
+
entry ? ` it will import from ./${entry}, which you will still have to wire into a mount()` : " no app entry found, so the import line is a placeholder you will have to point somewhere"
|
|
90
|
+
);
|
|
91
|
+
if (!options.yes) {
|
|
92
|
+
const answered = await ask(options, `Write ${CONFIG_NAME}?`);
|
|
93
|
+
if (!answered) {
|
|
94
|
+
write("");
|
|
95
|
+
write("Nothing written.");
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
writeFileSync(configPath, scaffold(entry), "utf8");
|
|
100
|
+
write("");
|
|
101
|
+
write(`wrote ${relative(process.cwd(), configPath)}`);
|
|
102
|
+
write("");
|
|
103
|
+
write("Next:");
|
|
104
|
+
write(" 1. fill in mount() \u2014 it should call your existing composition root");
|
|
105
|
+
write(" 2. `agent-surface inspect` to see what an agent can reach");
|
|
106
|
+
write(" 3. `agent-surface snapshot` to commit the baseline, then `check` in CI");
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
async function ask(options, question) {
|
|
110
|
+
if (isPlain(options) || process.stdin.isTTY !== true) {
|
|
111
|
+
writeError("");
|
|
112
|
+
writeError("stdin is not a terminal, so there is nobody to ask \u2014 re-run with --yes to accept.");
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const ink = await loadInk();
|
|
116
|
+
if (!ink) {
|
|
117
|
+
writeError("");
|
|
118
|
+
writeError("no interactive renderer available here \u2014 re-run with --yes to accept.");
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const { render } = await import("ink");
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const instance = render(
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
ink.Confirm,
|
|
126
|
+
{
|
|
127
|
+
question,
|
|
128
|
+
onAnswer: (yes) => {
|
|
129
|
+
instance.clear();
|
|
130
|
+
instance.unmount();
|
|
131
|
+
resolve(yes);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
runInit
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=init-ODFEGU3P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/init.tsx"],"sourcesContent":["import { existsSync, writeFileSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { UsageError } from \"../analysis.js\";\nimport { authoredIds, extractCapabilities, findTsconfig, unresolved } from \"../extract.js\";\nimport { isPlain, loadInk, write, writeError } from \"../output.js\";\n\nexport interface InitOptions {\n cwd: string;\n tsconfig?: string;\n yes?: boolean;\n plain?: boolean;\n}\n\nconst CONFIG_NAME = \"agent-surface.config.tsx\";\n\n/**\n * Where an app is usually assembled. `init` does not *probe* these — it cannot,\n * because a surface config needs a `mount()` that builds the app, and there is\n * no export a tool can import to get one. It names the likeliest file so the\n * scaffold's import line points somewhere real more often than not.\n */\nconst ENTRY_CANDIDATES = [\n \"src/main.tsx\",\n \"src/main.ts\",\n \"src/index.tsx\",\n \"src/App.tsx\",\n \"src/app/App.tsx\",\n \"app/root.tsx\",\n];\n\nfunction scaffold(entry: string | undefined): string {\n const importPath = entry ? `./${entry.replace(/\\.tsx?$/, \".js\")}` : \"./src/App.js\";\n return `import { defineSurface } from \"@agent-surface/cli\";\n// TODO: point these at your own composition root — whatever \\`main.tsx\\` calls.\n// The config should *reuse* how the app builds itself, not restate it.\nimport { App } from \"${importPath}\";\n\nexport default defineSurface({\n mount: ({ user }) => {\n // TODO: build the app the way the app builds itself, and hand back the\n // registry it created plus the tree that registers into it.\n const app = createApp({ environment: \"test\", user });\n return { registry: app.registry, ui: <App app={app} />, app };\n },\n\n // Named prop bundles. Free-form — a user, a route, a feature flag; the CLI\n // never interprets them. Every scenario you leave out is a surface nothing\n // measures, which is what \\`--depth full\\` reports as unreached.\n scenarios: {\n default: { user: { id: \"u_1\", permissions: [] } },\n },\n});\n`;\n}\n\n/**\n * `agent-surface init` — the on-ramp.\n *\n * It reads the codebase first and writes nothing before it has shown you what\n * it found. That order is the whole point: the number it prints is the one\n * every later command is relative to, and a scaffold that appears before the\n * summary asks you to accept a config for a codebase neither of you has looked\n * at yet.\n *\n * It mounts nothing and needs no config to exist — it is `--depth static` with\n * a file write on the end.\n */\nexport async function runInit(options: InitOptions): Promise<number> {\n const configPath = join(options.cwd, CONFIG_NAME);\n if (existsSync(configPath)) {\n throw new UsageError(\n `${relative(process.cwd(), configPath)} already exists — edit it, or delete it and re-run`,\n );\n }\n\n const tsconfig = options.tsconfig ?? findTsconfig(options.cwd);\n if (!tsconfig) {\n throw new UsageError(\n `no tsconfig.json found from ${options.cwd} — agent-surface reads your TypeScript program ` +\n \"to find registration call sites, and cannot do that without one\",\n );\n }\n\n const inventory = extractCapabilities({ root: options.cwd, tsconfig });\n const ids = authoredIds(inventory);\n const unread = unresolved(inventory);\n const components = new Set(\n [...ids].map((id) => id.replace(/^view:/, \"\").split(\".\").slice(0, -1).join(\".\")),\n );\n\n write(`Read ${inventory.filesAnalyzed} file${inventory.filesAnalyzed === 1 ? \"\" : \"s\"} from ${relative(process.cwd(), tsconfig) || \"tsconfig.json\"}`);\n write(\"\");\n write(` authored capabilities ${ids.size}`);\n write(` components ${components.size}`);\n write(` unread call sites ${unread.length}`);\n\n if (ids.size === 0) {\n write(\"\");\n write(\n \"Nothing is annotated yet — that is the default, and it is the safe one: a capability \" +\n \"exists only where someone wrote one. Start with `useAgentComponent` in a component \" +\n \"that owns state worth acting on, then re-run this.\",\n );\n } else {\n write(\"\");\n for (const component of [...components].sort()) write(` ${component}`);\n }\n\n const entry = ENTRY_CANDIDATES.find((candidate) => existsSync(join(options.cwd, candidate)));\n write(\"\");\n write(`Write ${relative(process.cwd(), configPath)}?`);\n write(\n entry\n ? ` it will import from ./${entry}, which you will still have to wire into a mount()`\n : \" no app entry found, so the import line is a placeholder you will have to point somewhere\",\n );\n\n if (!options.yes) {\n const answered = await ask(options, `Write ${CONFIG_NAME}?`);\n if (!answered) {\n write(\"\");\n write(\"Nothing written.\");\n return 0;\n }\n }\n\n writeFileSync(configPath, scaffold(entry), \"utf8\");\n write(\"\");\n write(`wrote ${relative(process.cwd(), configPath)}`);\n write(\"\");\n write(\"Next:\");\n write(\" 1. fill in mount() — it should call your existing composition root\");\n write(\" 2. `agent-surface inspect` to see what an agent can reach\");\n write(\" 3. `agent-surface snapshot` to commit the baseline, then `check` in CI\");\n return 0;\n}\n\n/**\n * There is no prompt to give when nothing is attached to answer it. Failing\n * with the flag that would have worked beats writing a file the caller never\n * agreed to, and beats hanging on a read that will never return.\n */\nasync function ask(options: InitOptions, question: string): Promise<boolean> {\n if (isPlain(options) || process.stdin.isTTY !== true) {\n writeError(\"\");\n writeError(\"stdin is not a terminal, so there is nobody to ask — re-run with --yes to accept.\");\n return false;\n }\n const ink = await loadInk();\n if (!ink) {\n writeError(\"\");\n writeError(\"no interactive renderer available here — re-run with --yes to accept.\");\n return false;\n }\n const { render } = await import(\"ink\");\n return new Promise<boolean>((resolve) => {\n const instance = render(\n <ink.Confirm\n question={question}\n onAnswer={(yes) => {\n instance.clear();\n instance.unmount();\n resolve(yes);\n }}\n />,\n );\n });\n}\n"],"mappings":"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","names":[]}
|