@oprt/cli 0.1.0 → 0.3.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 +48 -23
- package/dist/bin.js +1750 -673
- package/dist/contract.json +1 -1
- package/dist/index.cjs +1493 -408
- package/dist/index.d.cts +259 -90
- package/dist/index.d.ts +259 -90
- package/dist/index.js +1470 -397
- package/dist/{read-8a9euzJx.d.cts → read-C7rE5UlY.d.cts} +48 -21
- package/dist/{read-8a9euzJx.d.ts → read-C7rE5UlY.d.ts} +48 -21
- package/dist/read.cjs +2 -1
- package/dist/read.d.cts +1 -1
- package/dist/read.d.ts +1 -1
- package/dist/read.js +2 -1
- package/dist/registry.json +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -22,26 +22,38 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CliError: () => CliError,
|
|
24
24
|
DEFAULT_OUT: () => DEFAULT_OUT,
|
|
25
|
+
RESULT_SCHEMAS: () => RESULT_SCHEMAS,
|
|
26
|
+
RESULT_SCHEMA_VERSION: () => RESULT_SCHEMA_VERSION,
|
|
27
|
+
RESULT_TYPES: () => RESULT_TYPES,
|
|
25
28
|
TOOLS: () => TOOLS,
|
|
26
29
|
VERSION: () => VERSION,
|
|
27
30
|
applyBlock: () => applyBlock,
|
|
28
31
|
blockVersion: () => blockVersion,
|
|
32
|
+
buildCheckReport: () => buildCheckReport,
|
|
33
|
+
checkExitCode: () => checkExitCode,
|
|
29
34
|
checkPaths: () => checkPaths,
|
|
30
35
|
checkSource: () => checkSource,
|
|
31
36
|
componentDetail: () => componentDetail,
|
|
32
37
|
componentIndex: () => componentIndex,
|
|
33
38
|
contract: () => contract,
|
|
39
|
+
contractDigest: () => contractDigest,
|
|
34
40
|
doctor: () => doctor,
|
|
41
|
+
envelope: () => envelope,
|
|
42
|
+
errorEnvelope: () => errorEnvelope,
|
|
35
43
|
handleMessage: () => handleMessage,
|
|
44
|
+
identityOf: () => identityOf,
|
|
36
45
|
init: () => init,
|
|
37
46
|
install: () => install,
|
|
47
|
+
outputSchemaFor: () => outputSchemaFor,
|
|
38
48
|
parseArgs: () => parseArgs,
|
|
39
49
|
registry: () => registry,
|
|
40
50
|
removeBlock: () => removeBlock,
|
|
41
51
|
renderAgentBlock: () => renderAgentBlock,
|
|
52
|
+
renderCheckText: () => renderCheckText,
|
|
42
53
|
renderComponent: () => renderComponent,
|
|
43
54
|
resolveEntries: () => resolveEntries,
|
|
44
55
|
runAgentCommand: () => runAgentCommand,
|
|
56
|
+
schemaFor: () => schemaFor,
|
|
45
57
|
search: () => search,
|
|
46
58
|
serve: () => serve
|
|
47
59
|
});
|
|
@@ -53,78 +65,9 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
53
65
|
|
|
54
66
|
// src/index.ts
|
|
55
67
|
var import_node_fs6 = require("fs");
|
|
56
|
-
var
|
|
68
|
+
var import_node_path7 = require("path");
|
|
57
69
|
var import_node_url3 = require("url");
|
|
58
70
|
|
|
59
|
-
// package.json
|
|
60
|
-
var package_default = {
|
|
61
|
-
name: "@oprt/cli",
|
|
62
|
-
version: "0.1.0",
|
|
63
|
-
description: "The OPERATOR command line: copy components and recipes into your app as source, give coding agents the contract, and check their code against it. Includes an MCP server.",
|
|
64
|
-
keywords: [
|
|
65
|
-
"design-system",
|
|
66
|
-
"cli",
|
|
67
|
-
"coding-agents",
|
|
68
|
-
"mcp",
|
|
69
|
-
"model-context-protocol",
|
|
70
|
-
"code-check",
|
|
71
|
-
"components"
|
|
72
|
-
],
|
|
73
|
-
homepage: "https://oprt.dev/docs/agents/cli",
|
|
74
|
-
type: "module",
|
|
75
|
-
main: "./dist/index.cjs",
|
|
76
|
-
module: "./dist/index.js",
|
|
77
|
-
types: "./dist/index.d.ts",
|
|
78
|
-
bin: {
|
|
79
|
-
oprt: "./dist/bin.js"
|
|
80
|
-
},
|
|
81
|
-
engines: {
|
|
82
|
-
node: ">=22"
|
|
83
|
-
},
|
|
84
|
-
exports: {
|
|
85
|
-
".": {
|
|
86
|
-
import: {
|
|
87
|
-
types: "./dist/index.d.ts",
|
|
88
|
-
default: "./dist/index.js"
|
|
89
|
-
},
|
|
90
|
-
require: {
|
|
91
|
-
types: "./dist/index.d.cts",
|
|
92
|
-
default: "./dist/index.cjs"
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
"./read": {
|
|
96
|
-
import: {
|
|
97
|
-
types: "./dist/read.d.ts",
|
|
98
|
-
default: "./dist/read.js"
|
|
99
|
-
},
|
|
100
|
-
require: {
|
|
101
|
-
types: "./dist/read.d.cts",
|
|
102
|
-
default: "./dist/read.cjs"
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
files: [
|
|
107
|
-
"dist"
|
|
108
|
-
],
|
|
109
|
-
scripts: {
|
|
110
|
-
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
111
|
-
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
112
|
-
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
113
|
-
test: "vitest run",
|
|
114
|
-
typecheck: "tsc --noEmit",
|
|
115
|
-
lint: "eslint src scripts"
|
|
116
|
-
},
|
|
117
|
-
devDependencies: {
|
|
118
|
-
tsup: "^8.3.0",
|
|
119
|
-
typescript: "^5.6.3",
|
|
120
|
-
vitest: "^2.1.3"
|
|
121
|
-
},
|
|
122
|
-
license: "MIT",
|
|
123
|
-
publishConfig: {
|
|
124
|
-
access: "public"
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
71
|
// src/contract.ts
|
|
129
72
|
var import_node_fs = require("fs");
|
|
130
73
|
var import_node_path = require("path");
|
|
@@ -159,6 +102,197 @@ var CliError = class extends Error {
|
|
|
159
102
|
suggestions;
|
|
160
103
|
};
|
|
161
104
|
|
|
105
|
+
// src/lookup.ts
|
|
106
|
+
var SEARCH_LIMIT_MAX = 50;
|
|
107
|
+
var SEARCH_LIMIT_DEFAULT = 10;
|
|
108
|
+
var squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
109
|
+
var firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
|
|
110
|
+
function distance(a, b) {
|
|
111
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
112
|
+
for (let i = 1; i <= a.length; i++) {
|
|
113
|
+
let diagonal = row[0];
|
|
114
|
+
row[0] = i;
|
|
115
|
+
for (let j = 1; j <= b.length; j++) {
|
|
116
|
+
const above = row[j];
|
|
117
|
+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
118
|
+
diagonal = above;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return row[b.length];
|
|
122
|
+
}
|
|
123
|
+
function nearest(wanted, names2, limit = 5) {
|
|
124
|
+
const target = squash(wanted);
|
|
125
|
+
return names2.map((name) => {
|
|
126
|
+
const candidate = squash(name);
|
|
127
|
+
const apart = distance(target, candidate);
|
|
128
|
+
const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
|
|
129
|
+
return { name, apart, close };
|
|
130
|
+
}).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
|
|
131
|
+
}
|
|
132
|
+
function resolveComponent(contract2, wanted) {
|
|
133
|
+
const names2 = Object.keys(contract2.components);
|
|
134
|
+
const found = names2.find((name) => name === wanted) ?? names2.find((name) => squash(name) === squash(wanted));
|
|
135
|
+
if (found !== void 0) return found;
|
|
136
|
+
const suggestions = nearest(wanted, names2);
|
|
137
|
+
throw new CliError(
|
|
138
|
+
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
139
|
+
2,
|
|
140
|
+
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function allowedChildren(rules, name) {
|
|
144
|
+
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
145
|
+
if (parent === name) return rule.allowed_children;
|
|
146
|
+
const nested = allowedChildren(rule.children, name);
|
|
147
|
+
if (nested !== void 0) return nested;
|
|
148
|
+
}
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
152
|
+
(part) => contract2.guidance.components[part]?.part_of === name
|
|
153
|
+
);
|
|
154
|
+
function componentIndex(contract2) {
|
|
155
|
+
return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
156
|
+
name,
|
|
157
|
+
purpose: contract2.guidance.components[name].use_for.join("; "),
|
|
158
|
+
parts: partsOf(contract2, name)
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function componentDetail(contract2, wanted) {
|
|
162
|
+
const name = resolveComponent(contract2, wanted);
|
|
163
|
+
const component = contract2.components[name];
|
|
164
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
165
|
+
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
166
|
+
const notes = guide.values?.[prop];
|
|
167
|
+
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
168
|
+
});
|
|
169
|
+
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
description: component.description,
|
|
173
|
+
use_for: guide.use_for ?? [],
|
|
174
|
+
not_for: guide.not_for ?? [],
|
|
175
|
+
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
176
|
+
parts: partsOf(contract2, name),
|
|
177
|
+
props,
|
|
178
|
+
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
179
|
+
composable_with: component.composable_with ?? [],
|
|
180
|
+
never_inside: component.never_inside ?? [],
|
|
181
|
+
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
182
|
+
example: component.example
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function propLine(prop, dense) {
|
|
186
|
+
const kind = prop.type === "enum" ? (prop.values ?? []).map((value) => value === prop.default ? `${value} (default)` : value).join(" | ") : (prop.type === "union" ? (prop.types ?? []).join(" or ") : prop.type) + (prop.default === void 0 ? "" : ` (default ${String(prop.default)})`);
|
|
187
|
+
const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
|
|
188
|
+
const lines = [
|
|
189
|
+
` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
|
|
190
|
+
];
|
|
191
|
+
for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
function renderComponent(detail, { dense }) {
|
|
195
|
+
const lines = [
|
|
196
|
+
detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
|
|
197
|
+
];
|
|
198
|
+
if (detail.not_for.length > 0)
|
|
199
|
+
lines.push(
|
|
200
|
+
`Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
|
|
201
|
+
);
|
|
202
|
+
if (!dense) lines.push("", detail.description, "");
|
|
203
|
+
if (detail.props.length > 0) {
|
|
204
|
+
lines.push("Props:");
|
|
205
|
+
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
206
|
+
}
|
|
207
|
+
for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
|
|
208
|
+
lines.push(
|
|
209
|
+
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
210
|
+
);
|
|
211
|
+
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
212
|
+
if (detail.allowed_children !== void 0)
|
|
213
|
+
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
214
|
+
if (detail.composable_with.length > 0)
|
|
215
|
+
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
216
|
+
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
217
|
+
lines.push(
|
|
218
|
+
detail.example.includes("\n") ? `Example:
|
|
219
|
+
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
220
|
+
);
|
|
221
|
+
return `${lines.join("\n")}
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
var STOP = new Set(
|
|
225
|
+
"a an the to of for in on at with and or i we need want that which is are it its my some from by".split(
|
|
226
|
+
" "
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
var words = (text2) => (text2.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((word) => !STOP.has(word)).map((word) => word.length > 4 ? word.replace(/(?:ing|ed|es|s)$/, "") : word);
|
|
230
|
+
var matched = (query, text2) => {
|
|
231
|
+
const present = new Set(words(text2));
|
|
232
|
+
return query.filter((word) => present.has(word)).length;
|
|
233
|
+
};
|
|
234
|
+
function search(contract2, registry2, query, limit = SEARCH_LIMIT_DEFAULT) {
|
|
235
|
+
const wanted = words(query);
|
|
236
|
+
if (wanted.length === 0) return [];
|
|
237
|
+
const whole = squash(query);
|
|
238
|
+
const scores = /* @__PURE__ */ new Map();
|
|
239
|
+
const add = (name, points, why) => {
|
|
240
|
+
const before = scores.get(name) ?? { score: 0 };
|
|
241
|
+
scores.set(name, {
|
|
242
|
+
score: before.score + points,
|
|
243
|
+
...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
for (const [name, component] of Object.entries(contract2.components)) {
|
|
247
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
248
|
+
const plain = squash(name);
|
|
249
|
+
if (plain === whole) add(name, 100);
|
|
250
|
+
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
251
|
+
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
252
|
+
for (const phrase of guide.use_for ?? []) {
|
|
253
|
+
const hits = matched(wanted, phrase);
|
|
254
|
+
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
255
|
+
}
|
|
256
|
+
for (const { need, use } of guide.not_for ?? []) {
|
|
257
|
+
const hits = matched(wanted, need);
|
|
258
|
+
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
259
|
+
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
260
|
+
}
|
|
261
|
+
const inDescription = matched(wanted, component.description);
|
|
262
|
+
if (inDescription > 0) add(name, 2 * inDescription);
|
|
263
|
+
}
|
|
264
|
+
const results = [];
|
|
265
|
+
for (const [name, { score, why }] of scores) {
|
|
266
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
267
|
+
results.push({
|
|
268
|
+
kind: "component",
|
|
269
|
+
name,
|
|
270
|
+
purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
|
|
271
|
+
score,
|
|
272
|
+
...why === void 0 ? {} : { why },
|
|
273
|
+
next: `oprt component ${name}`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
|
|
277
|
+
const recipe = contract2.patterns?.[entry.name];
|
|
278
|
+
const plain = squash(entry.name);
|
|
279
|
+
const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
|
|
280
|
+
const hits = matched(wanted, entry.description);
|
|
281
|
+
const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
|
|
282
|
+
if (score > 0)
|
|
283
|
+
results.push({
|
|
284
|
+
kind: "recipe",
|
|
285
|
+
name: entry.name,
|
|
286
|
+
purpose: firstSentence(entry.description),
|
|
287
|
+
score,
|
|
288
|
+
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
292
|
+
const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
|
|
293
|
+
return results.filter(({ score }) => score >= floor).slice(0, limit);
|
|
294
|
+
}
|
|
295
|
+
|
|
162
296
|
// src/args.ts
|
|
163
297
|
var DEFAULT_OUT = "src/components/operator";
|
|
164
298
|
function split(command, rest, spec) {
|
|
@@ -214,9 +348,10 @@ var AGENT_COMMANDS = {
|
|
|
214
348
|
"--json": "flag"
|
|
215
349
|
});
|
|
216
350
|
if (positional.length === 0) throw new CliError("Usage: oprt search <need> [--limit <n>]", 2);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
351
|
+
const given = value("--limit");
|
|
352
|
+
const limit = given === void 0 ? SEARCH_LIMIT_DEFAULT : Number(given);
|
|
353
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > SEARCH_LIMIT_MAX)
|
|
354
|
+
throw new CliError(`--limit takes a whole number from 1 to ${SEARCH_LIMIT_MAX}.`, 2);
|
|
220
355
|
return { command: "search", query: positional.join(" "), limit, json: has("--json") };
|
|
221
356
|
},
|
|
222
357
|
init(rest) {
|
|
@@ -279,7 +414,8 @@ function parseArgs(args) {
|
|
|
279
414
|
names: [],
|
|
280
415
|
out: DEFAULT_OUT,
|
|
281
416
|
dryRun: false,
|
|
282
|
-
overwrite: false
|
|
417
|
+
overwrite: false,
|
|
418
|
+
json: false
|
|
283
419
|
};
|
|
284
420
|
const seen = /* @__PURE__ */ new Set();
|
|
285
421
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -288,7 +424,7 @@ function parseArgs(args) {
|
|
|
288
424
|
parsed.names.push(token);
|
|
289
425
|
continue;
|
|
290
426
|
}
|
|
291
|
-
if (!["--out", "--dry-run", "--overwrite"].includes(token))
|
|
427
|
+
if (!["--out", "--dry-run", "--overwrite", "--json"].includes(token))
|
|
292
428
|
throw new CliError(`Unknown option ${JSON.stringify(token)}. Run oprt --help.`, 2);
|
|
293
429
|
if (seen.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
|
|
294
430
|
seen.add(token);
|
|
@@ -298,6 +434,7 @@ function parseArgs(args) {
|
|
|
298
434
|
throw new CliError("--out requires a relative directory.", 2);
|
|
299
435
|
parsed.out = value;
|
|
300
436
|
} else if (token === "--dry-run") parsed.dryRun = true;
|
|
437
|
+
else if (token === "--json") parsed.json = true;
|
|
301
438
|
else parsed.overwrite = true;
|
|
302
439
|
}
|
|
303
440
|
if (!parsed.names.length)
|
|
@@ -306,10 +443,10 @@ function parseArgs(args) {
|
|
|
306
443
|
}
|
|
307
444
|
|
|
308
445
|
// src/installer.ts
|
|
309
|
-
function resolveEntries(
|
|
310
|
-
if (!
|
|
446
|
+
function resolveEntries(names2, registry2) {
|
|
447
|
+
if (!names2.length) throw new CliError("add requires one or more names.", 2);
|
|
311
448
|
const selected = /* @__PURE__ */ new Map();
|
|
312
|
-
for (const name of
|
|
449
|
+
for (const name of names2) {
|
|
313
450
|
const normalized = name.toLowerCase();
|
|
314
451
|
const entry = registry2.entries.find(
|
|
315
452
|
(candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
|
|
@@ -417,202 +554,196 @@ function installSources(options, context) {
|
|
|
417
554
|
return result;
|
|
418
555
|
}
|
|
419
556
|
|
|
420
|
-
// src/
|
|
421
|
-
var
|
|
422
|
-
var import_node_module = require("module");
|
|
423
|
-
var import_node_path3 = require("path");
|
|
424
|
-
var import_node_url2 = require("url");
|
|
557
|
+
// src/identity.ts
|
|
558
|
+
var import_node_crypto = require("crypto");
|
|
425
559
|
|
|
426
|
-
//
|
|
427
|
-
var
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
560
|
+
// package.json
|
|
561
|
+
var package_default = {
|
|
562
|
+
name: "@oprt/cli",
|
|
563
|
+
version: "0.3.0",
|
|
564
|
+
description: "The OPERATOR command line: copy components and recipes into your app as source, give coding agents the contract, and check their code against it. Includes an MCP server.",
|
|
565
|
+
keywords: [
|
|
566
|
+
"design-system",
|
|
567
|
+
"cli",
|
|
568
|
+
"coding-agents",
|
|
569
|
+
"mcp",
|
|
570
|
+
"model-context-protocol",
|
|
571
|
+
"code-check",
|
|
572
|
+
"components"
|
|
573
|
+
],
|
|
574
|
+
homepage: "https://oprt.dev/docs/agents/cli",
|
|
575
|
+
type: "module",
|
|
576
|
+
main: "./dist/index.cjs",
|
|
577
|
+
module: "./dist/index.js",
|
|
578
|
+
types: "./dist/index.d.ts",
|
|
579
|
+
bin: {
|
|
580
|
+
oprt: "./dist/bin.js"
|
|
581
|
+
},
|
|
582
|
+
engines: {
|
|
583
|
+
node: ">=22"
|
|
584
|
+
},
|
|
585
|
+
exports: {
|
|
586
|
+
".": {
|
|
587
|
+
import: {
|
|
588
|
+
types: "./dist/index.d.ts",
|
|
589
|
+
default: "./dist/index.js"
|
|
590
|
+
},
|
|
591
|
+
require: {
|
|
592
|
+
types: "./dist/index.d.cts",
|
|
593
|
+
default: "./dist/index.cjs"
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
"./read": {
|
|
597
|
+
import: {
|
|
598
|
+
types: "./dist/read.d.ts",
|
|
599
|
+
default: "./dist/read.js"
|
|
600
|
+
},
|
|
601
|
+
require: {
|
|
602
|
+
types: "./dist/read.d.cts",
|
|
603
|
+
default: "./dist/read.cjs"
|
|
604
|
+
}
|
|
438
605
|
}
|
|
606
|
+
},
|
|
607
|
+
files: [
|
|
608
|
+
"dist"
|
|
609
|
+
],
|
|
610
|
+
scripts: {
|
|
611
|
+
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
612
|
+
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
613
|
+
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
614
|
+
test: "vitest run",
|
|
615
|
+
typecheck: "tsc --noEmit",
|
|
616
|
+
lint: "eslint src scripts"
|
|
617
|
+
},
|
|
618
|
+
devDependencies: {
|
|
619
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
620
|
+
tsup: "^8.3.0",
|
|
621
|
+
typescript: "^5.6.3",
|
|
622
|
+
vitest: "^2.1.3"
|
|
623
|
+
},
|
|
624
|
+
license: "MIT",
|
|
625
|
+
publishConfig: {
|
|
626
|
+
access: "public"
|
|
439
627
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
function resolveComponent(contract2, wanted) {
|
|
452
|
-
const names = Object.keys(contract2.components);
|
|
453
|
-
const found = names.find((name) => name === wanted) ?? names.find((name) => squash(name) === squash(wanted));
|
|
454
|
-
if (found !== void 0) return found;
|
|
455
|
-
const suggestions = nearest(wanted, names);
|
|
456
|
-
throw new CliError(
|
|
457
|
-
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
458
|
-
2,
|
|
459
|
-
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
function allowedChildren(rules, name) {
|
|
463
|
-
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
464
|
-
if (parent === name) return rule.allowed_children;
|
|
465
|
-
const nested = allowedChildren(rule.children, name);
|
|
466
|
-
if (nested !== void 0) return nested;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/identity.ts
|
|
631
|
+
var VERSION = package_default.version;
|
|
632
|
+
var RESULT_SCHEMA_VERSION = 1;
|
|
633
|
+
var digests = /* @__PURE__ */ new WeakMap();
|
|
634
|
+
function contractDigest(contract2) {
|
|
635
|
+
let digest = digests.get(contract2);
|
|
636
|
+
if (digest === void 0) {
|
|
637
|
+
digest = `sha256:${(0, import_node_crypto.createHash)("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
|
|
638
|
+
digests.set(contract2, digest);
|
|
467
639
|
}
|
|
468
|
-
return
|
|
640
|
+
return digest;
|
|
469
641
|
}
|
|
470
|
-
|
|
471
|
-
(part) => contract2.guidance.components[part]?.part_of === name
|
|
472
|
-
);
|
|
473
|
-
function componentIndex(contract2) {
|
|
474
|
-
return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
475
|
-
name,
|
|
476
|
-
purpose: contract2.guidance.components[name].use_for.join("; "),
|
|
477
|
-
parts: partsOf(contract2, name)
|
|
478
|
-
}));
|
|
479
|
-
}
|
|
480
|
-
function componentDetail(contract2, wanted) {
|
|
481
|
-
const name = resolveComponent(contract2, wanted);
|
|
482
|
-
const component = contract2.components[name];
|
|
483
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
484
|
-
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
485
|
-
const notes = guide.values?.[prop];
|
|
486
|
-
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
487
|
-
});
|
|
488
|
-
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
642
|
+
function identityOf(contract2) {
|
|
489
643
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
495
|
-
parts: partsOf(contract2, name),
|
|
496
|
-
props,
|
|
497
|
-
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
498
|
-
composable_with: component.composable_with ?? [],
|
|
499
|
-
never_inside: component.never_inside ?? [],
|
|
500
|
-
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
501
|
-
example: component.example
|
|
644
|
+
cli_version: VERSION,
|
|
645
|
+
contract_version: contract2.version,
|
|
646
|
+
contract_digest: contractDigest(contract2),
|
|
647
|
+
schema_version: RESULT_SCHEMA_VERSION
|
|
502
648
|
};
|
|
503
649
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
533
|
-
if (detail.composable_with.length > 0)
|
|
534
|
-
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
535
|
-
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
536
|
-
lines.push(
|
|
537
|
-
detail.example.includes("\n") ? `Example:
|
|
538
|
-
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
539
|
-
);
|
|
540
|
-
return `${lines.join("\n")}
|
|
541
|
-
`;
|
|
542
|
-
}
|
|
543
|
-
var STOP = new Set(
|
|
544
|
-
"a an the to of for in on at with and or i we need want that which is are it its my some from by".split(
|
|
545
|
-
" "
|
|
546
|
-
)
|
|
547
|
-
);
|
|
548
|
-
var words = (text2) => (text2.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((word) => !STOP.has(word)).map((word) => word.length > 4 ? word.replace(/(?:ing|ed|es|s)$/, "") : word);
|
|
549
|
-
var matched = (query, text2) => {
|
|
550
|
-
const present = new Set(words(text2));
|
|
551
|
-
return query.filter((word) => present.has(word)).length;
|
|
552
|
-
};
|
|
553
|
-
function search(contract2, registry2, query, limit = 10) {
|
|
554
|
-
const wanted = words(query);
|
|
555
|
-
if (wanted.length === 0) return [];
|
|
556
|
-
const whole = squash(query);
|
|
557
|
-
const scores = /* @__PURE__ */ new Map();
|
|
558
|
-
const add = (name, points, why) => {
|
|
559
|
-
const before = scores.get(name) ?? { score: 0 };
|
|
560
|
-
scores.set(name, {
|
|
561
|
-
score: before.score + points,
|
|
562
|
-
...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
|
|
563
|
-
});
|
|
564
|
-
};
|
|
565
|
-
for (const [name, component] of Object.entries(contract2.components)) {
|
|
566
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
567
|
-
const plain = squash(name);
|
|
568
|
-
if (plain === whole) add(name, 100);
|
|
569
|
-
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
570
|
-
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
571
|
-
for (const phrase of guide.use_for ?? []) {
|
|
572
|
-
const hits = matched(wanted, phrase);
|
|
573
|
-
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
574
|
-
}
|
|
575
|
-
for (const { need, use } of guide.not_for ?? []) {
|
|
576
|
-
const hits = matched(wanted, need);
|
|
577
|
-
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
578
|
-
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
579
|
-
}
|
|
580
|
-
const inDescription = matched(wanted, component.description);
|
|
581
|
-
if (inDescription > 0) add(name, 2 * inDescription);
|
|
582
|
-
}
|
|
583
|
-
const results = [];
|
|
584
|
-
for (const [name, { score, why }] of scores) {
|
|
585
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
586
|
-
results.push({
|
|
587
|
-
kind: "component",
|
|
588
|
-
name,
|
|
589
|
-
purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
|
|
590
|
-
score,
|
|
591
|
-
...why === void 0 ? {} : { why },
|
|
592
|
-
next: `oprt component ${name}`
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
|
|
596
|
-
const recipe = contract2.patterns?.[entry.name];
|
|
597
|
-
const plain = squash(entry.name);
|
|
598
|
-
const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
|
|
599
|
-
const hits = matched(wanted, entry.description);
|
|
600
|
-
const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
|
|
601
|
-
if (score > 0)
|
|
602
|
-
results.push({
|
|
603
|
-
kind: "recipe",
|
|
604
|
-
name: entry.name,
|
|
605
|
-
purpose: firstSentence(entry.description),
|
|
606
|
-
score,
|
|
607
|
-
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
608
|
-
});
|
|
650
|
+
|
|
651
|
+
// src/check.ts
|
|
652
|
+
var import_node_fs3 = require("fs");
|
|
653
|
+
var import_node_path4 = require("path");
|
|
654
|
+
|
|
655
|
+
// src/parser.ts
|
|
656
|
+
var import_node_module = require("module");
|
|
657
|
+
var import_node_path3 = require("path");
|
|
658
|
+
var import_node_url2 = require("url");
|
|
659
|
+
var SUPPORTED_TYPESCRIPT = "TypeScript 5 or 6";
|
|
660
|
+
var INSTALL_TYPESCRIPT = "npm install -D typescript@6";
|
|
661
|
+
function usable(typescript) {
|
|
662
|
+
const ts = typescript;
|
|
663
|
+
if (typeof ts.createSourceFile !== "function" || ts.ScriptKind === void 0)
|
|
664
|
+
return `TypeScript ${ts.version ?? "(unknown version)"} has no JavaScript syntax API (createSourceFile), which oprt reads code with; ${SUPPORTED_TYPESCRIPT} has it`;
|
|
665
|
+
try {
|
|
666
|
+
const probe = ts.createSourceFile(
|
|
667
|
+
"probe.tsx",
|
|
668
|
+
"<a>",
|
|
669
|
+
ts.ScriptTarget?.Latest ?? 99,
|
|
670
|
+
true,
|
|
671
|
+
ts.ScriptKind.TSX
|
|
672
|
+
);
|
|
673
|
+
const diagnostics = probe.parseDiagnostics;
|
|
674
|
+
if (!Array.isArray(diagnostics)) return "it does not report parse errors on a source file";
|
|
675
|
+
if (diagnostics.length === 0) return "it did not report a syntax error in code that has one";
|
|
676
|
+
} catch (error) {
|
|
677
|
+
return `it failed on a probe parse: ${error instanceof Error ? error.message : String(error)}`;
|
|
609
678
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
var packageFolder = (file) => /^(.*[\\/]node_modules[\\/]typescript)[\\/]/.exec(file)?.[1] ?? file;
|
|
682
|
+
function probeTypeScript(cwd, options = {}) {
|
|
683
|
+
const places = [
|
|
684
|
+
{ from: (0, import_node_path3.join)(cwd, "package.json"), source: "project" },
|
|
685
|
+
...options.installer === false ? [] : [{ from: (0, import_node_url2.fileURLToPath)(importMetaUrl), source: "installer" }]
|
|
686
|
+
];
|
|
687
|
+
let unusable;
|
|
688
|
+
for (const { from, source } of places) {
|
|
689
|
+
let file;
|
|
690
|
+
let loaded2;
|
|
691
|
+
try {
|
|
692
|
+
const require2 = (0, import_node_module.createRequire)(from);
|
|
693
|
+
file = require2.resolve("typescript");
|
|
694
|
+
loaded2 = require2("typescript");
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (error.code !== "MODULE_NOT_FOUND")
|
|
697
|
+
unusable ??= `typescript was found but failed to load: ${error instanceof Error ? error.message : String(error)}`;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const typescript = loaded2.default ?? loaded2;
|
|
701
|
+
const problem = usable(typescript);
|
|
702
|
+
if (problem !== null) {
|
|
703
|
+
unusable ??= `${problem} (at ${packageFolder(file)}).`;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
ready: true,
|
|
708
|
+
typescript,
|
|
709
|
+
resolution: {
|
|
710
|
+
name: "typescript",
|
|
711
|
+
version: typescript.version,
|
|
712
|
+
source,
|
|
713
|
+
resolved_from: packageFolder(file)
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (unusable !== void 0)
|
|
718
|
+
return { ready: false, reason: "typescript_unusable", detail: unusable };
|
|
719
|
+
return {
|
|
720
|
+
ready: false,
|
|
721
|
+
reason: "typescript_missing",
|
|
722
|
+
detail: `typescript could not be resolved from ${cwd}${options.installer === false ? "" : " or from beside the installer"}.`
|
|
723
|
+
};
|
|
613
724
|
}
|
|
614
725
|
|
|
615
726
|
// src/check.ts
|
|
727
|
+
var FINDING_CODES = [
|
|
728
|
+
"INVALID_PROP_VALUE",
|
|
729
|
+
"UNKNOWN_EXPORT",
|
|
730
|
+
"DOT_NOTATION",
|
|
731
|
+
"INVALID_NESTING",
|
|
732
|
+
"INVALID_CHILD",
|
|
733
|
+
"MISSING_REQUIRED_PROP",
|
|
734
|
+
"UNKNOWN_PROP",
|
|
735
|
+
"RAW_COLOR",
|
|
736
|
+
"ARBITRARY_SIZE",
|
|
737
|
+
"ROUNDED_CORNER",
|
|
738
|
+
"NATIVE_CONTROL",
|
|
739
|
+
/** The file does not parse. It was not checked against the contract. */
|
|
740
|
+
"SYNTAX_ERROR"
|
|
741
|
+
];
|
|
742
|
+
var LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
|
|
743
|
+
var SCOPE = {
|
|
744
|
+
checked: ["syntax", "contract", "house_rules"],
|
|
745
|
+
not_checked: ["types", "render"]
|
|
746
|
+
};
|
|
616
747
|
var PACKAGE = "@oprt/react";
|
|
617
748
|
var NATIVE = {
|
|
618
749
|
button: "Button",
|
|
@@ -648,11 +779,49 @@ function allowedChildren2(rules, name) {
|
|
|
648
779
|
}
|
|
649
780
|
return void 0;
|
|
650
781
|
}
|
|
782
|
+
var quote = (text2) => {
|
|
783
|
+
const flat = text2.replace(/\s+/g, " ").trim();
|
|
784
|
+
return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
|
|
785
|
+
};
|
|
786
|
+
var MAX_SYNTAX_ERRORS = 10;
|
|
651
787
|
function checkSource(typescript, contract2, file, text2) {
|
|
652
788
|
const ts = typescript;
|
|
653
789
|
const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
|
|
654
790
|
const source = ts.createSourceFile(file, text2, ts.ScriptTarget.Latest, true, kind);
|
|
791
|
+
const parseDiagnostics = source.parseDiagnostics;
|
|
792
|
+
if (!Array.isArray(parseDiagnostics))
|
|
793
|
+
throw new CliError(
|
|
794
|
+
`This TypeScript does not report syntax errors on a parsed file, so a file that does not parse could be called clean. Install TypeScript 5 or 6 in this project (${INSTALL_TYPESCRIPT}).`,
|
|
795
|
+
1,
|
|
796
|
+
{ code: "ERR_TYPESCRIPT_UNUSABLE" }
|
|
797
|
+
);
|
|
798
|
+
if (parseDiagnostics.length > 0) {
|
|
799
|
+
const ordered = [...parseDiagnostics].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
800
|
+
const shown = ordered.slice(0, MAX_SYNTAX_ERRORS);
|
|
801
|
+
return {
|
|
802
|
+
file,
|
|
803
|
+
status: "not_evaluated",
|
|
804
|
+
limitations: [],
|
|
805
|
+
findings: shown.map((diagnostic, index) => {
|
|
806
|
+
const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
807
|
+
const more = ordered.length - shown.length;
|
|
808
|
+
return {
|
|
809
|
+
code: "SYNTAX_ERROR",
|
|
810
|
+
severity: "error",
|
|
811
|
+
file,
|
|
812
|
+
line: line + 1,
|
|
813
|
+
column: character + 1,
|
|
814
|
+
message: `${ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")} (TS${diagnostic.code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
|
|
815
|
+
};
|
|
816
|
+
})
|
|
817
|
+
};
|
|
818
|
+
}
|
|
655
819
|
const findings = [];
|
|
820
|
+
const limitations = [];
|
|
821
|
+
const limit = (node, limitation) => {
|
|
822
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
823
|
+
limitations.push({ ...limitation, file, line: line + 1, column: character + 1 });
|
|
824
|
+
};
|
|
656
825
|
const report = (node, finding) => {
|
|
657
826
|
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
658
827
|
findings.push({
|
|
@@ -677,7 +846,7 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
677
846
|
const from = statement.moduleSpecifier.text;
|
|
678
847
|
const clause = statement.importClause;
|
|
679
848
|
if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
|
|
680
|
-
const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0,
|
|
849
|
+
const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0, import_node_path4.basename)(from));
|
|
681
850
|
if (from !== PACKAGE && !copied) continue;
|
|
682
851
|
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
683
852
|
if (from === PACKAGE) namespaces.add(clause.namedBindings.name.text);
|
|
@@ -723,6 +892,31 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
723
892
|
return [];
|
|
724
893
|
};
|
|
725
894
|
const valueOf = (attribute) => attribute.initializer === void 0 ? void 0 : ts.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
|
|
895
|
+
const optionValue = (expression) => {
|
|
896
|
+
const read = [];
|
|
897
|
+
const unread = [];
|
|
898
|
+
const walk = (node) => {
|
|
899
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
900
|
+
read.push(node.text);
|
|
901
|
+
else if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(node))
|
|
902
|
+
walk(node.expression);
|
|
903
|
+
else if (ts.isConditionalExpression(node)) {
|
|
904
|
+
walk(node.whenTrue);
|
|
905
|
+
walk(node.whenFalse);
|
|
906
|
+
} else if (ts.isBinaryExpression(node)) {
|
|
907
|
+
const operator = node.operatorToken.kind;
|
|
908
|
+
if (operator === ts.SyntaxKind.QuestionQuestionToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
909
|
+
walk(node.left);
|
|
910
|
+
walk(node.right);
|
|
911
|
+
} else if (operator === ts.SyntaxKind.AmpersandAmpersandToken) walk(node.right);
|
|
912
|
+
else unread.push(node);
|
|
913
|
+
} else if (!// No value: the component's default applies.
|
|
914
|
+
(ts.isIdentifier(node) && node.text === "undefined" || node.kind === ts.SyntaxKind.NullKeyword))
|
|
915
|
+
unread.push(node);
|
|
916
|
+
};
|
|
917
|
+
if (expression) walk(expression);
|
|
918
|
+
return { literals: read, unread };
|
|
919
|
+
};
|
|
726
920
|
const classText = (expression) => {
|
|
727
921
|
if (!expression) return "";
|
|
728
922
|
if (ts.isTemplateExpression(expression))
|
|
@@ -795,6 +989,13 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
795
989
|
if (component === null) return null;
|
|
796
990
|
const definition = contract2.components[component];
|
|
797
991
|
const spread = attributes.some((attribute) => ts.isJsxSpreadAttribute(attribute));
|
|
992
|
+
for (const attribute of attributes)
|
|
993
|
+
if (ts.isJsxSpreadAttribute(attribute))
|
|
994
|
+
limit(attribute, {
|
|
995
|
+
code: "UNRESOLVED_SPREAD",
|
|
996
|
+
component,
|
|
997
|
+
message: `Props spread into <${component}> (${quote(attribute.getText(source))}) were not read: option values and required props that arrive through them were not checked.`
|
|
998
|
+
});
|
|
798
999
|
const given = /* @__PURE__ */ new Set();
|
|
799
1000
|
for (const attribute of attributes) {
|
|
800
1001
|
if (!ts.isJsxAttribute(attribute)) continue;
|
|
@@ -803,18 +1004,26 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
803
1004
|
const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
|
|
804
1005
|
if (known === void 0) {
|
|
805
1006
|
if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
|
|
806
|
-
const
|
|
1007
|
+
const names2 = Object.keys(definition.props);
|
|
807
1008
|
report(attribute, {
|
|
808
1009
|
code: "UNKNOWN_PROP",
|
|
809
1010
|
severity: "warning",
|
|
810
1011
|
component,
|
|
811
1012
|
prop,
|
|
812
|
-
message: `${component} has no prop ${prop}.` + (
|
|
1013
|
+
message: `${component} has no prop ${prop}.` + (names2.length === 0 ? " It takes no props of its own." : ` Its props: ${names2.slice(0, 14).join(", ")}${names2.length > 14 ? ", \u2026" : ""}.`)
|
|
813
1014
|
});
|
|
814
1015
|
continue;
|
|
815
1016
|
}
|
|
816
1017
|
if (known.type !== "enum" || known.values === void 0) continue;
|
|
817
|
-
|
|
1018
|
+
const { literals: options, unread } = optionValue(valueOf(attribute));
|
|
1019
|
+
for (const expression of unread)
|
|
1020
|
+
limit(attribute, {
|
|
1021
|
+
code: "UNRESOLVED_VALUE",
|
|
1022
|
+
component,
|
|
1023
|
+
prop,
|
|
1024
|
+
message: `${component} ${prop} is set from an expression (${quote(expression.getText(source))}) whose value is not in the source, so it was not compared with the options.`
|
|
1025
|
+
});
|
|
1026
|
+
for (const literal of options) {
|
|
818
1027
|
if (known.values.includes(literal)) continue;
|
|
819
1028
|
const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
|
|
820
1029
|
report(attribute, {
|
|
@@ -884,64 +1093,153 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
884
1093
|
ts.forEachChild(node, (child) => visit(child, ancestors, parent));
|
|
885
1094
|
};
|
|
886
1095
|
visit(source, [], null);
|
|
887
|
-
|
|
1096
|
+
const byPlace = (a, b) => a.line - b.line || a.column - b.column;
|
|
1097
|
+
return {
|
|
1098
|
+
file,
|
|
1099
|
+
status: limitations.length > 0 ? "partial" : "evaluated",
|
|
1100
|
+
findings: findings.sort(byPlace),
|
|
1101
|
+
limitations: limitations.sort(byPlace)
|
|
1102
|
+
};
|
|
888
1103
|
}
|
|
889
1104
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
890
1105
|
var READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
891
1106
|
var NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
1107
|
+
var excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
|
|
892
1108
|
function* sources(path) {
|
|
893
|
-
|
|
894
|
-
if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
|
|
1109
|
+
if (excluded((0, import_node_path4.basename)(path))) return;
|
|
895
1110
|
if ((0, import_node_fs3.statSync)(path).isDirectory()) {
|
|
896
|
-
for (const entry of (0, import_node_fs3.readdirSync)(path).sort()) yield* sources((0,
|
|
897
|
-
} else if (READ.has((0,
|
|
1111
|
+
for (const entry of (0, import_node_fs3.readdirSync)(path).sort()) yield* sources((0, import_node_path4.join)(path, entry));
|
|
1112
|
+
} else if (READ.has((0, import_node_path4.extname)(path)) && !NOT_INTERFACE.test((0, import_node_path4.basename)(path))) yield path;
|
|
898
1113
|
}
|
|
899
|
-
function
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1114
|
+
function whyNotRead(path) {
|
|
1115
|
+
const name = (0, import_node_path4.basename)(path);
|
|
1116
|
+
if (excluded(name)) return "excluded_folder";
|
|
1117
|
+
if ((0, import_node_fs3.statSync)(path).isDirectory()) return null;
|
|
1118
|
+
if (!READ.has((0, import_node_path4.extname)(path))) return "unsupported_extension";
|
|
1119
|
+
return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
|
|
1120
|
+
}
|
|
1121
|
+
function buildCheckReport(evaluations, context) {
|
|
1122
|
+
const findings = evaluations.flatMap(({ findings: found }) => found);
|
|
1123
|
+
const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
|
|
1124
|
+
const partial = count("partial");
|
|
1125
|
+
const notEvaluated = count("not_evaluated");
|
|
1126
|
+
const evaluated = evaluations.length - notEvaluated;
|
|
1127
|
+
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1128
|
+
const warnings = findings.length - errors;
|
|
1129
|
+
let status;
|
|
1130
|
+
if (evaluations.length === 0) status = "no_files_evaluated";
|
|
1131
|
+
else if (evaluated === 0) status = "not_evaluated";
|
|
1132
|
+
else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
|
|
1133
|
+
else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
|
|
1134
|
+
return {
|
|
1135
|
+
evaluation_status: status,
|
|
1136
|
+
files: evaluations.length,
|
|
1137
|
+
findings,
|
|
1138
|
+
summary: { errors, warnings },
|
|
1139
|
+
coverage: {
|
|
1140
|
+
files_found: evaluations.length,
|
|
1141
|
+
files_evaluated: evaluated,
|
|
1142
|
+
files_partial: partial,
|
|
1143
|
+
files_not_evaluated: notEvaluated
|
|
1144
|
+
},
|
|
1145
|
+
limitations: evaluations.flatMap(({ limitations }) => limitations),
|
|
1146
|
+
skipped_inputs: context.skipped ?? [],
|
|
1147
|
+
scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
|
|
1148
|
+
parser: context.parser
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function checkExitCode(report, { strict }) {
|
|
1152
|
+
if (report.evaluation_status === "no_files_evaluated") return 1;
|
|
1153
|
+
if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
|
|
1154
|
+
return strict && report.summary.warnings > 0 ? 1 : 0;
|
|
1155
|
+
}
|
|
1156
|
+
var describeProvided = (typescript) => ({
|
|
1157
|
+
name: "typescript",
|
|
1158
|
+
version: typescript.version ?? "unknown",
|
|
1159
|
+
source: "provided"
|
|
1160
|
+
});
|
|
1161
|
+
function resolveParser(cwd, provided, who) {
|
|
1162
|
+
if (provided !== void 0 && provided !== null)
|
|
1163
|
+
return { typescript: provided, parser: describeProvided(provided) };
|
|
1164
|
+
const probe = provided === null ? null : probeTypeScript(cwd);
|
|
1165
|
+
if (probe?.ready) {
|
|
1166
|
+
const { name, version, source } = probe.resolution;
|
|
1167
|
+
return { typescript: probe.typescript, parser: { name, version, source } };
|
|
905
1168
|
}
|
|
906
|
-
|
|
1169
|
+
if (probe !== null && probe.reason === "typescript_unusable")
|
|
1170
|
+
throw new CliError(`${who} cannot use the TypeScript it found: ${probe.detail}`, 1, {
|
|
1171
|
+
code: "ERR_TYPESCRIPT_UNUSABLE"
|
|
1172
|
+
});
|
|
1173
|
+
throw new CliError(
|
|
1174
|
+
`${who} reads code with the project's TypeScript, and found none. Install typescript in this project (${INSTALL_TYPESCRIPT}) and run it again. It looked from ${cwd}.`,
|
|
1175
|
+
1,
|
|
1176
|
+
{ code: "ERR_TYPESCRIPT_MISSING" }
|
|
1177
|
+
);
|
|
907
1178
|
}
|
|
908
1179
|
function checkPaths(paths, context) {
|
|
909
|
-
const root = (0,
|
|
1180
|
+
const root = (0, import_node_path4.resolve)(context.cwd);
|
|
910
1181
|
const files = [];
|
|
1182
|
+
const skipped = [];
|
|
911
1183
|
for (const path of paths) {
|
|
912
|
-
const absolute = (0,
|
|
1184
|
+
const absolute = (0, import_node_path4.resolve)(root, path);
|
|
913
1185
|
if (!(0, import_node_fs3.existsSync)(absolute))
|
|
914
1186
|
throw new CliError(`No such file or folder: ${path}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
1187
|
+
const reason = whyNotRead(absolute);
|
|
1188
|
+
if (reason !== null) skipped.push({ path, reason });
|
|
915
1189
|
files.push(...sources(absolute));
|
|
916
1190
|
}
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
throw new CliError(
|
|
920
|
-
"oprt check reads code with the project's TypeScript, and found none. Install typescript in this project (npm install -D typescript) and run it again.",
|
|
921
|
-
1,
|
|
922
|
-
{ code: "ERR_TYPESCRIPT_MISSING" }
|
|
923
|
-
);
|
|
924
|
-
const findings = [...new Set(files)].flatMap(
|
|
1191
|
+
const { typescript, parser } = resolveParser(root, context.typescript, "oprt check");
|
|
1192
|
+
const evaluations = [...new Set(files)].map(
|
|
925
1193
|
(file) => checkSource(
|
|
926
|
-
|
|
1194
|
+
typescript,
|
|
927
1195
|
context.contract,
|
|
928
|
-
(0,
|
|
1196
|
+
(0, import_node_path4.relative)(root, file).split(import_node_path4.sep).join("/"),
|
|
929
1197
|
(0, import_node_fs3.readFileSync)(file, "utf8")
|
|
930
1198
|
)
|
|
931
1199
|
);
|
|
932
|
-
return {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
1200
|
+
return buildCheckReport(evaluations, { parser, skipped });
|
|
1201
|
+
}
|
|
1202
|
+
var plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1203
|
+
var MAX_LIMITS_SHOWN = 20;
|
|
1204
|
+
function renderCheckText(report) {
|
|
1205
|
+
const lines = report.findings.map(
|
|
1206
|
+
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1207
|
+
);
|
|
1208
|
+
const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
|
|
1209
|
+
for (const { file, line, column, code, message } of shown)
|
|
1210
|
+
lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
|
|
1211
|
+
if (report.limitations.length > shown.length)
|
|
1212
|
+
lines.push(
|
|
1213
|
+
` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
|
|
1214
|
+
);
|
|
1215
|
+
const { coverage, evaluation_status: status } = report;
|
|
1216
|
+
if (status === "no_files_evaluated") {
|
|
1217
|
+
lines.push("No .tsx or .jsx file was read, so nothing was checked.");
|
|
1218
|
+
const labels = {
|
|
1219
|
+
unsupported_extension: "not a .tsx or .jsx file",
|
|
1220
|
+
test_or_story_file: "a test, spec or story file, which is skipped",
|
|
1221
|
+
excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
|
|
1222
|
+
};
|
|
1223
|
+
for (const { path, reason } of report.skipped_inputs)
|
|
1224
|
+
lines.push(` ${path}: ${labels[reason]}`);
|
|
1225
|
+
} else if (coverage.files_not_evaluated > 0)
|
|
1226
|
+
lines.push(
|
|
1227
|
+
`${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
|
|
1228
|
+
);
|
|
1229
|
+
else if (status === "evaluated_clean")
|
|
1230
|
+
lines.push(
|
|
1231
|
+
"Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
|
|
1232
|
+
);
|
|
1233
|
+
lines.push(
|
|
1234
|
+
`${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
|
|
1235
|
+
);
|
|
1236
|
+
return `${lines.join("\n")}
|
|
1237
|
+
`;
|
|
940
1238
|
}
|
|
941
1239
|
|
|
942
1240
|
// src/doctor.ts
|
|
943
1241
|
var import_node_fs4 = require("fs");
|
|
944
|
-
var
|
|
1242
|
+
var import_node_path5 = require("path");
|
|
945
1243
|
|
|
946
1244
|
// src/agent-block.ts
|
|
947
1245
|
var START = "<!-- OPERATOR:START -->";
|
|
@@ -1025,14 +1323,18 @@ var INSTRUCTION_FILES = [
|
|
|
1025
1323
|
];
|
|
1026
1324
|
var minor = (version) => version.split(".").slice(0, 2).join(".");
|
|
1027
1325
|
function installedVersion(cwd, name) {
|
|
1028
|
-
for (let folder = cwd; ; folder = (0,
|
|
1029
|
-
const manifest = (0,
|
|
1326
|
+
for (let folder = cwd; ; folder = (0, import_node_path5.dirname)(folder)) {
|
|
1327
|
+
const manifest = (0, import_node_path5.join)(folder, "node_modules", name, "package.json");
|
|
1030
1328
|
if ((0, import_node_fs4.existsSync)(manifest))
|
|
1031
1329
|
return JSON.parse((0, import_node_fs4.readFileSync)(manifest, "utf8")).version;
|
|
1032
|
-
if ((0,
|
|
1330
|
+
if ((0, import_node_path5.dirname)(folder) === folder) return void 0;
|
|
1033
1331
|
}
|
|
1034
1332
|
}
|
|
1035
|
-
function doctor({
|
|
1333
|
+
function doctor({
|
|
1334
|
+
cwd,
|
|
1335
|
+
contract: contract2,
|
|
1336
|
+
parser = probeTypeScript(cwd)
|
|
1337
|
+
}) {
|
|
1036
1338
|
const checks = [];
|
|
1037
1339
|
for (const name of PACKAGES) {
|
|
1038
1340
|
const version = installedVersion(cwd, name);
|
|
@@ -1054,7 +1356,7 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1054
1356
|
else checks.push({ id, status: "ok", message: `${name} ${version} is installed.` });
|
|
1055
1357
|
}
|
|
1056
1358
|
const written = INSTRUCTION_FILES.map((path) => {
|
|
1057
|
-
const file = (0,
|
|
1359
|
+
const file = (0, import_node_path5.join)(cwd, path);
|
|
1058
1360
|
return {
|
|
1059
1361
|
path,
|
|
1060
1362
|
version: (0, import_node_fs4.existsSync)(file) ? blockVersion((0, import_node_fs4.readFileSync)(file, "utf8")) : void 0
|
|
@@ -1081,13 +1383,60 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1081
1383
|
status: "ok",
|
|
1082
1384
|
message: `${written.map(({ path }) => path).join(", ")} carries the OPERATOR block (${contract2.version}).`
|
|
1083
1385
|
});
|
|
1386
|
+
const install2 = `${INSTALL_TYPESCRIPT} (or your package manager's equivalent), then run oprt doctor again. Lookups work without it.`;
|
|
1387
|
+
let checkCode;
|
|
1388
|
+
if (parser.ready) {
|
|
1389
|
+
const { version, resolved_from, source } = parser.resolution;
|
|
1390
|
+
checkCode = {
|
|
1391
|
+
status: "ready",
|
|
1392
|
+
message: `TypeScript ${version}, resolved from ${resolved_from}${source === "project" ? "" : " (beside oprt, not this project)"}.`,
|
|
1393
|
+
typescript: { version, resolved_from, source }
|
|
1394
|
+
};
|
|
1395
|
+
checks.push(
|
|
1396
|
+
source === "project" ? {
|
|
1397
|
+
id: "typescript",
|
|
1398
|
+
status: "ok",
|
|
1399
|
+
message: `TypeScript ${version} is installed in this project (${resolved_from}).`
|
|
1400
|
+
} : {
|
|
1401
|
+
id: "typescript",
|
|
1402
|
+
status: "warn",
|
|
1403
|
+
message: `TypeScript ${version} is not in this project; the one beside oprt is used (${resolved_from}).`,
|
|
1404
|
+
fix: `${INSTALL_TYPESCRIPT}, so checking reads code with a version installed in the project.`
|
|
1405
|
+
}
|
|
1406
|
+
);
|
|
1407
|
+
} else {
|
|
1408
|
+
checkCode = {
|
|
1409
|
+
status: "blocked",
|
|
1410
|
+
message: `${parser.reason}: ${parser.detail}`,
|
|
1411
|
+
reason: parser.reason,
|
|
1412
|
+
fix: install2
|
|
1413
|
+
};
|
|
1414
|
+
checks.push({
|
|
1415
|
+
id: "typescript",
|
|
1416
|
+
status: "fail",
|
|
1417
|
+
message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
|
|
1418
|
+
fix: install2
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1084
1421
|
const count = (status) => checks.filter((check) => check.status === status).length;
|
|
1085
|
-
return {
|
|
1422
|
+
return {
|
|
1423
|
+
consumer_directory: cwd,
|
|
1424
|
+
operations: {
|
|
1425
|
+
lookup: {
|
|
1426
|
+
status: "ready",
|
|
1427
|
+
message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
|
|
1428
|
+
contract_version: contract2.version
|
|
1429
|
+
},
|
|
1430
|
+
check_code: checkCode
|
|
1431
|
+
},
|
|
1432
|
+
checks,
|
|
1433
|
+
summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
|
|
1434
|
+
};
|
|
1086
1435
|
}
|
|
1087
1436
|
|
|
1088
1437
|
// src/init.ts
|
|
1089
1438
|
var import_node_fs5 = require("fs");
|
|
1090
|
-
var
|
|
1439
|
+
var import_node_path6 = require("path");
|
|
1091
1440
|
var AGENT_FILES = {
|
|
1092
1441
|
claude: ["CLAUDE.md"],
|
|
1093
1442
|
cursor: [".cursor/rules/operator.mdc"],
|
|
@@ -1115,7 +1464,7 @@ function init(options, context) {
|
|
|
1115
1464
|
const dryRun = options.dryRun ?? false;
|
|
1116
1465
|
const files = targets(options).map((path) => {
|
|
1117
1466
|
validateRelative(path);
|
|
1118
|
-
const destination = (0,
|
|
1467
|
+
const destination = (0, import_node_path6.resolve)(root, path);
|
|
1119
1468
|
inspectPath(root, destination);
|
|
1120
1469
|
const existing = (0, import_node_fs5.existsSync)(destination) ? (0, import_node_fs5.readFileSync)(destination, "utf8") : null;
|
|
1121
1470
|
if (options.remove) {
|
|
@@ -1132,7 +1481,7 @@ function init(options, context) {
|
|
|
1132
1481
|
const fresh = existing === null && path.endsWith(".mdc") ? CURSOR_FRONT_MATTER : existing;
|
|
1133
1482
|
const { text: text2, action } = applyBlock(fresh, block);
|
|
1134
1483
|
if (action !== "unchanged" && !dryRun) {
|
|
1135
|
-
(0, import_node_fs5.mkdirSync)((0,
|
|
1484
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(destination), { recursive: true });
|
|
1136
1485
|
(0, import_node_fs5.writeFileSync)(destination, text2);
|
|
1137
1486
|
}
|
|
1138
1487
|
return { path, action: existing === null ? "create" : action };
|
|
@@ -1140,8 +1489,536 @@ function init(options, context) {
|
|
|
1140
1489
|
return { version: context.contract.version, dryRun, files };
|
|
1141
1490
|
}
|
|
1142
1491
|
|
|
1492
|
+
// src/results.ts
|
|
1493
|
+
var RESULT_TYPES = [
|
|
1494
|
+
"check",
|
|
1495
|
+
"search",
|
|
1496
|
+
"component.detail",
|
|
1497
|
+
"component.batch",
|
|
1498
|
+
"component.list",
|
|
1499
|
+
"init",
|
|
1500
|
+
"doctor",
|
|
1501
|
+
"add",
|
|
1502
|
+
"list",
|
|
1503
|
+
"instructions"
|
|
1504
|
+
];
|
|
1505
|
+
var envelope = (type, data, contract2) => ({
|
|
1506
|
+
apiVersion: 1,
|
|
1507
|
+
type,
|
|
1508
|
+
identity: identityOf(contract2),
|
|
1509
|
+
data
|
|
1510
|
+
});
|
|
1511
|
+
function errorEnvelope(error, contract2) {
|
|
1512
|
+
const known = error instanceof CliError ? error : void 0;
|
|
1513
|
+
const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
|
|
1514
|
+
cli_version,
|
|
1515
|
+
schema_version,
|
|
1516
|
+
contract_version,
|
|
1517
|
+
contract_digest
|
|
1518
|
+
}))(identityOf(contract2));
|
|
1519
|
+
return {
|
|
1520
|
+
apiVersion: 1,
|
|
1521
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1522
|
+
code: known?.code ?? "ERR_FAILED",
|
|
1523
|
+
...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
|
|
1524
|
+
identity
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
function componentData(detail, dense) {
|
|
1528
|
+
if (!dense) return { ...detail, dense };
|
|
1529
|
+
const { description: _description, ...rest } = detail;
|
|
1530
|
+
return {
|
|
1531
|
+
...rest,
|
|
1532
|
+
props: detail.props.map(
|
|
1533
|
+
(prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
|
|
1534
|
+
),
|
|
1535
|
+
dense
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function instructionsData(contract2) {
|
|
1539
|
+
return {
|
|
1540
|
+
rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
|
|
1541
|
+
id,
|
|
1542
|
+
rule,
|
|
1543
|
+
...wrong === void 0 ? {} : { wrong },
|
|
1544
|
+
...right === void 0 ? {} : { right }
|
|
1545
|
+
})),
|
|
1546
|
+
components: componentIndex(contract2),
|
|
1547
|
+
recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
|
|
1548
|
+
name,
|
|
1549
|
+
description,
|
|
1550
|
+
install: install2
|
|
1551
|
+
}))
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
var str = (extra = {}) => ({ type: "string", ...extra });
|
|
1555
|
+
var int = () => ({ type: "integer", minimum: 0 });
|
|
1556
|
+
var bool = () => ({ type: "boolean" });
|
|
1557
|
+
var arr = (items) => ({ type: "array", items });
|
|
1558
|
+
var names = () => arr(str());
|
|
1559
|
+
var words2 = (...values) => ({ type: "string", enum: [...values] });
|
|
1560
|
+
var ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
1561
|
+
var obj = (properties, optional = [], open = false) => ({
|
|
1562
|
+
type: "object",
|
|
1563
|
+
properties,
|
|
1564
|
+
required: Object.keys(properties).filter((key) => !optional.includes(key)),
|
|
1565
|
+
additionalProperties: open
|
|
1566
|
+
});
|
|
1567
|
+
var scalar = { type: ["string", "number", "boolean"] };
|
|
1568
|
+
var DEFS = {
|
|
1569
|
+
identity: obj({
|
|
1570
|
+
cli_version: str(),
|
|
1571
|
+
contract_version: str(),
|
|
1572
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
|
|
1573
|
+
schema_version: { type: "integer", minimum: 1 }
|
|
1574
|
+
}),
|
|
1575
|
+
error_identity: obj(
|
|
1576
|
+
{
|
|
1577
|
+
cli_version: str(),
|
|
1578
|
+
schema_version: { type: "integer", minimum: 1 },
|
|
1579
|
+
contract_version: str(),
|
|
1580
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
|
|
1581
|
+
},
|
|
1582
|
+
["contract_version", "contract_digest"]
|
|
1583
|
+
),
|
|
1584
|
+
finding: obj(
|
|
1585
|
+
{
|
|
1586
|
+
code: words2(...FINDING_CODES),
|
|
1587
|
+
severity: words2("error", "warning"),
|
|
1588
|
+
file: str(),
|
|
1589
|
+
line: { type: "integer", minimum: 1 },
|
|
1590
|
+
column: { type: "integer", minimum: 1 },
|
|
1591
|
+
component: str(),
|
|
1592
|
+
prop: str(),
|
|
1593
|
+
message: str(),
|
|
1594
|
+
valid_values: names(),
|
|
1595
|
+
suggestions: names()
|
|
1596
|
+
},
|
|
1597
|
+
["component", "prop", "valid_values", "suggestions"]
|
|
1598
|
+
),
|
|
1599
|
+
limitation: obj(
|
|
1600
|
+
{
|
|
1601
|
+
code: words2(...LIMITATION_CODES),
|
|
1602
|
+
file: str(),
|
|
1603
|
+
line: { type: "integer", minimum: 1 },
|
|
1604
|
+
column: { type: "integer", minimum: 1 },
|
|
1605
|
+
component: str(),
|
|
1606
|
+
prop: str(),
|
|
1607
|
+
message: str()
|
|
1608
|
+
},
|
|
1609
|
+
["prop"]
|
|
1610
|
+
),
|
|
1611
|
+
skipped_input: obj({
|
|
1612
|
+
path: str(),
|
|
1613
|
+
reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
|
|
1614
|
+
}),
|
|
1615
|
+
parser: obj({
|
|
1616
|
+
name: words2("typescript"),
|
|
1617
|
+
version: str(),
|
|
1618
|
+
source: words2("project", "installer", "provided")
|
|
1619
|
+
}),
|
|
1620
|
+
check_data: obj({
|
|
1621
|
+
evaluation_status: words2(
|
|
1622
|
+
"evaluated_clean",
|
|
1623
|
+
"evaluated_with_findings",
|
|
1624
|
+
"partially_evaluated",
|
|
1625
|
+
"not_evaluated",
|
|
1626
|
+
"no_files_evaluated"
|
|
1627
|
+
),
|
|
1628
|
+
files: int(),
|
|
1629
|
+
findings: arr(ref("finding")),
|
|
1630
|
+
summary: obj({ errors: int(), warnings: int() }),
|
|
1631
|
+
coverage: obj({
|
|
1632
|
+
files_found: int(),
|
|
1633
|
+
files_evaluated: int(),
|
|
1634
|
+
files_partial: int(),
|
|
1635
|
+
files_not_evaluated: int()
|
|
1636
|
+
}),
|
|
1637
|
+
limitations: arr(ref("limitation")),
|
|
1638
|
+
skipped_inputs: arr(ref("skipped_input")),
|
|
1639
|
+
scope: obj({ checked: names(), not_checked: names() }),
|
|
1640
|
+
parser: ref("parser")
|
|
1641
|
+
}),
|
|
1642
|
+
search_result: obj(
|
|
1643
|
+
{
|
|
1644
|
+
kind: words2("component", "recipe"),
|
|
1645
|
+
name: str(),
|
|
1646
|
+
purpose: str(),
|
|
1647
|
+
score: { type: "number" },
|
|
1648
|
+
why: str(),
|
|
1649
|
+
next: str()
|
|
1650
|
+
},
|
|
1651
|
+
["why"]
|
|
1652
|
+
),
|
|
1653
|
+
search_data: obj({
|
|
1654
|
+
query: str(),
|
|
1655
|
+
limit: { type: "integer", minimum: 1 },
|
|
1656
|
+
matchCount: int(),
|
|
1657
|
+
results: arr(ref("search_result"))
|
|
1658
|
+
}),
|
|
1659
|
+
index_entry: obj({ name: str(), purpose: str(), parts: names() }),
|
|
1660
|
+
component_list_data: obj({ components: arr(ref("index_entry")) }),
|
|
1661
|
+
detail_prop: obj(
|
|
1662
|
+
{
|
|
1663
|
+
name: str(),
|
|
1664
|
+
type: str(),
|
|
1665
|
+
values: names(),
|
|
1666
|
+
types: names(),
|
|
1667
|
+
default: scalar,
|
|
1668
|
+
required: bool(),
|
|
1669
|
+
description: str(),
|
|
1670
|
+
notes: { type: "object", additionalProperties: str() }
|
|
1671
|
+
},
|
|
1672
|
+
["values", "types", "default", "required", "description", "notes"],
|
|
1673
|
+
// The contract may describe a prop with more than these (an array's item type, for one).
|
|
1674
|
+
true
|
|
1675
|
+
),
|
|
1676
|
+
component_detail: obj(
|
|
1677
|
+
{
|
|
1678
|
+
name: str(),
|
|
1679
|
+
description: str(),
|
|
1680
|
+
use_for: names(),
|
|
1681
|
+
not_for: arr(obj({ need: str(), use: str() })),
|
|
1682
|
+
part_of: str(),
|
|
1683
|
+
parts: names(),
|
|
1684
|
+
props: arr(ref("detail_prop")),
|
|
1685
|
+
slots: {
|
|
1686
|
+
type: "object",
|
|
1687
|
+
additionalProperties: obj({ type: str(), required: bool() }, ["required"])
|
|
1688
|
+
},
|
|
1689
|
+
composable_with: names(),
|
|
1690
|
+
never_inside: names(),
|
|
1691
|
+
allowed_children: names(),
|
|
1692
|
+
example: str(),
|
|
1693
|
+
dense: bool()
|
|
1694
|
+
},
|
|
1695
|
+
["description", "part_of", "slots", "allowed_children"]
|
|
1696
|
+
),
|
|
1697
|
+
component_batch_data: obj({ components: arr(ref("component_detail")) }),
|
|
1698
|
+
doctor_check: obj(
|
|
1699
|
+
{ id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
|
|
1700
|
+
["fix"]
|
|
1701
|
+
),
|
|
1702
|
+
doctor_data: obj({
|
|
1703
|
+
consumer_directory: str(),
|
|
1704
|
+
operations: obj({
|
|
1705
|
+
lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
|
|
1706
|
+
check_code: obj(
|
|
1707
|
+
{
|
|
1708
|
+
status: words2("ready", "blocked"),
|
|
1709
|
+
message: str(),
|
|
1710
|
+
reason: words2("typescript_missing", "typescript_unusable"),
|
|
1711
|
+
fix: str(),
|
|
1712
|
+
typescript: obj({
|
|
1713
|
+
version: str(),
|
|
1714
|
+
resolved_from: str(),
|
|
1715
|
+
source: words2("project", "installer", "provided")
|
|
1716
|
+
})
|
|
1717
|
+
},
|
|
1718
|
+
["reason", "fix", "typescript"]
|
|
1719
|
+
)
|
|
1720
|
+
}),
|
|
1721
|
+
checks: arr(ref("doctor_check")),
|
|
1722
|
+
summary: obj({ ok: int(), warn: int(), fail: int() })
|
|
1723
|
+
}),
|
|
1724
|
+
init_data: obj({
|
|
1725
|
+
version: str(),
|
|
1726
|
+
dryRun: bool(),
|
|
1727
|
+
files: arr(
|
|
1728
|
+
obj({
|
|
1729
|
+
path: str(),
|
|
1730
|
+
action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
|
|
1731
|
+
})
|
|
1732
|
+
)
|
|
1733
|
+
}),
|
|
1734
|
+
install_file: obj({
|
|
1735
|
+
path: str(),
|
|
1736
|
+
destination: str(),
|
|
1737
|
+
action: words2("create", "overwrite", "skip")
|
|
1738
|
+
}),
|
|
1739
|
+
add_data: obj({
|
|
1740
|
+
outputDirectory: str(),
|
|
1741
|
+
entries: names(),
|
|
1742
|
+
dependencies: names(),
|
|
1743
|
+
devDependencies: names(),
|
|
1744
|
+
ranges: { type: "object", additionalProperties: str() },
|
|
1745
|
+
files: arr(ref("install_file")),
|
|
1746
|
+
dryRun: bool(),
|
|
1747
|
+
setup: ref("setup")
|
|
1748
|
+
}),
|
|
1749
|
+
setup: obj({
|
|
1750
|
+
tailwind: str(),
|
|
1751
|
+
packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
|
|
1752
|
+
stylesheet: arr(obj({ line: str(), why: str() })),
|
|
1753
|
+
root_import: obj({ line: str(), note: str() }),
|
|
1754
|
+
not_needed: arr(obj({ name: str(), why: str() }))
|
|
1755
|
+
}),
|
|
1756
|
+
list_data: obj({
|
|
1757
|
+
version: str(),
|
|
1758
|
+
entries: arr(
|
|
1759
|
+
obj(
|
|
1760
|
+
{
|
|
1761
|
+
name: str(),
|
|
1762
|
+
slug: str(),
|
|
1763
|
+
kind: words2("component", "recipe"),
|
|
1764
|
+
description: str(),
|
|
1765
|
+
files: names(),
|
|
1766
|
+
dependencies: names(),
|
|
1767
|
+
devDependencies: names()
|
|
1768
|
+
},
|
|
1769
|
+
["devDependencies"]
|
|
1770
|
+
)
|
|
1771
|
+
),
|
|
1772
|
+
ranges: { type: "object", additionalProperties: str() }
|
|
1773
|
+
}),
|
|
1774
|
+
instructions_data: obj({
|
|
1775
|
+
rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
|
|
1776
|
+
components: arr(ref("index_entry")),
|
|
1777
|
+
recipes: arr(obj({ name: str(), description: str(), install: str() }))
|
|
1778
|
+
})
|
|
1779
|
+
};
|
|
1780
|
+
var DATA = {
|
|
1781
|
+
check: "check_data",
|
|
1782
|
+
search: "search_data",
|
|
1783
|
+
"component.detail": "component_detail",
|
|
1784
|
+
"component.batch": "component_batch_data",
|
|
1785
|
+
"component.list": "component_list_data",
|
|
1786
|
+
init: "init_data",
|
|
1787
|
+
doctor: "doctor_data",
|
|
1788
|
+
add: "add_data",
|
|
1789
|
+
list: "list_data",
|
|
1790
|
+
instructions: "instructions_data"
|
|
1791
|
+
};
|
|
1792
|
+
var success = (type) => obj({
|
|
1793
|
+
apiVersion: { const: 1 },
|
|
1794
|
+
type: { const: type },
|
|
1795
|
+
identity: ref("identity"),
|
|
1796
|
+
data: ref(DATA[type])
|
|
1797
|
+
});
|
|
1798
|
+
var failure = () => obj(
|
|
1799
|
+
{
|
|
1800
|
+
apiVersion: { const: 1 },
|
|
1801
|
+
error: str(),
|
|
1802
|
+
code: str({ pattern: "^ERR_[A-Z_]+$" }),
|
|
1803
|
+
suggestions: names(),
|
|
1804
|
+
identity: ref("error_identity")
|
|
1805
|
+
},
|
|
1806
|
+
["suggestions"]
|
|
1807
|
+
);
|
|
1808
|
+
function bundle(root) {
|
|
1809
|
+
const used = {};
|
|
1810
|
+
const visit = (node) => {
|
|
1811
|
+
if (Array.isArray(node)) node.forEach(visit);
|
|
1812
|
+
else if (typeof node === "object" && node !== null) {
|
|
1813
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1814
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1815
|
+
const name = value.replace("#/$defs/", "");
|
|
1816
|
+
if (used[name] === void 0) {
|
|
1817
|
+
used[name] = DEFS[name];
|
|
1818
|
+
visit(DEFS[name]);
|
|
1819
|
+
}
|
|
1820
|
+
} else visit(value);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
visit(root);
|
|
1825
|
+
return { ...root, $defs: used };
|
|
1826
|
+
}
|
|
1827
|
+
function schemaFor(type) {
|
|
1828
|
+
return bundle(type === "error" ? failure() : success(type));
|
|
1829
|
+
}
|
|
1830
|
+
var S = { type: "string" };
|
|
1831
|
+
var I = { type: "integer" };
|
|
1832
|
+
var B = { type: "boolean" };
|
|
1833
|
+
var list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
|
|
1834
|
+
var shape = (properties, required) => ({
|
|
1835
|
+
type: "object",
|
|
1836
|
+
properties,
|
|
1837
|
+
...required === void 0 ? {} : { required }
|
|
1838
|
+
});
|
|
1839
|
+
var strings = list(S);
|
|
1840
|
+
var SUMMARIES = {
|
|
1841
|
+
check: shape(
|
|
1842
|
+
{
|
|
1843
|
+
evaluation_status: words2(
|
|
1844
|
+
"evaluated_clean",
|
|
1845
|
+
"evaluated_with_findings",
|
|
1846
|
+
"partially_evaluated",
|
|
1847
|
+
"not_evaluated",
|
|
1848
|
+
"no_files_evaluated"
|
|
1849
|
+
),
|
|
1850
|
+
findings: list(
|
|
1851
|
+
shape({
|
|
1852
|
+
code: S,
|
|
1853
|
+
severity: words2("error", "warning"),
|
|
1854
|
+
file: S,
|
|
1855
|
+
line: I,
|
|
1856
|
+
column: I,
|
|
1857
|
+
message: S
|
|
1858
|
+
})
|
|
1859
|
+
),
|
|
1860
|
+
summary: shape({ errors: I, warnings: I }),
|
|
1861
|
+
coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
|
|
1862
|
+
limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
|
|
1863
|
+
skipped_inputs: list(),
|
|
1864
|
+
scope: { type: "object" },
|
|
1865
|
+
parser: { type: "object" }
|
|
1866
|
+
},
|
|
1867
|
+
["evaluation_status", "findings", "summary", "coverage", "limitations"]
|
|
1868
|
+
),
|
|
1869
|
+
search: shape(
|
|
1870
|
+
{
|
|
1871
|
+
matchCount: I,
|
|
1872
|
+
results: list(
|
|
1873
|
+
shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
|
|
1874
|
+
)
|
|
1875
|
+
},
|
|
1876
|
+
["results"]
|
|
1877
|
+
),
|
|
1878
|
+
"component.detail": shape(
|
|
1879
|
+
{
|
|
1880
|
+
name: S,
|
|
1881
|
+
description: S,
|
|
1882
|
+
use_for: strings,
|
|
1883
|
+
not_for: list(),
|
|
1884
|
+
part_of: S,
|
|
1885
|
+
parts: strings,
|
|
1886
|
+
props: list(
|
|
1887
|
+
shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
|
|
1888
|
+
),
|
|
1889
|
+
slots: { type: "object" },
|
|
1890
|
+
composable_with: strings,
|
|
1891
|
+
never_inside: strings,
|
|
1892
|
+
allowed_children: strings,
|
|
1893
|
+
example: S,
|
|
1894
|
+
dense: B
|
|
1895
|
+
},
|
|
1896
|
+
["name", "props", "example", "dense"]
|
|
1897
|
+
),
|
|
1898
|
+
"component.list": shape({ components: list() }, ["components"]),
|
|
1899
|
+
"component.batch": shape({ components: list() }, ["components"]),
|
|
1900
|
+
instructions: shape(
|
|
1901
|
+
{
|
|
1902
|
+
rules: list(),
|
|
1903
|
+
components: list(),
|
|
1904
|
+
recipes: list()
|
|
1905
|
+
},
|
|
1906
|
+
["rules", "components", "recipes"]
|
|
1907
|
+
),
|
|
1908
|
+
init: shape({ files: list() }),
|
|
1909
|
+
doctor: shape({ operations: { type: "object" }, checks: list() }),
|
|
1910
|
+
add: shape({ files: list(), setup: { type: "object" } }),
|
|
1911
|
+
list: shape({ entries: list() })
|
|
1912
|
+
};
|
|
1913
|
+
function shareRepeats(schema) {
|
|
1914
|
+
const count = /* @__PURE__ */ new Map();
|
|
1915
|
+
const tally = (node) => {
|
|
1916
|
+
if (Array.isArray(node)) node.forEach(tally);
|
|
1917
|
+
else if (typeof node === "object" && node !== null) {
|
|
1918
|
+
const text2 = JSON.stringify(node);
|
|
1919
|
+
if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
|
|
1920
|
+
Object.values(node).forEach(tally);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
tally(schema);
|
|
1924
|
+
const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
|
|
1925
|
+
if (repeated.length === 0) return schema;
|
|
1926
|
+
const names2 = new Map(
|
|
1927
|
+
repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
|
|
1928
|
+
);
|
|
1929
|
+
const defs = {};
|
|
1930
|
+
const rewrite = (node) => {
|
|
1931
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
1932
|
+
if (typeof node !== "object" || node === null) return node;
|
|
1933
|
+
const name = names2.get(JSON.stringify(node));
|
|
1934
|
+
if (name !== void 0) {
|
|
1935
|
+
defs[name] = node;
|
|
1936
|
+
return { $ref: `#/$defs/${name}` };
|
|
1937
|
+
}
|
|
1938
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
|
|
1939
|
+
};
|
|
1940
|
+
const rewritten = rewrite(schema);
|
|
1941
|
+
return { ...rewritten, $defs: defs };
|
|
1942
|
+
}
|
|
1943
|
+
function outputSchemaFor(type) {
|
|
1944
|
+
return shareRepeats({
|
|
1945
|
+
type: "object",
|
|
1946
|
+
properties: {
|
|
1947
|
+
type: { const: type },
|
|
1948
|
+
// The four versions and the digest; the docs name them, and so does every answer.
|
|
1949
|
+
identity: { type: "object" },
|
|
1950
|
+
data: SUMMARIES[type]
|
|
1951
|
+
},
|
|
1952
|
+
required: ["identity"],
|
|
1953
|
+
anyOf: [{ required: ["data"] }, { required: ["error"] }]
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
var RESULT_SCHEMAS = Object.freeze(
|
|
1957
|
+
Object.fromEntries(
|
|
1958
|
+
[...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
|
|
1959
|
+
)
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
// src/setup.ts
|
|
1963
|
+
function setupFor(dependencies, contract2) {
|
|
1964
|
+
const { setup } = contract2.guidance;
|
|
1965
|
+
const scansPackage = dependencies.includes(setup.package_source.when);
|
|
1966
|
+
return {
|
|
1967
|
+
tailwind: setup.tailwind,
|
|
1968
|
+
packages: setup.packages.map(({ name, range, dev, why }) => ({
|
|
1969
|
+
name,
|
|
1970
|
+
range: range ?? "latest",
|
|
1971
|
+
dev,
|
|
1972
|
+
why
|
|
1973
|
+
})),
|
|
1974
|
+
stylesheet: [
|
|
1975
|
+
...setup.stylesheet,
|
|
1976
|
+
...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
|
|
1977
|
+
],
|
|
1978
|
+
root_import: { line: setup.root_import, note: setup.root_import_note },
|
|
1979
|
+
not_needed: setup.not_needed
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function renderSetup(setup, copiedPackageImports) {
|
|
1983
|
+
const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
|
|
1984
|
+
const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
|
|
1985
|
+
const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
|
|
1986
|
+
return [
|
|
1987
|
+
`Setup, once per project: ${setup.tailwind}`,
|
|
1988
|
+
` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
|
|
1989
|
+
" In the stylesheet that imports tailwindcss, after the import:",
|
|
1990
|
+
...setup.stylesheet.map(({ line }) => ` ${line}`),
|
|
1991
|
+
...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
|
|
1992
|
+
" (source copied into the project is scanned by Tailwind 4 without an @source line)"
|
|
1993
|
+
],
|
|
1994
|
+
` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
|
|
1995
|
+
...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
|
|
1996
|
+
];
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1143
1999
|
// src/agent-commands.ts
|
|
1144
|
-
|
|
2000
|
+
function renderInstall(result, setup) {
|
|
2001
|
+
const lines = [
|
|
2002
|
+
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
|
|
2003
|
+
`Output: ${result.outputDirectory}`,
|
|
2004
|
+
...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
|
|
2005
|
+
];
|
|
2006
|
+
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
2007
|
+
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
2008
|
+
lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
|
|
2009
|
+
if (result.devDependencies.length > 0)
|
|
2010
|
+
lines.push(
|
|
2011
|
+
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
|
|
2012
|
+
);
|
|
2013
|
+
lines.push(
|
|
2014
|
+
"",
|
|
2015
|
+
...renderSetup(setup, setup.stylesheet.length > 1),
|
|
2016
|
+
"",
|
|
2017
|
+
"Install what is missing yourself. No package configuration or stylesheet was changed."
|
|
2018
|
+
);
|
|
2019
|
+
return `${lines.join("\n")}
|
|
2020
|
+
`;
|
|
2021
|
+
}
|
|
1145
2022
|
function runAgentCommand(command, context) {
|
|
1146
2023
|
const { contract: contract2, registry: registry2, cwd } = context;
|
|
1147
2024
|
switch (command.command) {
|
|
@@ -1153,14 +2030,15 @@ function runAgentCommand(command, context) {
|
|
|
1153
2030
|
({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
|
|
1154
2031
|
`
|
|
1155
2032
|
).join(""),
|
|
1156
|
-
json: envelope("component.list", { components }),
|
|
2033
|
+
json: envelope("component.list", { components }, contract2),
|
|
1157
2034
|
exitCode: 0
|
|
1158
2035
|
};
|
|
1159
2036
|
}
|
|
1160
2037
|
const details = command.names.map((name) => componentDetail(contract2, name));
|
|
2038
|
+
const data = details.map((detail) => componentData(detail, command.dense));
|
|
1161
2039
|
return {
|
|
1162
2040
|
text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
|
|
1163
|
-
json:
|
|
2041
|
+
json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
|
|
1164
2042
|
exitCode: 0
|
|
1165
2043
|
};
|
|
1166
2044
|
}
|
|
@@ -1173,7 +2051,11 @@ function runAgentCommand(command, context) {
|
|
|
1173
2051
|
${next}
|
|
1174
2052
|
`
|
|
1175
2053
|
).join(""),
|
|
1176
|
-
json: envelope(
|
|
2054
|
+
json: envelope(
|
|
2055
|
+
"search",
|
|
2056
|
+
{ query: command.query, limit: command.limit, matchCount: results.length, results },
|
|
2057
|
+
contract2
|
|
2058
|
+
),
|
|
1177
2059
|
exitCode: 0
|
|
1178
2060
|
};
|
|
1179
2061
|
}
|
|
@@ -1184,146 +2066,328 @@ function runAgentCommand(command, context) {
|
|
|
1184
2066
|
...result.files.map(({ path, action }) => ` ${action.padEnd(9)} ${path}`),
|
|
1185
2067
|
command.remove ? "The block was taken out; the rest of each file is as it was." : "Run oprt init again after an upgrade to refresh the block."
|
|
1186
2068
|
];
|
|
1187
|
-
return {
|
|
1188
|
-
|
|
2069
|
+
return {
|
|
2070
|
+
text: `${lines.join("\n")}
|
|
2071
|
+
`,
|
|
2072
|
+
json: envelope("init", result, contract2),
|
|
2073
|
+
exitCode: 0
|
|
2074
|
+
};
|
|
1189
2075
|
}
|
|
1190
2076
|
case "check": {
|
|
1191
2077
|
const report = checkPaths(command.paths, { cwd, contract: contract2 });
|
|
1192
|
-
const { errors, warnings } = report.summary;
|
|
1193
|
-
const lines = report.findings.map(
|
|
1194
|
-
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1195
|
-
);
|
|
1196
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1197
|
-
lines.push(
|
|
1198
|
-
`${count(errors, "error")}, ${count(warnings, "warning")} in ${count(report.files, "file")}.`
|
|
1199
|
-
);
|
|
1200
2078
|
return {
|
|
1201
|
-
text:
|
|
1202
|
-
|
|
1203
|
-
json: envelope("check", report),
|
|
2079
|
+
text: renderCheckText(report),
|
|
2080
|
+
json: envelope("check", report, contract2),
|
|
1204
2081
|
// Warnings fail the run only when asked to: they are house rules, or may be native attributes.
|
|
1205
|
-
exitCode:
|
|
2082
|
+
exitCode: checkExitCode(report, { strict: command.strict })
|
|
1206
2083
|
};
|
|
1207
2084
|
}
|
|
1208
2085
|
case "doctor": {
|
|
1209
|
-
const report = doctor({
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
...
|
|
1213
|
-
|
|
2086
|
+
const report = doctor({
|
|
2087
|
+
cwd,
|
|
2088
|
+
contract: contract2,
|
|
2089
|
+
...context.parser ? { parser: context.parser } : {}
|
|
2090
|
+
});
|
|
2091
|
+
const { lookup, check_code: checking } = report.operations;
|
|
2092
|
+
const lines = [
|
|
2093
|
+
"Readiness",
|
|
2094
|
+
` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
|
|
2095
|
+
` code checking ${checking.status.padEnd(8)} ${checking.message}`,
|
|
2096
|
+
...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
|
|
2097
|
+
"",
|
|
2098
|
+
"Setup"
|
|
2099
|
+
];
|
|
2100
|
+
lines.push(
|
|
2101
|
+
...report.checks.flatMap(({ status, message, fix }) => [
|
|
2102
|
+
`[${status}] ${message}`,
|
|
2103
|
+
...fix === void 0 ? [] : [` fix: ${fix}`]
|
|
2104
|
+
])
|
|
2105
|
+
);
|
|
1214
2106
|
lines.push(
|
|
1215
2107
|
`${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
|
|
1216
2108
|
);
|
|
1217
2109
|
return {
|
|
1218
2110
|
text: `${lines.join("\n")}
|
|
1219
2111
|
`,
|
|
1220
|
-
json: envelope("doctor", report),
|
|
2112
|
+
json: envelope("doctor", report, contract2),
|
|
1221
2113
|
exitCode: report.summary.fail > 0 ? 1 : 0
|
|
1222
2114
|
};
|
|
1223
2115
|
}
|
|
2116
|
+
case "list": {
|
|
2117
|
+
const lines = [`OPERATOR ${VERSION}`, ""];
|
|
2118
|
+
for (const kind of ["component", "recipe"]) {
|
|
2119
|
+
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
2120
|
+
lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
|
|
2121
|
+
for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
|
|
2122
|
+
lines.push("");
|
|
2123
|
+
}
|
|
2124
|
+
return {
|
|
2125
|
+
text: `${lines.join("\n")}
|
|
2126
|
+
`,
|
|
2127
|
+
json: envelope(
|
|
2128
|
+
"list",
|
|
2129
|
+
{ version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
2130
|
+
contract2
|
|
2131
|
+
),
|
|
2132
|
+
exitCode: 0
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
case "add": {
|
|
2136
|
+
const result = installSources(command, { cwd, registry: registry2 });
|
|
2137
|
+
const setup = setupFor(result.dependencies, contract2);
|
|
2138
|
+
return {
|
|
2139
|
+
text: renderInstall(result, setup),
|
|
2140
|
+
json: envelope("add", { ...result, setup }, contract2),
|
|
2141
|
+
exitCode: 0
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
1224
2144
|
}
|
|
1225
2145
|
}
|
|
1226
2146
|
|
|
1227
2147
|
// src/mcp.ts
|
|
1228
2148
|
var import_node_readline = require("readline");
|
|
2149
|
+
|
|
2150
|
+
// src/validate.ts
|
|
2151
|
+
var KNOWN = /* @__PURE__ */ new Set([
|
|
2152
|
+
"type",
|
|
2153
|
+
"description",
|
|
2154
|
+
"default",
|
|
2155
|
+
"enum",
|
|
2156
|
+
"minimum",
|
|
2157
|
+
"maximum",
|
|
2158
|
+
"minLength",
|
|
2159
|
+
"maxLength"
|
|
2160
|
+
]);
|
|
2161
|
+
var typeName = (value) => value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object" : typeof value === "number" ? "a number" : typeof value === "string" ? "a string" : typeof value === "boolean" ? "a boolean" : typeof value;
|
|
2162
|
+
function describeValue(value) {
|
|
2163
|
+
if (typeof value === "string")
|
|
2164
|
+
return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
|
|
2165
|
+
if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
|
|
2166
|
+
if (value === null || typeof value === "boolean" || value === void 0) return String(value);
|
|
2167
|
+
return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
|
|
2168
|
+
}
|
|
2169
|
+
function expectation(schema) {
|
|
2170
|
+
switch (schema.type) {
|
|
2171
|
+
case "boolean":
|
|
2172
|
+
return "true or false";
|
|
2173
|
+
case "integer":
|
|
2174
|
+
case "number": {
|
|
2175
|
+
const noun = schema.type === "integer" ? "a whole number" : "a number";
|
|
2176
|
+
if (schema.minimum !== void 0 && schema.maximum !== void 0)
|
|
2177
|
+
return `${noun} from ${schema.minimum} to ${schema.maximum}`;
|
|
2178
|
+
if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
|
|
2179
|
+
if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
|
|
2180
|
+
return noun;
|
|
2181
|
+
}
|
|
2182
|
+
case "string": {
|
|
2183
|
+
if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
|
|
2184
|
+
const { minLength, maxLength } = schema;
|
|
2185
|
+
if (minLength !== void 0 && maxLength !== void 0)
|
|
2186
|
+
return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
|
|
2187
|
+
if (maxLength !== void 0)
|
|
2188
|
+
return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
|
|
2189
|
+
return "a string";
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function accepts(schema, value) {
|
|
2194
|
+
switch (schema.type) {
|
|
2195
|
+
case "boolean":
|
|
2196
|
+
return typeof value === "boolean";
|
|
2197
|
+
case "string":
|
|
2198
|
+
return typeof value === "string" && (schema.minLength === void 0 || value.length >= schema.minLength) && (schema.maxLength === void 0 || value.length <= schema.maxLength) && (schema.enum === void 0 || schema.enum.includes(value));
|
|
2199
|
+
case "integer":
|
|
2200
|
+
case "number":
|
|
2201
|
+
return typeof value === "number" && Number.isFinite(value) && (schema.type === "number" || Number.isInteger(value)) && (schema.minimum === void 0 || value >= schema.minimum) && (schema.maximum === void 0 || value <= schema.maximum);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function validateArguments(tool, schema, args) {
|
|
2205
|
+
for (const [field, property] of Object.entries(schema.properties))
|
|
2206
|
+
for (const key of Object.keys(property))
|
|
2207
|
+
if (!KNOWN.has(key))
|
|
2208
|
+
throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
|
|
2209
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
2210
|
+
return `${tool}: arguments must be an object.`;
|
|
2211
|
+
const given = args;
|
|
2212
|
+
const required = schema.required ?? [];
|
|
2213
|
+
const blank = (value) => typeof value === "string" && value.trim() === "";
|
|
2214
|
+
const missing = [];
|
|
2215
|
+
const wrong = [];
|
|
2216
|
+
const unknown = [];
|
|
2217
|
+
for (const field of required)
|
|
2218
|
+
if (given[field] === void 0 || blank(given[field])) missing.push(field);
|
|
2219
|
+
for (const [field, value] of Object.entries(given)) {
|
|
2220
|
+
const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
|
|
2221
|
+
if (property === void 0) unknown.push(field);
|
|
2222
|
+
else if (value !== void 0 && !(required.includes(field) && blank(value))) {
|
|
2223
|
+
if (!accepts(property, value))
|
|
2224
|
+
wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const reject = schema.additionalProperties === false ? unknown : [];
|
|
2228
|
+
const names2 = Object.keys(schema.properties);
|
|
2229
|
+
if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
|
|
2230
|
+
return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
|
|
2231
|
+
const fragments = [
|
|
2232
|
+
...missing.map(
|
|
2233
|
+
(field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
|
|
2234
|
+
),
|
|
2235
|
+
...wrong,
|
|
2236
|
+
...reject.map(
|
|
2237
|
+
(field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
|
|
2238
|
+
)
|
|
2239
|
+
];
|
|
2240
|
+
return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/mcp.ts
|
|
1229
2244
|
var VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1230
2245
|
var text = (value) => ({ type: "text", text: value });
|
|
1231
|
-
var
|
|
2246
|
+
var LIMITS = { query: 500, name: 100, code: 2e5 };
|
|
2247
|
+
var ANSWERS = {
|
|
2248
|
+
search: "search",
|
|
2249
|
+
get_component: "component.detail",
|
|
2250
|
+
list_components: "component.list",
|
|
2251
|
+
get_instructions: "instructions",
|
|
2252
|
+
check_code: "check"
|
|
2253
|
+
};
|
|
2254
|
+
var readOnly = { readOnlyHint: true };
|
|
2255
|
+
var define = (tool) => ({
|
|
2256
|
+
...tool,
|
|
2257
|
+
outputSchema: outputSchemaFor(ANSWERS[tool.name]),
|
|
2258
|
+
annotations: readOnly
|
|
2259
|
+
});
|
|
1232
2260
|
var TOOLS = [
|
|
1233
|
-
{
|
|
2261
|
+
define({
|
|
1234
2262
|
name: "search",
|
|
1235
|
-
description: "Find the OPERATOR component or recipe
|
|
2263
|
+
description: "Find the OPERATOR component or recipe for a need, such as 'show a status' or 'table with selection'. Call before writing interface code.",
|
|
1236
2264
|
inputSchema: {
|
|
1237
2265
|
type: "object",
|
|
1238
2266
|
properties: {
|
|
1239
|
-
query:
|
|
1240
|
-
|
|
2267
|
+
query: {
|
|
2268
|
+
type: "string",
|
|
2269
|
+
minLength: 1,
|
|
2270
|
+
maxLength: LIMITS.query,
|
|
2271
|
+
description: "The need, in plain words."
|
|
2272
|
+
},
|
|
2273
|
+
limit: {
|
|
2274
|
+
type: "integer",
|
|
2275
|
+
minimum: 1,
|
|
2276
|
+
maximum: SEARCH_LIMIT_MAX,
|
|
2277
|
+
default: SEARCH_LIMIT_DEFAULT,
|
|
2278
|
+
description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
|
|
2279
|
+
}
|
|
1241
2280
|
},
|
|
1242
|
-
required: ["query"]
|
|
2281
|
+
required: ["query"],
|
|
2282
|
+
additionalProperties: false
|
|
1243
2283
|
}
|
|
1244
|
-
},
|
|
1245
|
-
{
|
|
2284
|
+
}),
|
|
2285
|
+
define({
|
|
1246
2286
|
name: "get_component",
|
|
1247
|
-
description: "One
|
|
2287
|
+
description: "One component's contract: what it is for, what to use instead, every prop and option value, nesting rules, an example. Call before using a component.",
|
|
1248
2288
|
inputSchema: {
|
|
1249
2289
|
type: "object",
|
|
1250
2290
|
properties: {
|
|
1251
|
-
name:
|
|
2291
|
+
name: {
|
|
2292
|
+
type: "string",
|
|
2293
|
+
minLength: 1,
|
|
2294
|
+
maxLength: LIMITS.name,
|
|
2295
|
+
description: "Component name, such as Button."
|
|
2296
|
+
},
|
|
1252
2297
|
dense: {
|
|
1253
2298
|
type: "boolean",
|
|
1254
|
-
|
|
2299
|
+
default: true,
|
|
2300
|
+
description: "Compact form (every constraint, one sentence per prop). False: full prose."
|
|
1255
2301
|
}
|
|
1256
2302
|
},
|
|
1257
|
-
required: ["name"]
|
|
2303
|
+
required: ["name"],
|
|
2304
|
+
additionalProperties: false
|
|
1258
2305
|
}
|
|
1259
|
-
},
|
|
1260
|
-
{
|
|
2306
|
+
}),
|
|
2307
|
+
define({
|
|
1261
2308
|
name: "list_components",
|
|
1262
2309
|
description: "Every OPERATOR component in one line each, with its parts.",
|
|
1263
|
-
inputSchema: { type: "object", properties: {} }
|
|
1264
|
-
},
|
|
1265
|
-
{
|
|
2310
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2311
|
+
}),
|
|
2312
|
+
define({
|
|
1266
2313
|
name: "get_instructions",
|
|
1267
|
-
description: "OPERATOR's house rules,
|
|
1268
|
-
inputSchema: { type: "object", properties: {} }
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
2314
|
+
description: "OPERATOR's house rules, component index and recipes: read once before building.",
|
|
2315
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2316
|
+
}),
|
|
2317
|
+
define({
|
|
1271
2318
|
name: "check_code",
|
|
1272
|
-
description: "Check TSX
|
|
2319
|
+
description: "Check TSX against OPERATOR's contract: option values, nesting, required props, house rules. Call before you finish. Read evaluation_status: only evaluated_clean means every file was read in full with nothing found. Types and rendering are not checked.",
|
|
1273
2320
|
inputSchema: {
|
|
1274
2321
|
type: "object",
|
|
1275
|
-
properties: {
|
|
1276
|
-
|
|
2322
|
+
properties: {
|
|
2323
|
+
code: {
|
|
2324
|
+
type: "string",
|
|
2325
|
+
minLength: 1,
|
|
2326
|
+
maxLength: LIMITS.code,
|
|
2327
|
+
description: "The TSX source of one file."
|
|
2328
|
+
}
|
|
2329
|
+
},
|
|
2330
|
+
required: ["code"],
|
|
2331
|
+
additionalProperties: false
|
|
1277
2332
|
}
|
|
1278
|
-
}
|
|
2333
|
+
})
|
|
1279
2334
|
];
|
|
1280
2335
|
function runTool(name, args, context) {
|
|
1281
2336
|
const { contract: contract2, registry: registry2 } = context;
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
return value;
|
|
1287
|
-
};
|
|
2337
|
+
const definition = TOOLS.find((tool) => tool.name === name);
|
|
2338
|
+
if (definition === void 0) throw new UnknownTool(name);
|
|
2339
|
+
const problem = validateArguments(name, definition.inputSchema, args);
|
|
2340
|
+
if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
|
|
1288
2341
|
switch (name) {
|
|
1289
2342
|
case "search": {
|
|
1290
|
-
const query =
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
query
|
|
1295
|
-
|
|
1296
|
-
);
|
|
1297
|
-
return results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
1298
|
-
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
2343
|
+
const query = args["query"];
|
|
2344
|
+
const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
|
|
2345
|
+
const results = search(contract2, registry2, query, limit);
|
|
2346
|
+
return {
|
|
2347
|
+
text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
2348
|
+
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
1299
2349
|
${next}`
|
|
1300
|
-
|
|
2350
|
+
).join("\n"),
|
|
2351
|
+
structured: envelope(
|
|
2352
|
+
"search",
|
|
2353
|
+
{ query, limit, matchCount: results.length, results },
|
|
2354
|
+
contract2
|
|
2355
|
+
)
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
case "get_component": {
|
|
2359
|
+
const dense = args["dense"] ?? true;
|
|
2360
|
+
const detail = componentDetail(contract2, args["name"]);
|
|
2361
|
+
return {
|
|
2362
|
+
text: renderComponent(detail, { dense }),
|
|
2363
|
+
structured: envelope("component.detail", componentData(detail, dense), contract2)
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
case "list_components": {
|
|
2367
|
+
const components = componentIndex(contract2);
|
|
2368
|
+
return {
|
|
2369
|
+
text: components.map(
|
|
2370
|
+
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
2371
|
+
).join("\n"),
|
|
2372
|
+
structured: envelope("component.list", { components }, contract2)
|
|
2373
|
+
};
|
|
1301
2374
|
}
|
|
1302
|
-
case "get_component":
|
|
1303
|
-
return renderComponent(componentDetail(contract2, required("name")), {
|
|
1304
|
-
dense: args.dense !== false
|
|
1305
|
-
});
|
|
1306
|
-
case "list_components":
|
|
1307
|
-
return componentIndex(contract2).map(
|
|
1308
|
-
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
1309
|
-
).join("\n");
|
|
1310
2375
|
case "get_instructions":
|
|
1311
|
-
return
|
|
2376
|
+
return {
|
|
2377
|
+
text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
|
|
2378
|
+
structured: envelope("instructions", instructionsData(contract2), contract2)
|
|
2379
|
+
};
|
|
1312
2380
|
case "check_code": {
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
({ line, column, severity, code: finding, message }) => `${line}:${column} ${severity} ${finding} ${message}`
|
|
1324
|
-
),
|
|
1325
|
-
`${count(errors, "error")}, ${count(findings.length - errors, "warning")}.`
|
|
1326
|
-
].join("\n");
|
|
2381
|
+
const { typescript, parser } = resolveParser(
|
|
2382
|
+
context.cwd ?? process.cwd(),
|
|
2383
|
+
context.typescript,
|
|
2384
|
+
"check_code"
|
|
2385
|
+
);
|
|
2386
|
+
const report = buildCheckReport(
|
|
2387
|
+
[checkSource(typescript, contract2, "snippet.tsx", args["code"])],
|
|
2388
|
+
{ parser }
|
|
2389
|
+
);
|
|
2390
|
+
return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
|
|
1327
2391
|
}
|
|
1328
2392
|
default:
|
|
1329
2393
|
throw new UnknownTool(name);
|
|
@@ -1337,24 +2401,28 @@ var UnknownTool = class extends Error {
|
|
|
1337
2401
|
}
|
|
1338
2402
|
};
|
|
1339
2403
|
function handleMessage(raw, context) {
|
|
1340
|
-
const
|
|
2404
|
+
const failure2 = (id2, code, message) => ({
|
|
1341
2405
|
jsonrpc: "2.0",
|
|
1342
2406
|
id: id2 ?? null,
|
|
1343
2407
|
error: { code, message }
|
|
1344
2408
|
});
|
|
1345
|
-
if (typeof raw !== "object" || raw === null) return
|
|
2409
|
+
if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
|
|
1346
2410
|
const { id, method, params } = raw;
|
|
1347
|
-
if (typeof method !== "string") return
|
|
2411
|
+
if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
|
|
1348
2412
|
if (id === void 0) return null;
|
|
1349
2413
|
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
1350
2414
|
switch (method) {
|
|
1351
2415
|
case "initialize": {
|
|
1352
2416
|
const asked = params?.protocolVersion;
|
|
2417
|
+
const identity = identityOf(context.contract);
|
|
1353
2418
|
return result({
|
|
1354
2419
|
protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
|
|
1355
2420
|
capabilities: { tools: {} },
|
|
1356
|
-
|
|
1357
|
-
|
|
2421
|
+
// The program's version, not the contract's: the contract has its own, in `_meta` and in
|
|
2422
|
+
// the identity of every result.
|
|
2423
|
+
serverInfo: { name: "oprt", version: VERSION },
|
|
2424
|
+
instructions: "OPERATOR design system. Call get_instructions once, search for the component that answers a need, get_component before using one, and check_code before you finish. check_code says how much it could read: only evaluated_clean means the code was read in full with nothing found.",
|
|
2425
|
+
_meta: { "dev.oprt/identity": identity }
|
|
1358
2426
|
});
|
|
1359
2427
|
}
|
|
1360
2428
|
case "ping":
|
|
@@ -1364,17 +2432,23 @@ function handleMessage(raw, context) {
|
|
|
1364
2432
|
case "tools/call": {
|
|
1365
2433
|
const { name, arguments: args } = params ?? {};
|
|
1366
2434
|
try {
|
|
1367
|
-
|
|
2435
|
+
const output = runTool(String(name), args ?? {}, context);
|
|
2436
|
+
return result({
|
|
2437
|
+
content: [text(output.text)],
|
|
2438
|
+
structuredContent: output.structured
|
|
2439
|
+
});
|
|
1368
2440
|
} catch (error) {
|
|
1369
|
-
if (error instanceof UnknownTool) return
|
|
2441
|
+
if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
|
|
2442
|
+
const failed = errorEnvelope(error, context.contract);
|
|
1370
2443
|
return result({
|
|
1371
|
-
content: [text(
|
|
2444
|
+
content: [text(failed.error)],
|
|
2445
|
+
structuredContent: failed,
|
|
1372
2446
|
isError: true
|
|
1373
2447
|
});
|
|
1374
2448
|
}
|
|
1375
2449
|
}
|
|
1376
2450
|
default:
|
|
1377
|
-
return
|
|
2451
|
+
return failure2(id, -32601, `Method not found: ${method}`);
|
|
1378
2452
|
}
|
|
1379
2453
|
}
|
|
1380
2454
|
function serve(input, output, context) {
|
|
@@ -1395,10 +2469,9 @@ function serve(input, output, context) {
|
|
|
1395
2469
|
}
|
|
1396
2470
|
|
|
1397
2471
|
// src/index.ts
|
|
1398
|
-
var VERSION = package_default.version;
|
|
1399
2472
|
function loadRegistry() {
|
|
1400
|
-
const here = (0,
|
|
1401
|
-
const candidates = [(0,
|
|
2473
|
+
const here = (0, import_node_path7.dirname)((0, import_node_url3.fileURLToPath)(importMetaUrl));
|
|
2474
|
+
const candidates = [(0, import_node_path7.join)(here, "registry.json"), (0, import_node_path7.join)(here, "../generated/registry.json")];
|
|
1402
2475
|
const found = candidates.find((candidate) => (0, import_node_fs6.existsSync)(candidate));
|
|
1403
2476
|
if (found === void 0) {
|
|
1404
2477
|
throw new Error(
|
|
@@ -1420,26 +2493,38 @@ function install(options, context = {}) {
|
|
|
1420
2493
|
0 && (module.exports = {
|
|
1421
2494
|
CliError,
|
|
1422
2495
|
DEFAULT_OUT,
|
|
2496
|
+
RESULT_SCHEMAS,
|
|
2497
|
+
RESULT_SCHEMA_VERSION,
|
|
2498
|
+
RESULT_TYPES,
|
|
1423
2499
|
TOOLS,
|
|
1424
2500
|
VERSION,
|
|
1425
2501
|
applyBlock,
|
|
1426
2502
|
blockVersion,
|
|
2503
|
+
buildCheckReport,
|
|
2504
|
+
checkExitCode,
|
|
1427
2505
|
checkPaths,
|
|
1428
2506
|
checkSource,
|
|
1429
2507
|
componentDetail,
|
|
1430
2508
|
componentIndex,
|
|
1431
2509
|
contract,
|
|
2510
|
+
contractDigest,
|
|
1432
2511
|
doctor,
|
|
2512
|
+
envelope,
|
|
2513
|
+
errorEnvelope,
|
|
1433
2514
|
handleMessage,
|
|
2515
|
+
identityOf,
|
|
1434
2516
|
init,
|
|
1435
2517
|
install,
|
|
2518
|
+
outputSchemaFor,
|
|
1436
2519
|
parseArgs,
|
|
1437
2520
|
registry,
|
|
1438
2521
|
removeBlock,
|
|
1439
2522
|
renderAgentBlock,
|
|
2523
|
+
renderCheckText,
|
|
1440
2524
|
renderComponent,
|
|
1441
2525
|
resolveEntries,
|
|
1442
2526
|
runAgentCommand,
|
|
2527
|
+
schemaFor,
|
|
1443
2528
|
search,
|
|
1444
2529
|
serve
|
|
1445
2530
|
});
|