@oprt/cli 0.1.0 → 0.4.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 +49 -23
- package/dist/babel-parser.cjs +14339 -0
- package/dist/bin.js +1950 -743
- package/dist/contract.json +1 -1
- package/dist/index.cjs +1662 -459
- package/dist/index.d.cts +250 -97
- package/dist/index.d.ts +250 -97
- package/dist/index.js +1652 -461
- 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 +4 -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
|
});
|
|
@@ -56,75 +68,6 @@ var import_node_fs6 = require("fs");
|
|
|
56
68
|
var import_node_path6 = 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,362 @@ 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.4.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
|
+
"@babel/parser": "8.0.8",
|
|
620
|
+
"@babel/types": "8.0.8",
|
|
621
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
622
|
+
tsup: "^8.3.0",
|
|
623
|
+
typescript: "^5.6.3",
|
|
624
|
+
vitest: "^2.1.3"
|
|
625
|
+
},
|
|
626
|
+
license: "MIT",
|
|
627
|
+
publishConfig: {
|
|
628
|
+
access: "public"
|
|
439
629
|
}
|
|
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;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/identity.ts
|
|
633
|
+
var VERSION = package_default.version;
|
|
634
|
+
var RESULT_SCHEMA_VERSION = 2;
|
|
635
|
+
var digests = /* @__PURE__ */ new WeakMap();
|
|
636
|
+
function contractDigest(contract2) {
|
|
637
|
+
let digest = digests.get(contract2);
|
|
638
|
+
if (digest === void 0) {
|
|
639
|
+
digest = `sha256:${(0, import_node_crypto.createHash)("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
|
|
640
|
+
digests.set(contract2, digest);
|
|
467
641
|
}
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
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
|
-
}));
|
|
642
|
+
return digest;
|
|
479
643
|
}
|
|
480
|
-
function
|
|
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);
|
|
644
|
+
function identityOf(contract2) {
|
|
489
645
|
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
|
|
646
|
+
cli_version: VERSION,
|
|
647
|
+
contract_version: contract2.version,
|
|
648
|
+
contract_digest: contractDigest(contract2),
|
|
649
|
+
schema_version: RESULT_SCHEMA_VERSION
|
|
502
650
|
};
|
|
503
651
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
652
|
+
|
|
653
|
+
// src/check.ts
|
|
654
|
+
var import_node_fs3 = require("fs");
|
|
655
|
+
var import_node_path3 = require("path");
|
|
656
|
+
|
|
657
|
+
// src/parser.ts
|
|
658
|
+
var import_node_module = require("module");
|
|
659
|
+
var import_node_url2 = require("url");
|
|
660
|
+
|
|
661
|
+
// ../../node_modules/.pnpm/@babel+parser@8.0.8/node_modules/@babel/parser/package.json
|
|
662
|
+
var version = "8.0.8";
|
|
663
|
+
|
|
664
|
+
// src/parser.ts
|
|
665
|
+
var PARSER = {
|
|
666
|
+
name: "@babel/parser",
|
|
667
|
+
version,
|
|
668
|
+
source: "bundled"
|
|
669
|
+
};
|
|
670
|
+
function optionsFor(file) {
|
|
671
|
+
const typed = /\.tsx$/.test(file);
|
|
672
|
+
const plugins = [
|
|
673
|
+
"jsx",
|
|
674
|
+
...typed ? ["typescript"] : [],
|
|
675
|
+
"decorators",
|
|
676
|
+
"decoratorAutoAccessors",
|
|
677
|
+
"deferredImportEvaluation",
|
|
678
|
+
"sourcePhaseImports"
|
|
509
679
|
];
|
|
510
|
-
|
|
511
|
-
|
|
680
|
+
return {
|
|
681
|
+
sourceType: "module",
|
|
682
|
+
plugins,
|
|
683
|
+
// Recoverable errors are returned beside the tree, instead of the first one being thrown.
|
|
684
|
+
errorRecovery: true,
|
|
685
|
+
attachComment: false,
|
|
686
|
+
allowReturnOutsideFunction: true,
|
|
687
|
+
allowUndeclaredExports: true,
|
|
688
|
+
allowSuperOutsideMethod: true,
|
|
689
|
+
allowNewTargetOutsideFunction: true
|
|
690
|
+
};
|
|
512
691
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
692
|
+
var FILE = "babel-parser.cjs";
|
|
693
|
+
var REINSTALL = "Install @oprt/cli again: the parser ships beside the executable in dist.";
|
|
694
|
+
var loaded;
|
|
695
|
+
function loadBabel() {
|
|
696
|
+
if (loaded !== void 0) return loaded;
|
|
697
|
+
const fromSource = /\.[cm]?tsx?$/.test((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
698
|
+
try {
|
|
699
|
+
const require2 = (0, import_node_module.createRequire)(importMetaUrl);
|
|
700
|
+
loaded = fromSource ? require2("@babel/parser") : require2("./babel-parser.cjs");
|
|
701
|
+
} catch (error) {
|
|
702
|
+
throw new CliError(
|
|
703
|
+
`Code checking cannot load its parser (${FILE}): ${error instanceof Error ? error.message : String(error)} ${REINSTALL}`,
|
|
704
|
+
1,
|
|
705
|
+
{ code: "ERR_PARSER_MISSING" }
|
|
520
706
|
);
|
|
521
|
-
if (!dense) lines.push("", detail.description, "");
|
|
522
|
-
if (detail.props.length > 0) {
|
|
523
|
-
lines.push("Props:");
|
|
524
|
-
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
525
707
|
}
|
|
526
|
-
|
|
527
|
-
lines.push(
|
|
528
|
-
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
529
|
-
);
|
|
530
|
-
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
531
|
-
if (detail.allowed_children !== void 0)
|
|
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
|
-
`;
|
|
708
|
+
return loaded;
|
|
542
709
|
}
|
|
543
|
-
var
|
|
544
|
-
|
|
545
|
-
"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
var matched = (query, text2) => {
|
|
550
|
-
const present = new Set(words(text2));
|
|
551
|
-
return query.filter((word) => present.has(word)).length;
|
|
710
|
+
var rejects = (babel, code) => {
|
|
711
|
+
try {
|
|
712
|
+
return babel.parse(code, optionsFor("probe.tsx")).errors.length > 0;
|
|
713
|
+
} catch {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
552
716
|
};
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
717
|
+
function probeParser(load = loadBabel) {
|
|
718
|
+
let babel;
|
|
719
|
+
try {
|
|
720
|
+
babel = load();
|
|
721
|
+
} catch (error) {
|
|
722
|
+
return {
|
|
723
|
+
ready: false,
|
|
724
|
+
reason: "parser_missing",
|
|
725
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (rejects(babel, "export const a = <a b={1 as number} />;"))
|
|
729
|
+
return {
|
|
730
|
+
ready: false,
|
|
731
|
+
reason: "parser_unusable",
|
|
732
|
+
detail: "it rejected valid code in a probe parse"
|
|
733
|
+
};
|
|
734
|
+
if (!rejects(babel, "export const a = <a>;"))
|
|
735
|
+
return {
|
|
736
|
+
ready: false,
|
|
737
|
+
reason: "parser_unusable",
|
|
738
|
+
detail: "it did not report a syntax error in code that has one"
|
|
739
|
+
};
|
|
740
|
+
return { ready: true, parser: PARSER };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/syntax.ts
|
|
744
|
+
var attempt = (babel, text2, file) => {
|
|
745
|
+
try {
|
|
746
|
+
const tree = babel.parse(text2, optionsFor(file));
|
|
747
|
+
return { tree, errors: tree.errors };
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return { tree: null, fatal: error };
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
var sentence = (message) => message.replace(/\s*\(\d+:\d+\)$/, "");
|
|
753
|
+
var problemOf = (failure2) => ({
|
|
754
|
+
line: failure2.loc.line,
|
|
755
|
+
column: failure2.loc.column + 1,
|
|
756
|
+
message: sentence(failure2.message),
|
|
757
|
+
code: failure2.reasonCode ?? "SyntaxError",
|
|
758
|
+
position: failure2.pos
|
|
759
|
+
});
|
|
760
|
+
var tagName = (name) => {
|
|
761
|
+
if (name === void 0) return "";
|
|
762
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
763
|
+
if (name.type === "JSXMemberExpression")
|
|
764
|
+
return `${tagName(name.object)}.${name.property.name}`;
|
|
765
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
766
|
+
};
|
|
767
|
+
var NOT_NODES = /* @__PURE__ */ new Set(["loc", "extra", "errors", "tokens", "comments"]);
|
|
768
|
+
function* childNodes(node) {
|
|
769
|
+
for (const [key, value] of Object.entries(node)) {
|
|
770
|
+
if (NOT_NODES.has(key) || typeof value !== "object" || value === null) continue;
|
|
771
|
+
if (Array.isArray(value)) {
|
|
772
|
+
for (const item of value)
|
|
773
|
+
if (typeof item === "object" && item !== null && typeof item.type === "string")
|
|
774
|
+
yield item;
|
|
775
|
+
} else if (typeof value.type === "string") yield value;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
var walk = (node, ancestors, visit) => {
|
|
779
|
+
visit(node, ancestors);
|
|
780
|
+
ancestors.push(node);
|
|
781
|
+
for (const child of childNodes(node)) walk(child, ancestors, visit);
|
|
782
|
+
ancestors.pop();
|
|
783
|
+
};
|
|
784
|
+
var unclosed = (element) => {
|
|
785
|
+
const name = element.openingElement.name;
|
|
786
|
+
return {
|
|
787
|
+
line: name.loc?.start.line ?? 1,
|
|
788
|
+
column: (name.loc?.start.column ?? 0) + 1,
|
|
789
|
+
message: `JSX element '${tagName(name)}' has no corresponding closing tag.`,
|
|
790
|
+
code: "UnclosedJsxElement",
|
|
791
|
+
position: name.start ?? 0
|
|
564
792
|
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
793
|
+
};
|
|
794
|
+
function problemsOf(tree, errors, limit, consumed) {
|
|
795
|
+
const mismatched = /* @__PURE__ */ new Map();
|
|
796
|
+
walk(tree.program, [], (node, ancestors) => {
|
|
797
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) < limit && tagName(node.openingElement.name) !== tagName(node.closingElement.name))
|
|
798
|
+
mismatched.set(node.closingElement.start ?? 0, { element: node, ancestors: [...ancestors] });
|
|
799
|
+
});
|
|
800
|
+
const problems = [];
|
|
801
|
+
for (const error of errors) {
|
|
802
|
+
if (error.pos >= limit) continue;
|
|
803
|
+
const entry = error.reasonCode === "MissingClosingTagElement" && mismatched.get(error.pos);
|
|
804
|
+
if (entry) {
|
|
805
|
+
const closing = tagName(entry.element.closingElement?.name);
|
|
806
|
+
const outer = [...entry.ancestors].reverse().find(
|
|
807
|
+
(ancestor) => ancestor.type === "JSXElement" && tagName(ancestor.openingElement.name) === closing && !consumed.has(ancestor)
|
|
808
|
+
);
|
|
809
|
+
if (outer !== void 0) {
|
|
810
|
+
consumed.add(outer);
|
|
811
|
+
problems.push(unclosed(entry.element));
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
579
814
|
}
|
|
580
|
-
|
|
581
|
-
if (inDescription > 0) add(name, 2 * inDescription);
|
|
815
|
+
problems.push(problemOf(error));
|
|
582
816
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
817
|
+
return problems;
|
|
818
|
+
}
|
|
819
|
+
var TYPE_ARGUMENTS = /(?<=[\w$\])])<(?:[^<>()]|<(?:[^<>()]|<[^<>()]*>)*>)*>(?=\()/g;
|
|
820
|
+
var LINE_BREAKS = new RegExp(`[^\\r\\n${String.fromCharCode(8232, 8233)}]`, "g");
|
|
821
|
+
var blankedOut = (match) => match.replace(LINE_BREAKS, " ");
|
|
822
|
+
var CLOSING_TAG = "</_>";
|
|
823
|
+
var COMPLETIONS = [CLOSING_TAG, ")", "}", "]"];
|
|
824
|
+
var MAX_COMPLETIONS = 24;
|
|
825
|
+
function complete(babel, prefix, file) {
|
|
826
|
+
let text2 = prefix;
|
|
827
|
+
let first = null;
|
|
828
|
+
for (let step = 0; step <= MAX_COMPLETIONS; step++) {
|
|
829
|
+
const tried = attempt(babel, text2, file);
|
|
830
|
+
if (tried.tree !== null) return { tree: tried.tree, errors: tried.errors, first };
|
|
831
|
+
const wantsMore = tried.fatal.reasonCode === "UnterminatedJsxContent" || tried.fatal.pos >= text2.length;
|
|
832
|
+
if (!wantsMore) return null;
|
|
833
|
+
const next = COMPLETIONS.find((piece) => {
|
|
834
|
+
const extended = attempt(babel, text2 + piece, file);
|
|
835
|
+
return extended.tree !== null || extended.fatal.reasonCode === "UnterminatedJsxContent" || extended.fatal.pos >= text2.length + piece.length;
|
|
593
836
|
});
|
|
837
|
+
if (next === void 0) return null;
|
|
838
|
+
first ??= next;
|
|
839
|
+
text2 += next;
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
function shutByCompletion(tree, limit) {
|
|
844
|
+
const shut = [];
|
|
845
|
+
walk(tree.program, [], (node) => {
|
|
846
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) >= limit)
|
|
847
|
+
shut.push(node);
|
|
848
|
+
});
|
|
849
|
+
return shut.sort((a, b) => (a.closingElement?.start ?? 0) - (b.closingElement?.start ?? 0));
|
|
850
|
+
}
|
|
851
|
+
var byPlace = (a, b) => a.position - b.position;
|
|
852
|
+
var MAX_EXPLAINED = 2e5;
|
|
853
|
+
function readSyntax(file, text2, babel = loadBabel()) {
|
|
854
|
+
const first = attempt(babel, text2, file);
|
|
855
|
+
if (first.tree !== null) {
|
|
856
|
+
const problems = problemsOf(first.tree, first.errors, Infinity, /* @__PURE__ */ new Set());
|
|
857
|
+
return problems.length === 0 ? { tree: first.tree, problems: [] } : { tree: null, problems: problems.sort(byPlace) };
|
|
594
858
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
kind: "recipe",
|
|
604
|
-
name: entry.name,
|
|
605
|
-
purpose: firstSentence(entry.description),
|
|
606
|
-
score,
|
|
607
|
-
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
608
|
-
});
|
|
859
|
+
let fatal = first.fatal;
|
|
860
|
+
if (text2.length > MAX_EXPLAINED) return { tree: null, problems: [problemOf(fatal)] };
|
|
861
|
+
if (/\.tsx$/.test(file) && fatal.reasonCode !== "UnterminatedJsxContent") {
|
|
862
|
+
const plain = text2.replace(TYPE_ARGUMENTS, blankedOut);
|
|
863
|
+
if (plain !== text2) {
|
|
864
|
+
const again = attempt(babel, plain, file);
|
|
865
|
+
if (again.tree === null && again.fatal.pos > fatal.pos) fatal = again.fatal;
|
|
866
|
+
}
|
|
609
867
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
868
|
+
try {
|
|
869
|
+
const finished = complete(babel, text2.slice(0, fatal.pos), file);
|
|
870
|
+
if (finished !== null) {
|
|
871
|
+
const limit = fatal.pos;
|
|
872
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
873
|
+
const problems = problemsOf(finished.tree, finished.errors, limit, consumed);
|
|
874
|
+
const open = shutByCompletion(finished.tree, limit).filter(
|
|
875
|
+
(element) => !consumed.has(element)
|
|
876
|
+
);
|
|
877
|
+
const atEnd = fatal.reasonCode === "UnterminatedJsxContent" || fatal.pos >= text2.length;
|
|
878
|
+
const hints = (atEnd ? open : finished.first === CLOSING_TAG ? open.slice(0, 1) : []).map(
|
|
879
|
+
unclosed
|
|
880
|
+
);
|
|
881
|
+
const explained = [...problems, ...hints].some(({ code }) => code === "UnclosedJsxElement");
|
|
882
|
+
problems.push(...hints);
|
|
883
|
+
if (!(explained && fatal.reasonCode === "UnterminatedJsxContent"))
|
|
884
|
+
problems.push(problemOf(fatal));
|
|
885
|
+
return { tree: null, problems: problems.sort(byPlace) };
|
|
886
|
+
}
|
|
887
|
+
} catch {
|
|
888
|
+
}
|
|
889
|
+
return { tree: null, problems: [problemOf(fatal)] };
|
|
613
890
|
}
|
|
614
891
|
|
|
615
892
|
// src/check.ts
|
|
893
|
+
var FINDING_CODES = [
|
|
894
|
+
"INVALID_PROP_VALUE",
|
|
895
|
+
"UNKNOWN_EXPORT",
|
|
896
|
+
"DOT_NOTATION",
|
|
897
|
+
"INVALID_NESTING",
|
|
898
|
+
"INVALID_CHILD",
|
|
899
|
+
"MISSING_REQUIRED_PROP",
|
|
900
|
+
"UNKNOWN_PROP",
|
|
901
|
+
"RAW_COLOR",
|
|
902
|
+
"ARBITRARY_SIZE",
|
|
903
|
+
"ROUNDED_CORNER",
|
|
904
|
+
"NATIVE_CONTROL",
|
|
905
|
+
/** The file does not parse. It was not checked against the contract. */
|
|
906
|
+
"SYNTAX_ERROR"
|
|
907
|
+
];
|
|
908
|
+
var LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
|
|
909
|
+
var SCOPE = {
|
|
910
|
+
checked: ["syntax", "contract", "house_rules"],
|
|
911
|
+
not_checked: ["types", "render"]
|
|
912
|
+
};
|
|
616
913
|
var PACKAGE = "@oprt/react";
|
|
617
914
|
var NATIVE = {
|
|
618
915
|
button: "Button",
|
|
@@ -648,19 +945,48 @@ function allowedChildren2(rules, name) {
|
|
|
648
945
|
}
|
|
649
946
|
return void 0;
|
|
650
947
|
}
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
948
|
+
var quote = (text2) => {
|
|
949
|
+
const flat = text2.replace(/\s+/g, " ").trim();
|
|
950
|
+
return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
|
|
951
|
+
};
|
|
952
|
+
var MAX_SYNTAX_ERRORS = 10;
|
|
953
|
+
function checkSource(contract2, file, text2) {
|
|
954
|
+
const reading = readSyntax(file, text2);
|
|
955
|
+
if (reading.tree === null) {
|
|
956
|
+
const shown = reading.problems.slice(0, MAX_SYNTAX_ERRORS);
|
|
957
|
+
const more = reading.problems.length - shown.length;
|
|
958
|
+
return {
|
|
959
|
+
file,
|
|
960
|
+
status: "not_evaluated",
|
|
961
|
+
limitations: [],
|
|
962
|
+
findings: shown.map(
|
|
963
|
+
({ line, column, message, code }, index) => ({
|
|
964
|
+
code: "SYNTAX_ERROR",
|
|
965
|
+
severity: "error",
|
|
966
|
+
file,
|
|
967
|
+
line,
|
|
968
|
+
column,
|
|
969
|
+
message: `${message} (${code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
|
|
970
|
+
})
|
|
971
|
+
)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
655
974
|
const findings = [];
|
|
975
|
+
const limitations = [];
|
|
976
|
+
const place = (node) => ({
|
|
977
|
+
line: node.loc?.start.line ?? 1,
|
|
978
|
+
column: (node.loc?.start.column ?? 0) + 1
|
|
979
|
+
});
|
|
980
|
+
const textOf = (node) => text2.slice(node.start ?? 0, node.end ?? 0);
|
|
981
|
+
const limit = (node, limitation) => {
|
|
982
|
+
limitations.push({ ...limitation, file, ...place(node) });
|
|
983
|
+
};
|
|
656
984
|
const report = (node, finding) => {
|
|
657
|
-
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
658
985
|
findings.push({
|
|
659
986
|
code: finding.code,
|
|
660
987
|
severity: finding.severity,
|
|
661
988
|
file,
|
|
662
|
-
|
|
663
|
-
column: character + 1,
|
|
989
|
+
...place(node),
|
|
664
990
|
...finding.component === void 0 ? {} : { component: finding.component },
|
|
665
991
|
...finding.prop === void 0 ? {} : { prop: finding.prop },
|
|
666
992
|
message: finding.message,
|
|
@@ -671,21 +997,18 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
671
997
|
const isComponent = (name) => Object.hasOwn(contract2.components, name);
|
|
672
998
|
const named = /* @__PURE__ */ new Map();
|
|
673
999
|
const namespaces = /* @__PURE__ */ new Set();
|
|
674
|
-
for (const statement of
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
const from = statement.moduleSpecifier.text;
|
|
678
|
-
const clause = statement.importClause;
|
|
679
|
-
if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
|
|
1000
|
+
for (const statement of reading.tree.program.body) {
|
|
1001
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
1002
|
+
const from = statement.source.value;
|
|
680
1003
|
const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0, import_node_path3.basename)(from));
|
|
681
1004
|
if (from !== PACKAGE && !copied) continue;
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (element2.
|
|
688
|
-
const exported =
|
|
1005
|
+
for (const element2 of statement.specifiers) {
|
|
1006
|
+
if (element2.type === "ImportNamespaceSpecifier") {
|
|
1007
|
+
if (from === PACKAGE) namespaces.add(element2.local.name);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (element2.type !== "ImportSpecifier" || element2.importKind === "type") continue;
|
|
1011
|
+
const exported = element2.imported.type === "Identifier" ? element2.imported.name : element2.imported.value;
|
|
689
1012
|
if (from === PACKAGE && !contract2.exports.includes(exported)) {
|
|
690
1013
|
let suggestions = nearest(exported, Object.keys(contract2.components));
|
|
691
1014
|
if (suggestions.length === 0) suggestions = nearest(exported, contract2.exports);
|
|
@@ -710,26 +1033,55 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
710
1033
|
});
|
|
711
1034
|
continue;
|
|
712
1035
|
}
|
|
713
|
-
if (isComponent(exported)) named.set(element2.name
|
|
1036
|
+
if (isComponent(exported)) named.set(element2.local.name, exported);
|
|
714
1037
|
}
|
|
715
1038
|
}
|
|
1039
|
+
const isString = (node) => node.type === "StringLiteral";
|
|
1040
|
+
const plainTemplate = (node) => node.type === "TemplateLiteral" && node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw : void 0;
|
|
716
1041
|
const literals = (expression) => {
|
|
717
1042
|
if (!expression) return [];
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
if (
|
|
721
|
-
if (
|
|
722
|
-
return [...literals(expression.
|
|
1043
|
+
if (isString(expression)) return [expression.value];
|
|
1044
|
+
const template = plainTemplate(expression);
|
|
1045
|
+
if (template !== void 0) return [template];
|
|
1046
|
+
if (expression.type === "ConditionalExpression")
|
|
1047
|
+
return [...literals(expression.consequent), ...literals(expression.alternate)];
|
|
723
1048
|
return [];
|
|
724
1049
|
};
|
|
725
|
-
const valueOf = (attribute) =>
|
|
1050
|
+
const valueOf = (attribute) => {
|
|
1051
|
+
const value = attribute.value;
|
|
1052
|
+
if (value === null || value === void 0) return void 0;
|
|
1053
|
+
if (value.type === "JSXExpressionContainer")
|
|
1054
|
+
return value.expression.type === "JSXEmptyExpression" ? void 0 : value.expression;
|
|
1055
|
+
return value;
|
|
1056
|
+
};
|
|
1057
|
+
const optionValue = (expression) => {
|
|
1058
|
+
const read = [];
|
|
1059
|
+
const unread = [];
|
|
1060
|
+
const walk2 = (node) => {
|
|
1061
|
+
const template = plainTemplate(node);
|
|
1062
|
+
if (isString(node)) read.push(node.value);
|
|
1063
|
+
else if (template !== void 0) read.push(template);
|
|
1064
|
+
else if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "TSSatisfiesExpression")
|
|
1065
|
+
walk2(node.expression);
|
|
1066
|
+
else if (node.type === "ConditionalExpression") {
|
|
1067
|
+
walk2(node.consequent);
|
|
1068
|
+
walk2(node.alternate);
|
|
1069
|
+
} else if (node.type === "LogicalExpression") {
|
|
1070
|
+
if (node.operator === "??" || node.operator === "||") {
|
|
1071
|
+
walk2(node.left);
|
|
1072
|
+
walk2(node.right);
|
|
1073
|
+
} else walk2(node.right);
|
|
1074
|
+
} else if (!// No value: the component's default applies.
|
|
1075
|
+
(node.type === "Identifier" && node.name === "undefined" || node.type === "NullLiteral"))
|
|
1076
|
+
unread.push(node);
|
|
1077
|
+
};
|
|
1078
|
+
if (expression) walk2(expression);
|
|
1079
|
+
return { literals: read, unread };
|
|
1080
|
+
};
|
|
726
1081
|
const classText = (expression) => {
|
|
727
1082
|
if (!expression) return "";
|
|
728
|
-
if (
|
|
729
|
-
return
|
|
730
|
-
expression.head.text,
|
|
731
|
-
...expression.templateSpans.map((span) => span.literal.text)
|
|
732
|
-
].join(" ");
|
|
1083
|
+
if (expression.type === "TemplateLiteral" && expression.expressions.length > 0)
|
|
1084
|
+
return expression.quasis.map((span) => span.value.cooked ?? span.value.raw).join(" ");
|
|
733
1085
|
return literals(expression).join(" ");
|
|
734
1086
|
};
|
|
735
1087
|
const houseRules = (attribute, name) => {
|
|
@@ -755,9 +1107,9 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
755
1107
|
message: "A rounded corner. The radius is 0 everywhere in OPERATOR."
|
|
756
1108
|
});
|
|
757
1109
|
}
|
|
758
|
-
if (name === "style" && value &&
|
|
1110
|
+
if (name === "style" && value && value.type === "ObjectExpression") {
|
|
759
1111
|
for (const property of value.properties)
|
|
760
|
-
if (
|
|
1112
|
+
if (property.type === "ObjectProperty" && literals(property.value).some((literal) => RAW_COLOR_VALUE.test(literal)))
|
|
761
1113
|
report(attribute, {
|
|
762
1114
|
code: "RAW_COLOR",
|
|
763
1115
|
severity: "warning",
|
|
@@ -765,56 +1117,73 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
765
1117
|
});
|
|
766
1118
|
}
|
|
767
1119
|
};
|
|
1120
|
+
const attributeName = (attribute) => attribute.name.type === "JSXIdentifier" ? attribute.name.name : textOf(attribute.name);
|
|
768
1121
|
const element = (opening, ancestors, parent, hasChildren, renderedAs) => {
|
|
769
|
-
const tag = opening.
|
|
1122
|
+
const tag = opening.name;
|
|
770
1123
|
let component = null;
|
|
771
|
-
if (
|
|
772
|
-
component = named.get(tag.
|
|
773
|
-
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.
|
|
1124
|
+
if (tag.type === "JSXIdentifier") {
|
|
1125
|
+
component = named.get(tag.name) ?? null;
|
|
1126
|
+
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.name))
|
|
774
1127
|
report(opening, {
|
|
775
1128
|
code: "NATIVE_CONTROL",
|
|
776
1129
|
severity: "warning",
|
|
777
|
-
message: `A native <${tag.
|
|
1130
|
+
message: `A native <${tag.name}>: use ${NATIVE[tag.name]}.`
|
|
778
1131
|
});
|
|
779
|
-
} else if (
|
|
780
|
-
const owner = tag.
|
|
781
|
-
|
|
1132
|
+
} else if (tag.type === "JSXMemberExpression" && tag.object.type === "JSXIdentifier") {
|
|
1133
|
+
const owner = tag.object.name;
|
|
1134
|
+
const member = tag.property.name;
|
|
1135
|
+
if (namespaces.has(owner) && isComponent(member)) component = member;
|
|
782
1136
|
else if (named.has(owner)) {
|
|
783
|
-
const joined = `${named.get(owner)}${
|
|
1137
|
+
const joined = `${named.get(owner)}${member}`;
|
|
784
1138
|
report(opening, {
|
|
785
1139
|
code: "DOT_NOTATION",
|
|
786
1140
|
severity: "error",
|
|
787
|
-
message: `${owner}.${
|
|
1141
|
+
message: `${owner}.${member} is not a component.` + (isComponent(joined) ? ` Use <${joined}>.` : " OPERATOR's parts are components of their own, imported by name.")
|
|
788
1142
|
});
|
|
789
1143
|
}
|
|
790
1144
|
}
|
|
791
|
-
const attributes = opening.attributes
|
|
1145
|
+
const attributes = opening.attributes;
|
|
792
1146
|
for (const attribute of attributes)
|
|
793
|
-
if (
|
|
794
|
-
houseRules(attribute, attribute.name.
|
|
1147
|
+
if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier")
|
|
1148
|
+
houseRules(attribute, attribute.name.name);
|
|
795
1149
|
if (component === null) return null;
|
|
796
1150
|
const definition = contract2.components[component];
|
|
797
|
-
const spread = attributes.some((attribute) =>
|
|
1151
|
+
const spread = attributes.some((attribute) => attribute.type === "JSXSpreadAttribute");
|
|
1152
|
+
for (const attribute of attributes)
|
|
1153
|
+
if (attribute.type === "JSXSpreadAttribute")
|
|
1154
|
+
limit(attribute, {
|
|
1155
|
+
code: "UNRESOLVED_SPREAD",
|
|
1156
|
+
component,
|
|
1157
|
+
message: `Props spread into <${component}> (${quote(textOf(attribute))}) were not read: option values and required props that arrive through them were not checked.`
|
|
1158
|
+
});
|
|
798
1159
|
const given = /* @__PURE__ */ new Set();
|
|
799
1160
|
for (const attribute of attributes) {
|
|
800
|
-
if (
|
|
801
|
-
const prop =
|
|
1161
|
+
if (attribute.type !== "JSXAttribute") continue;
|
|
1162
|
+
const prop = attributeName(attribute);
|
|
802
1163
|
given.add(prop);
|
|
803
1164
|
const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
|
|
804
1165
|
if (known === void 0) {
|
|
805
1166
|
if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
|
|
806
|
-
const
|
|
1167
|
+
const names2 = Object.keys(definition.props);
|
|
807
1168
|
report(attribute, {
|
|
808
1169
|
code: "UNKNOWN_PROP",
|
|
809
1170
|
severity: "warning",
|
|
810
1171
|
component,
|
|
811
1172
|
prop,
|
|
812
|
-
message: `${component} has no prop ${prop}.` + (
|
|
1173
|
+
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
1174
|
});
|
|
814
1175
|
continue;
|
|
815
1176
|
}
|
|
816
1177
|
if (known.type !== "enum" || known.values === void 0) continue;
|
|
817
|
-
|
|
1178
|
+
const { literals: options, unread } = optionValue(valueOf(attribute));
|
|
1179
|
+
for (const expression of unread)
|
|
1180
|
+
limit(attribute, {
|
|
1181
|
+
code: "UNRESOLVED_VALUE",
|
|
1182
|
+
component,
|
|
1183
|
+
prop,
|
|
1184
|
+
message: `${component} ${prop} is set from an expression (${quote(textOf(expression))}) whose value is not in the source, so it was not compared with the options.`
|
|
1185
|
+
});
|
|
1186
|
+
for (const literal of options) {
|
|
818
1187
|
if (known.values.includes(literal)) continue;
|
|
819
1188
|
const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
|
|
820
1189
|
report(attribute, {
|
|
@@ -861,82 +1230,146 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
861
1230
|
return component;
|
|
862
1231
|
};
|
|
863
1232
|
const visit = (node, ancestors, parent, renderedAs = false) => {
|
|
864
|
-
if (
|
|
865
|
-
const opening =
|
|
866
|
-
const hasChildren =
|
|
867
|
-
(child) => !(
|
|
1233
|
+
if (node.type === "JSXElement") {
|
|
1234
|
+
const opening = node.openingElement;
|
|
1235
|
+
const hasChildren = node.children.some(
|
|
1236
|
+
(child) => !(child.type === "JSXText" && /^\s*$/.test(text2.slice(child.start ?? 0, child.end ?? 0)))
|
|
868
1237
|
);
|
|
869
1238
|
const component = element(opening, ancestors, parent, hasChildren, renderedAs);
|
|
870
|
-
const asChild = component !== null && opening.attributes.
|
|
871
|
-
(attribute) =>
|
|
1239
|
+
const asChild = component !== null && opening.attributes.some(
|
|
1240
|
+
(attribute) => attribute.type === "JSXAttribute" && attributeName(attribute) === "asChild"
|
|
872
1241
|
);
|
|
873
|
-
for (const attribute of opening.attributes
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
);
|
|
1242
|
+
for (const attribute of opening.attributes) visit(attribute, [], null);
|
|
1243
|
+
for (const child of node.children)
|
|
1244
|
+
visit(
|
|
1245
|
+
child,
|
|
1246
|
+
component === null ? ancestors : [...ancestors, component],
|
|
1247
|
+
component,
|
|
1248
|
+
asChild
|
|
1249
|
+
);
|
|
882
1250
|
return;
|
|
883
1251
|
}
|
|
884
|
-
|
|
1252
|
+
for (const child of childNodes(node)) visit(child, ancestors, parent);
|
|
1253
|
+
};
|
|
1254
|
+
visit(reading.tree.program, [], null);
|
|
1255
|
+
const byPlace2 = (a, b) => a.line - b.line || a.column - b.column;
|
|
1256
|
+
return {
|
|
1257
|
+
file,
|
|
1258
|
+
status: limitations.length > 0 ? "partial" : "evaluated",
|
|
1259
|
+
findings: findings.sort(byPlace2),
|
|
1260
|
+
limitations: limitations.sort(byPlace2)
|
|
885
1261
|
};
|
|
886
|
-
visit(source, [], null);
|
|
887
|
-
return findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
888
1262
|
}
|
|
889
1263
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
890
1264
|
var READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
891
1265
|
var NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
1266
|
+
var excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
|
|
892
1267
|
function* sources(path) {
|
|
893
|
-
|
|
894
|
-
if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
|
|
1268
|
+
if (excluded((0, import_node_path3.basename)(path))) return;
|
|
895
1269
|
if ((0, import_node_fs3.statSync)(path).isDirectory()) {
|
|
896
1270
|
for (const entry of (0, import_node_fs3.readdirSync)(path).sort()) yield* sources((0, import_node_path3.join)(path, entry));
|
|
897
|
-
} else if (READ.has((0, import_node_path3.extname)(path)) && !NOT_INTERFACE.test(
|
|
1271
|
+
} else if (READ.has((0, import_node_path3.extname)(path)) && !NOT_INTERFACE.test((0, import_node_path3.basename)(path))) yield path;
|
|
898
1272
|
}
|
|
899
|
-
function
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1273
|
+
function whyNotRead(path) {
|
|
1274
|
+
const name = (0, import_node_path3.basename)(path);
|
|
1275
|
+
if (excluded(name)) return "excluded_folder";
|
|
1276
|
+
if ((0, import_node_fs3.statSync)(path).isDirectory()) return null;
|
|
1277
|
+
if (!READ.has((0, import_node_path3.extname)(path))) return "unsupported_extension";
|
|
1278
|
+
return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
|
|
1279
|
+
}
|
|
1280
|
+
function buildCheckReport(evaluations, context = {}) {
|
|
1281
|
+
const findings = evaluations.flatMap(({ findings: found }) => found);
|
|
1282
|
+
const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
|
|
1283
|
+
const partial = count("partial");
|
|
1284
|
+
const notEvaluated = count("not_evaluated");
|
|
1285
|
+
const evaluated = evaluations.length - notEvaluated;
|
|
1286
|
+
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1287
|
+
const warnings = findings.length - errors;
|
|
1288
|
+
let status;
|
|
1289
|
+
if (evaluations.length === 0) status = "no_files_evaluated";
|
|
1290
|
+
else if (evaluated === 0) status = "not_evaluated";
|
|
1291
|
+
else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
|
|
1292
|
+
else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
|
|
1293
|
+
return {
|
|
1294
|
+
evaluation_status: status,
|
|
1295
|
+
files: evaluations.length,
|
|
1296
|
+
findings,
|
|
1297
|
+
summary: { errors, warnings },
|
|
1298
|
+
coverage: {
|
|
1299
|
+
files_found: evaluations.length,
|
|
1300
|
+
files_evaluated: evaluated,
|
|
1301
|
+
files_partial: partial,
|
|
1302
|
+
files_not_evaluated: notEvaluated
|
|
1303
|
+
},
|
|
1304
|
+
limitations: evaluations.flatMap(({ limitations }) => limitations),
|
|
1305
|
+
skipped_inputs: context.skipped ?? [],
|
|
1306
|
+
scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
|
|
1307
|
+
parser: context.parser ?? PARSER
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function checkExitCode(report, { strict }) {
|
|
1311
|
+
if (report.evaluation_status === "no_files_evaluated") return 1;
|
|
1312
|
+
if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
|
|
1313
|
+
return strict && report.summary.warnings > 0 ? 1 : 0;
|
|
907
1314
|
}
|
|
908
1315
|
function checkPaths(paths, context) {
|
|
909
1316
|
const root = (0, import_node_path3.resolve)(context.cwd);
|
|
910
1317
|
const files = [];
|
|
1318
|
+
const skipped = [];
|
|
911
1319
|
for (const path of paths) {
|
|
912
1320
|
const absolute = (0, import_node_path3.resolve)(root, path);
|
|
913
1321
|
if (!(0, import_node_fs3.existsSync)(absolute))
|
|
914
1322
|
throw new CliError(`No such file or folder: ${path}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
1323
|
+
const reason = whyNotRead(absolute);
|
|
1324
|
+
if (reason !== null) skipped.push({ path, reason });
|
|
915
1325
|
files.push(...sources(absolute));
|
|
916
1326
|
}
|
|
917
|
-
|
|
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(
|
|
1327
|
+
loadBabel();
|
|
1328
|
+
const evaluations = [...new Set(files)].map(
|
|
925
1329
|
(file) => checkSource(
|
|
926
|
-
ts,
|
|
927
1330
|
context.contract,
|
|
928
1331
|
(0, import_node_path3.relative)(root, file).split(import_node_path3.sep).join("/"),
|
|
929
1332
|
(0, import_node_fs3.readFileSync)(file, "utf8")
|
|
930
1333
|
)
|
|
931
1334
|
);
|
|
932
|
-
return {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
1335
|
+
return buildCheckReport(evaluations, { skipped });
|
|
1336
|
+
}
|
|
1337
|
+
var plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1338
|
+
var MAX_LIMITS_SHOWN = 20;
|
|
1339
|
+
function renderCheckText(report) {
|
|
1340
|
+
const lines = report.findings.map(
|
|
1341
|
+
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1342
|
+
);
|
|
1343
|
+
const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
|
|
1344
|
+
for (const { file, line, column, code, message } of shown)
|
|
1345
|
+
lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
|
|
1346
|
+
if (report.limitations.length > shown.length)
|
|
1347
|
+
lines.push(
|
|
1348
|
+
` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
|
|
1349
|
+
);
|
|
1350
|
+
const { coverage, evaluation_status: status } = report;
|
|
1351
|
+
if (status === "no_files_evaluated") {
|
|
1352
|
+
lines.push("No .tsx or .jsx file was read, so nothing was checked.");
|
|
1353
|
+
const labels = {
|
|
1354
|
+
unsupported_extension: "not a .tsx or .jsx file",
|
|
1355
|
+
test_or_story_file: "a test, spec or story file, which is skipped",
|
|
1356
|
+
excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
|
|
1357
|
+
};
|
|
1358
|
+
for (const { path, reason } of report.skipped_inputs)
|
|
1359
|
+
lines.push(` ${path}: ${labels[reason]}`);
|
|
1360
|
+
} else if (coverage.files_not_evaluated > 0)
|
|
1361
|
+
lines.push(
|
|
1362
|
+
`${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
|
|
1363
|
+
);
|
|
1364
|
+
else if (status === "evaluated_clean")
|
|
1365
|
+
lines.push(
|
|
1366
|
+
"Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
|
|
1367
|
+
);
|
|
1368
|
+
lines.push(
|
|
1369
|
+
`${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
|
|
1370
|
+
);
|
|
1371
|
+
return `${lines.join("\n")}
|
|
1372
|
+
`;
|
|
940
1373
|
}
|
|
941
1374
|
|
|
942
1375
|
// src/doctor.ts
|
|
@@ -1023,7 +1456,7 @@ var INSTRUCTION_FILES = [
|
|
|
1023
1456
|
".claude/CLAUDE.md",
|
|
1024
1457
|
".cursor/rules/operator.mdc"
|
|
1025
1458
|
];
|
|
1026
|
-
var minor = (
|
|
1459
|
+
var minor = (version2) => version2.split(".").slice(0, 2).join(".");
|
|
1027
1460
|
function installedVersion(cwd, name) {
|
|
1028
1461
|
for (let folder = cwd; ; folder = (0, import_node_path4.dirname)(folder)) {
|
|
1029
1462
|
const manifest = (0, import_node_path4.join)(folder, "node_modules", name, "package.json");
|
|
@@ -1032,26 +1465,30 @@ function installedVersion(cwd, name) {
|
|
|
1032
1465
|
if ((0, import_node_path4.dirname)(folder) === folder) return void 0;
|
|
1033
1466
|
}
|
|
1034
1467
|
}
|
|
1035
|
-
function doctor({
|
|
1468
|
+
function doctor({
|
|
1469
|
+
cwd,
|
|
1470
|
+
contract: contract2,
|
|
1471
|
+
parser = probeParser()
|
|
1472
|
+
}) {
|
|
1036
1473
|
const checks = [];
|
|
1037
1474
|
for (const name of PACKAGES) {
|
|
1038
|
-
const
|
|
1475
|
+
const version2 = installedVersion(cwd, name);
|
|
1039
1476
|
const id = `package:${name}`;
|
|
1040
|
-
if (
|
|
1477
|
+
if (version2 === void 0)
|
|
1041
1478
|
checks.push({
|
|
1042
1479
|
id,
|
|
1043
1480
|
status: "warn",
|
|
1044
1481
|
message: `${name} is not installed.`,
|
|
1045
1482
|
fix: `Install ${name}, or copy the sources you need with oprt add.`
|
|
1046
1483
|
});
|
|
1047
|
-
else if (minor(
|
|
1484
|
+
else if (minor(version2) !== minor(contract2.version))
|
|
1048
1485
|
checks.push({
|
|
1049
1486
|
id,
|
|
1050
1487
|
status: "warn",
|
|
1051
|
-
message: `${name} is ${
|
|
1488
|
+
message: `${name} is ${version2}; this installer describes ${contract2.version}.`,
|
|
1052
1489
|
fix: "Install matching versions of the OPERATOR packages and @oprt/cli."
|
|
1053
1490
|
});
|
|
1054
|
-
else checks.push({ id, status: "ok", message: `${name} ${
|
|
1491
|
+
else checks.push({ id, status: "ok", message: `${name} ${version2} is installed.` });
|
|
1055
1492
|
}
|
|
1056
1493
|
const written = INSTRUCTION_FILES.map((path) => {
|
|
1057
1494
|
const file = (0, import_node_path4.join)(cwd, path);
|
|
@@ -1059,8 +1496,8 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1059
1496
|
path,
|
|
1060
1497
|
version: (0, import_node_fs4.existsSync)(file) ? blockVersion((0, import_node_fs4.readFileSync)(file, "utf8")) : void 0
|
|
1061
1498
|
};
|
|
1062
|
-
}).filter(({ version }) =>
|
|
1063
|
-
const stale = written.find(({ version }) =>
|
|
1499
|
+
}).filter(({ version: version2 }) => version2 !== void 0);
|
|
1500
|
+
const stale = written.find(({ version: version2 }) => version2 !== contract2.version);
|
|
1064
1501
|
if (written.length === 0)
|
|
1065
1502
|
checks.push({
|
|
1066
1503
|
id: "agent-instructions",
|
|
@@ -1081,8 +1518,48 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1081
1518
|
status: "ok",
|
|
1082
1519
|
message: `${written.map(({ path }) => path).join(", ")} carries the OPERATOR block (${contract2.version}).`
|
|
1083
1520
|
});
|
|
1521
|
+
let checkCode;
|
|
1522
|
+
if (parser.ready) {
|
|
1523
|
+
const { name, version: version2 } = parser.parser;
|
|
1524
|
+
checkCode = {
|
|
1525
|
+
status: "ready",
|
|
1526
|
+
message: `${name} ${version2}, bundled with oprt: nothing to install.`,
|
|
1527
|
+
parser: parser.parser
|
|
1528
|
+
};
|
|
1529
|
+
checks.push({
|
|
1530
|
+
id: "parser",
|
|
1531
|
+
status: "ok",
|
|
1532
|
+
message: `Code checking reads code with ${name} ${version2}, which ships inside oprt.`
|
|
1533
|
+
});
|
|
1534
|
+
} else {
|
|
1535
|
+
const fix = "Install @oprt/cli again, then run oprt doctor again. Lookups work without it.";
|
|
1536
|
+
checkCode = {
|
|
1537
|
+
status: "blocked",
|
|
1538
|
+
message: `${parser.reason}: ${parser.detail}`,
|
|
1539
|
+
reason: parser.reason,
|
|
1540
|
+
fix
|
|
1541
|
+
};
|
|
1542
|
+
checks.push({
|
|
1543
|
+
id: "parser",
|
|
1544
|
+
status: "fail",
|
|
1545
|
+
message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
|
|
1546
|
+
fix
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1084
1549
|
const count = (status) => checks.filter((check) => check.status === status).length;
|
|
1085
|
-
return {
|
|
1550
|
+
return {
|
|
1551
|
+
consumer_directory: cwd,
|
|
1552
|
+
operations: {
|
|
1553
|
+
lookup: {
|
|
1554
|
+
status: "ready",
|
|
1555
|
+
message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
|
|
1556
|
+
contract_version: contract2.version
|
|
1557
|
+
},
|
|
1558
|
+
check_code: checkCode
|
|
1559
|
+
},
|
|
1560
|
+
checks,
|
|
1561
|
+
summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
|
|
1562
|
+
};
|
|
1086
1563
|
}
|
|
1087
1564
|
|
|
1088
1565
|
// src/init.ts
|
|
@@ -1140,8 +1617,532 @@ function init(options, context) {
|
|
|
1140
1617
|
return { version: context.contract.version, dryRun, files };
|
|
1141
1618
|
}
|
|
1142
1619
|
|
|
1620
|
+
// src/results.ts
|
|
1621
|
+
var RESULT_TYPES = [
|
|
1622
|
+
"check",
|
|
1623
|
+
"search",
|
|
1624
|
+
"component.detail",
|
|
1625
|
+
"component.batch",
|
|
1626
|
+
"component.list",
|
|
1627
|
+
"init",
|
|
1628
|
+
"doctor",
|
|
1629
|
+
"add",
|
|
1630
|
+
"list",
|
|
1631
|
+
"instructions"
|
|
1632
|
+
];
|
|
1633
|
+
var envelope = (type, data, contract2) => ({
|
|
1634
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1635
|
+
type,
|
|
1636
|
+
identity: identityOf(contract2),
|
|
1637
|
+
data
|
|
1638
|
+
});
|
|
1639
|
+
function errorEnvelope(error, contract2) {
|
|
1640
|
+
const known = error instanceof CliError ? error : void 0;
|
|
1641
|
+
const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
|
|
1642
|
+
cli_version,
|
|
1643
|
+
schema_version,
|
|
1644
|
+
contract_version,
|
|
1645
|
+
contract_digest
|
|
1646
|
+
}))(identityOf(contract2));
|
|
1647
|
+
return {
|
|
1648
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1649
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1650
|
+
code: known?.code ?? "ERR_FAILED",
|
|
1651
|
+
...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
|
|
1652
|
+
identity
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function componentData(detail, dense) {
|
|
1656
|
+
if (!dense) return { ...detail, dense };
|
|
1657
|
+
const { description: _description, ...rest } = detail;
|
|
1658
|
+
return {
|
|
1659
|
+
...rest,
|
|
1660
|
+
props: detail.props.map(
|
|
1661
|
+
(prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
|
|
1662
|
+
),
|
|
1663
|
+
dense
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function instructionsData(contract2) {
|
|
1667
|
+
return {
|
|
1668
|
+
rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
|
|
1669
|
+
id,
|
|
1670
|
+
rule,
|
|
1671
|
+
...wrong === void 0 ? {} : { wrong },
|
|
1672
|
+
...right === void 0 ? {} : { right }
|
|
1673
|
+
})),
|
|
1674
|
+
components: componentIndex(contract2),
|
|
1675
|
+
recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
|
|
1676
|
+
name,
|
|
1677
|
+
description,
|
|
1678
|
+
install: install2
|
|
1679
|
+
}))
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
var str = (extra = {}) => ({ type: "string", ...extra });
|
|
1683
|
+
var int = () => ({ type: "integer", minimum: 0 });
|
|
1684
|
+
var bool = () => ({ type: "boolean" });
|
|
1685
|
+
var arr = (items) => ({ type: "array", items });
|
|
1686
|
+
var names = () => arr(str());
|
|
1687
|
+
var words2 = (...values) => ({ type: "string", enum: [...values] });
|
|
1688
|
+
var ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
1689
|
+
var obj = (properties, optional = [], open = false) => ({
|
|
1690
|
+
type: "object",
|
|
1691
|
+
properties,
|
|
1692
|
+
required: Object.keys(properties).filter((key) => !optional.includes(key)),
|
|
1693
|
+
additionalProperties: open
|
|
1694
|
+
});
|
|
1695
|
+
var scalar = { type: ["string", "number", "boolean"] };
|
|
1696
|
+
var DEFS = {
|
|
1697
|
+
identity: obj({
|
|
1698
|
+
cli_version: str(),
|
|
1699
|
+
contract_version: str(),
|
|
1700
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
|
|
1701
|
+
schema_version: { type: "integer", minimum: 1 }
|
|
1702
|
+
}),
|
|
1703
|
+
error_identity: obj(
|
|
1704
|
+
{
|
|
1705
|
+
cli_version: str(),
|
|
1706
|
+
schema_version: { type: "integer", minimum: 1 },
|
|
1707
|
+
contract_version: str(),
|
|
1708
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
|
|
1709
|
+
},
|
|
1710
|
+
["contract_version", "contract_digest"]
|
|
1711
|
+
),
|
|
1712
|
+
finding: obj(
|
|
1713
|
+
{
|
|
1714
|
+
code: words2(...FINDING_CODES),
|
|
1715
|
+
severity: words2("error", "warning"),
|
|
1716
|
+
file: str(),
|
|
1717
|
+
line: { type: "integer", minimum: 1 },
|
|
1718
|
+
column: { type: "integer", minimum: 1 },
|
|
1719
|
+
component: str(),
|
|
1720
|
+
prop: str(),
|
|
1721
|
+
message: str(),
|
|
1722
|
+
valid_values: names(),
|
|
1723
|
+
suggestions: names()
|
|
1724
|
+
},
|
|
1725
|
+
["component", "prop", "valid_values", "suggestions"]
|
|
1726
|
+
),
|
|
1727
|
+
limitation: obj(
|
|
1728
|
+
{
|
|
1729
|
+
code: words2(...LIMITATION_CODES),
|
|
1730
|
+
file: str(),
|
|
1731
|
+
line: { type: "integer", minimum: 1 },
|
|
1732
|
+
column: { type: "integer", minimum: 1 },
|
|
1733
|
+
component: str(),
|
|
1734
|
+
prop: str(),
|
|
1735
|
+
message: str()
|
|
1736
|
+
},
|
|
1737
|
+
["prop"]
|
|
1738
|
+
),
|
|
1739
|
+
skipped_input: obj({
|
|
1740
|
+
path: str(),
|
|
1741
|
+
reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
|
|
1742
|
+
}),
|
|
1743
|
+
parser: obj({
|
|
1744
|
+
name: words2("@babel/parser"),
|
|
1745
|
+
version: str(),
|
|
1746
|
+
source: words2("bundled")
|
|
1747
|
+
}),
|
|
1748
|
+
check_data: obj({
|
|
1749
|
+
evaluation_status: words2(
|
|
1750
|
+
"evaluated_clean",
|
|
1751
|
+
"evaluated_with_findings",
|
|
1752
|
+
"partially_evaluated",
|
|
1753
|
+
"not_evaluated",
|
|
1754
|
+
"no_files_evaluated"
|
|
1755
|
+
),
|
|
1756
|
+
files: int(),
|
|
1757
|
+
findings: arr(ref("finding")),
|
|
1758
|
+
summary: obj({ errors: int(), warnings: int() }),
|
|
1759
|
+
coverage: obj({
|
|
1760
|
+
files_found: int(),
|
|
1761
|
+
files_evaluated: int(),
|
|
1762
|
+
files_partial: int(),
|
|
1763
|
+
files_not_evaluated: int()
|
|
1764
|
+
}),
|
|
1765
|
+
limitations: arr(ref("limitation")),
|
|
1766
|
+
skipped_inputs: arr(ref("skipped_input")),
|
|
1767
|
+
scope: obj({ checked: names(), not_checked: names() }),
|
|
1768
|
+
parser: ref("parser")
|
|
1769
|
+
}),
|
|
1770
|
+
search_result: obj(
|
|
1771
|
+
{
|
|
1772
|
+
kind: words2("component", "recipe"),
|
|
1773
|
+
name: str(),
|
|
1774
|
+
purpose: str(),
|
|
1775
|
+
score: { type: "number" },
|
|
1776
|
+
why: str(),
|
|
1777
|
+
next: str()
|
|
1778
|
+
},
|
|
1779
|
+
["why"]
|
|
1780
|
+
),
|
|
1781
|
+
search_data: obj({
|
|
1782
|
+
query: str(),
|
|
1783
|
+
limit: { type: "integer", minimum: 1 },
|
|
1784
|
+
matchCount: int(),
|
|
1785
|
+
results: arr(ref("search_result"))
|
|
1786
|
+
}),
|
|
1787
|
+
index_entry: obj({ name: str(), purpose: str(), parts: names() }),
|
|
1788
|
+
component_list_data: obj({ components: arr(ref("index_entry")) }),
|
|
1789
|
+
detail_prop: obj(
|
|
1790
|
+
{
|
|
1791
|
+
name: str(),
|
|
1792
|
+
type: str(),
|
|
1793
|
+
values: names(),
|
|
1794
|
+
types: names(),
|
|
1795
|
+
default: scalar,
|
|
1796
|
+
required: bool(),
|
|
1797
|
+
description: str(),
|
|
1798
|
+
notes: { type: "object", additionalProperties: str() }
|
|
1799
|
+
},
|
|
1800
|
+
["values", "types", "default", "required", "description", "notes"],
|
|
1801
|
+
// The contract may describe a prop with more than these (an array's item type, for one).
|
|
1802
|
+
true
|
|
1803
|
+
),
|
|
1804
|
+
component_detail: obj(
|
|
1805
|
+
{
|
|
1806
|
+
name: str(),
|
|
1807
|
+
description: str(),
|
|
1808
|
+
use_for: names(),
|
|
1809
|
+
not_for: arr(obj({ need: str(), use: str() })),
|
|
1810
|
+
part_of: str(),
|
|
1811
|
+
parts: names(),
|
|
1812
|
+
props: arr(ref("detail_prop")),
|
|
1813
|
+
slots: {
|
|
1814
|
+
type: "object",
|
|
1815
|
+
additionalProperties: obj({ type: str(), required: bool() }, ["required"])
|
|
1816
|
+
},
|
|
1817
|
+
composable_with: names(),
|
|
1818
|
+
never_inside: names(),
|
|
1819
|
+
allowed_children: names(),
|
|
1820
|
+
example: str(),
|
|
1821
|
+
dense: bool()
|
|
1822
|
+
},
|
|
1823
|
+
["description", "part_of", "slots", "allowed_children"]
|
|
1824
|
+
),
|
|
1825
|
+
component_batch_data: obj({ components: arr(ref("component_detail")) }),
|
|
1826
|
+
doctor_check: obj(
|
|
1827
|
+
{ id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
|
|
1828
|
+
["fix"]
|
|
1829
|
+
),
|
|
1830
|
+
doctor_data: obj({
|
|
1831
|
+
consumer_directory: str(),
|
|
1832
|
+
operations: obj({
|
|
1833
|
+
lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
|
|
1834
|
+
check_code: obj(
|
|
1835
|
+
{
|
|
1836
|
+
status: words2("ready", "blocked"),
|
|
1837
|
+
message: str(),
|
|
1838
|
+
reason: words2("parser_missing", "parser_unusable"),
|
|
1839
|
+
fix: str(),
|
|
1840
|
+
parser: ref("parser")
|
|
1841
|
+
},
|
|
1842
|
+
["reason", "fix", "parser"]
|
|
1843
|
+
)
|
|
1844
|
+
}),
|
|
1845
|
+
checks: arr(ref("doctor_check")),
|
|
1846
|
+
summary: obj({ ok: int(), warn: int(), fail: int() })
|
|
1847
|
+
}),
|
|
1848
|
+
init_data: obj({
|
|
1849
|
+
version: str(),
|
|
1850
|
+
dryRun: bool(),
|
|
1851
|
+
files: arr(
|
|
1852
|
+
obj({
|
|
1853
|
+
path: str(),
|
|
1854
|
+
action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
|
|
1855
|
+
})
|
|
1856
|
+
)
|
|
1857
|
+
}),
|
|
1858
|
+
install_file: obj({
|
|
1859
|
+
path: str(),
|
|
1860
|
+
destination: str(),
|
|
1861
|
+
action: words2("create", "overwrite", "skip")
|
|
1862
|
+
}),
|
|
1863
|
+
add_data: obj({
|
|
1864
|
+
outputDirectory: str(),
|
|
1865
|
+
entries: names(),
|
|
1866
|
+
dependencies: names(),
|
|
1867
|
+
devDependencies: names(),
|
|
1868
|
+
ranges: { type: "object", additionalProperties: str() },
|
|
1869
|
+
files: arr(ref("install_file")),
|
|
1870
|
+
dryRun: bool(),
|
|
1871
|
+
setup: ref("setup")
|
|
1872
|
+
}),
|
|
1873
|
+
setup: obj({
|
|
1874
|
+
tailwind: str(),
|
|
1875
|
+
packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
|
|
1876
|
+
stylesheet: arr(obj({ line: str(), why: str() })),
|
|
1877
|
+
root_import: obj({ line: str(), note: str() }),
|
|
1878
|
+
not_needed: arr(obj({ name: str(), why: str() }))
|
|
1879
|
+
}),
|
|
1880
|
+
list_data: obj({
|
|
1881
|
+
version: str(),
|
|
1882
|
+
entries: arr(
|
|
1883
|
+
obj(
|
|
1884
|
+
{
|
|
1885
|
+
name: str(),
|
|
1886
|
+
slug: str(),
|
|
1887
|
+
kind: words2("component", "recipe"),
|
|
1888
|
+
description: str(),
|
|
1889
|
+
files: names(),
|
|
1890
|
+
dependencies: names(),
|
|
1891
|
+
devDependencies: names()
|
|
1892
|
+
},
|
|
1893
|
+
["devDependencies"]
|
|
1894
|
+
)
|
|
1895
|
+
),
|
|
1896
|
+
ranges: { type: "object", additionalProperties: str() }
|
|
1897
|
+
}),
|
|
1898
|
+
instructions_data: obj({
|
|
1899
|
+
rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
|
|
1900
|
+
components: arr(ref("index_entry")),
|
|
1901
|
+
recipes: arr(obj({ name: str(), description: str(), install: str() }))
|
|
1902
|
+
})
|
|
1903
|
+
};
|
|
1904
|
+
var DATA = {
|
|
1905
|
+
check: "check_data",
|
|
1906
|
+
search: "search_data",
|
|
1907
|
+
"component.detail": "component_detail",
|
|
1908
|
+
"component.batch": "component_batch_data",
|
|
1909
|
+
"component.list": "component_list_data",
|
|
1910
|
+
init: "init_data",
|
|
1911
|
+
doctor: "doctor_data",
|
|
1912
|
+
add: "add_data",
|
|
1913
|
+
list: "list_data",
|
|
1914
|
+
instructions: "instructions_data"
|
|
1915
|
+
};
|
|
1916
|
+
var success = (type) => obj({
|
|
1917
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1918
|
+
type: { const: type },
|
|
1919
|
+
identity: ref("identity"),
|
|
1920
|
+
data: ref(DATA[type])
|
|
1921
|
+
});
|
|
1922
|
+
var failure = () => obj(
|
|
1923
|
+
{
|
|
1924
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1925
|
+
error: str(),
|
|
1926
|
+
code: str({ pattern: "^ERR_[A-Z_]+$" }),
|
|
1927
|
+
suggestions: names(),
|
|
1928
|
+
identity: ref("error_identity")
|
|
1929
|
+
},
|
|
1930
|
+
["suggestions"]
|
|
1931
|
+
);
|
|
1932
|
+
function bundle(root) {
|
|
1933
|
+
const used = {};
|
|
1934
|
+
const visit = (node) => {
|
|
1935
|
+
if (Array.isArray(node)) node.forEach(visit);
|
|
1936
|
+
else if (typeof node === "object" && node !== null) {
|
|
1937
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1938
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1939
|
+
const name = value.replace("#/$defs/", "");
|
|
1940
|
+
if (used[name] === void 0) {
|
|
1941
|
+
used[name] = DEFS[name];
|
|
1942
|
+
visit(DEFS[name]);
|
|
1943
|
+
}
|
|
1944
|
+
} else visit(value);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
visit(root);
|
|
1949
|
+
return { ...root, $defs: used };
|
|
1950
|
+
}
|
|
1951
|
+
function schemaFor(type) {
|
|
1952
|
+
return bundle(type === "error" ? failure() : success(type));
|
|
1953
|
+
}
|
|
1954
|
+
var S = { type: "string" };
|
|
1955
|
+
var I = { type: "integer" };
|
|
1956
|
+
var B = { type: "boolean" };
|
|
1957
|
+
var list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
|
|
1958
|
+
var shape = (properties, required) => ({
|
|
1959
|
+
type: "object",
|
|
1960
|
+
properties,
|
|
1961
|
+
...required === void 0 ? {} : { required }
|
|
1962
|
+
});
|
|
1963
|
+
var strings = list(S);
|
|
1964
|
+
var SUMMARIES = {
|
|
1965
|
+
check: shape(
|
|
1966
|
+
{
|
|
1967
|
+
evaluation_status: words2(
|
|
1968
|
+
"evaluated_clean",
|
|
1969
|
+
"evaluated_with_findings",
|
|
1970
|
+
"partially_evaluated",
|
|
1971
|
+
"not_evaluated",
|
|
1972
|
+
"no_files_evaluated"
|
|
1973
|
+
),
|
|
1974
|
+
findings: list(
|
|
1975
|
+
shape({
|
|
1976
|
+
code: S,
|
|
1977
|
+
severity: words2("error", "warning"),
|
|
1978
|
+
file: S,
|
|
1979
|
+
line: I,
|
|
1980
|
+
column: I,
|
|
1981
|
+
message: S
|
|
1982
|
+
})
|
|
1983
|
+
),
|
|
1984
|
+
summary: shape({ errors: I, warnings: I }),
|
|
1985
|
+
coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
|
|
1986
|
+
limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
|
|
1987
|
+
skipped_inputs: list(),
|
|
1988
|
+
scope: { type: "object" },
|
|
1989
|
+
parser: { type: "object" }
|
|
1990
|
+
},
|
|
1991
|
+
["evaluation_status", "findings", "summary", "coverage", "limitations"]
|
|
1992
|
+
),
|
|
1993
|
+
search: shape(
|
|
1994
|
+
{
|
|
1995
|
+
matchCount: I,
|
|
1996
|
+
results: list(
|
|
1997
|
+
shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
|
|
1998
|
+
)
|
|
1999
|
+
},
|
|
2000
|
+
["results"]
|
|
2001
|
+
),
|
|
2002
|
+
"component.detail": shape(
|
|
2003
|
+
{
|
|
2004
|
+
name: S,
|
|
2005
|
+
description: S,
|
|
2006
|
+
use_for: strings,
|
|
2007
|
+
not_for: list(),
|
|
2008
|
+
part_of: S,
|
|
2009
|
+
parts: strings,
|
|
2010
|
+
props: list(
|
|
2011
|
+
shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
|
|
2012
|
+
),
|
|
2013
|
+
slots: { type: "object" },
|
|
2014
|
+
composable_with: strings,
|
|
2015
|
+
never_inside: strings,
|
|
2016
|
+
allowed_children: strings,
|
|
2017
|
+
example: S,
|
|
2018
|
+
dense: B
|
|
2019
|
+
},
|
|
2020
|
+
["name", "props", "example", "dense"]
|
|
2021
|
+
),
|
|
2022
|
+
"component.list": shape({ components: list() }, ["components"]),
|
|
2023
|
+
"component.batch": shape({ components: list() }, ["components"]),
|
|
2024
|
+
instructions: shape(
|
|
2025
|
+
{
|
|
2026
|
+
rules: list(),
|
|
2027
|
+
components: list(),
|
|
2028
|
+
recipes: list()
|
|
2029
|
+
},
|
|
2030
|
+
["rules", "components", "recipes"]
|
|
2031
|
+
),
|
|
2032
|
+
init: shape({ files: list() }),
|
|
2033
|
+
doctor: shape({ operations: { type: "object" }, checks: list() }),
|
|
2034
|
+
add: shape({ files: list(), setup: { type: "object" } }),
|
|
2035
|
+
list: shape({ entries: list() })
|
|
2036
|
+
};
|
|
2037
|
+
function shareRepeats(schema) {
|
|
2038
|
+
const count = /* @__PURE__ */ new Map();
|
|
2039
|
+
const tally = (node) => {
|
|
2040
|
+
if (Array.isArray(node)) node.forEach(tally);
|
|
2041
|
+
else if (typeof node === "object" && node !== null) {
|
|
2042
|
+
const text2 = JSON.stringify(node);
|
|
2043
|
+
if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
|
|
2044
|
+
Object.values(node).forEach(tally);
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
tally(schema);
|
|
2048
|
+
const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
|
|
2049
|
+
if (repeated.length === 0) return schema;
|
|
2050
|
+
const names2 = new Map(
|
|
2051
|
+
repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
|
|
2052
|
+
);
|
|
2053
|
+
const defs = {};
|
|
2054
|
+
const rewrite = (node) => {
|
|
2055
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
2056
|
+
if (typeof node !== "object" || node === null) return node;
|
|
2057
|
+
const name = names2.get(JSON.stringify(node));
|
|
2058
|
+
if (name !== void 0) {
|
|
2059
|
+
defs[name] = node;
|
|
2060
|
+
return { $ref: `#/$defs/${name}` };
|
|
2061
|
+
}
|
|
2062
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
|
|
2063
|
+
};
|
|
2064
|
+
const rewritten = rewrite(schema);
|
|
2065
|
+
return { ...rewritten, $defs: defs };
|
|
2066
|
+
}
|
|
2067
|
+
function outputSchemaFor(type) {
|
|
2068
|
+
return shareRepeats({
|
|
2069
|
+
type: "object",
|
|
2070
|
+
properties: {
|
|
2071
|
+
type: { const: type },
|
|
2072
|
+
// The four versions and the digest; the docs name them, and so does every answer.
|
|
2073
|
+
identity: { type: "object" },
|
|
2074
|
+
data: SUMMARIES[type]
|
|
2075
|
+
},
|
|
2076
|
+
required: ["identity"],
|
|
2077
|
+
anyOf: [{ required: ["data"] }, { required: ["error"] }]
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
var RESULT_SCHEMAS = Object.freeze(
|
|
2081
|
+
Object.fromEntries(
|
|
2082
|
+
[...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
|
|
2086
|
+
// src/setup.ts
|
|
2087
|
+
function setupFor(dependencies, contract2) {
|
|
2088
|
+
const { setup } = contract2.guidance;
|
|
2089
|
+
const scansPackage = dependencies.includes(setup.package_source.when);
|
|
2090
|
+
return {
|
|
2091
|
+
tailwind: setup.tailwind,
|
|
2092
|
+
packages: setup.packages.map(({ name, range, dev, why }) => ({
|
|
2093
|
+
name,
|
|
2094
|
+
range: range ?? "latest",
|
|
2095
|
+
dev,
|
|
2096
|
+
why
|
|
2097
|
+
})),
|
|
2098
|
+
stylesheet: [
|
|
2099
|
+
...setup.stylesheet,
|
|
2100
|
+
...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
|
|
2101
|
+
],
|
|
2102
|
+
root_import: { line: setup.root_import, note: setup.root_import_note },
|
|
2103
|
+
not_needed: setup.not_needed
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function renderSetup(setup, copiedPackageImports) {
|
|
2107
|
+
const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
|
|
2108
|
+
const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
|
|
2109
|
+
const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
|
|
2110
|
+
return [
|
|
2111
|
+
`Setup, once per project: ${setup.tailwind}`,
|
|
2112
|
+
` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
|
|
2113
|
+
" In the stylesheet that imports tailwindcss, after the import:",
|
|
2114
|
+
...setup.stylesheet.map(({ line }) => ` ${line}`),
|
|
2115
|
+
...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
|
|
2116
|
+
" (source copied into the project is scanned by Tailwind 4 without an @source line)"
|
|
2117
|
+
],
|
|
2118
|
+
` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
|
|
2119
|
+
...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
|
|
2120
|
+
];
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1143
2123
|
// src/agent-commands.ts
|
|
1144
|
-
|
|
2124
|
+
function renderInstall(result, setup) {
|
|
2125
|
+
const lines = [
|
|
2126
|
+
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
|
|
2127
|
+
`Output: ${result.outputDirectory}`,
|
|
2128
|
+
...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
|
|
2129
|
+
];
|
|
2130
|
+
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
2131
|
+
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
2132
|
+
lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
|
|
2133
|
+
if (result.devDependencies.length > 0)
|
|
2134
|
+
lines.push(
|
|
2135
|
+
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
|
|
2136
|
+
);
|
|
2137
|
+
lines.push(
|
|
2138
|
+
"",
|
|
2139
|
+
...renderSetup(setup, setup.stylesheet.length > 1),
|
|
2140
|
+
"",
|
|
2141
|
+
"Install what is missing yourself. No package configuration or stylesheet was changed."
|
|
2142
|
+
);
|
|
2143
|
+
return `${lines.join("\n")}
|
|
2144
|
+
`;
|
|
2145
|
+
}
|
|
1145
2146
|
function runAgentCommand(command, context) {
|
|
1146
2147
|
const { contract: contract2, registry: registry2, cwd } = context;
|
|
1147
2148
|
switch (command.command) {
|
|
@@ -1153,14 +2154,15 @@ function runAgentCommand(command, context) {
|
|
|
1153
2154
|
({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
|
|
1154
2155
|
`
|
|
1155
2156
|
).join(""),
|
|
1156
|
-
json: envelope("component.list", { components }),
|
|
2157
|
+
json: envelope("component.list", { components }, contract2),
|
|
1157
2158
|
exitCode: 0
|
|
1158
2159
|
};
|
|
1159
2160
|
}
|
|
1160
2161
|
const details = command.names.map((name) => componentDetail(contract2, name));
|
|
2162
|
+
const data = details.map((detail) => componentData(detail, command.dense));
|
|
1161
2163
|
return {
|
|
1162
2164
|
text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
|
|
1163
|
-
json:
|
|
2165
|
+
json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
|
|
1164
2166
|
exitCode: 0
|
|
1165
2167
|
};
|
|
1166
2168
|
}
|
|
@@ -1173,7 +2175,11 @@ function runAgentCommand(command, context) {
|
|
|
1173
2175
|
${next}
|
|
1174
2176
|
`
|
|
1175
2177
|
).join(""),
|
|
1176
|
-
json: envelope(
|
|
2178
|
+
json: envelope(
|
|
2179
|
+
"search",
|
|
2180
|
+
{ query: command.query, limit: command.limit, matchCount: results.length, results },
|
|
2181
|
+
contract2
|
|
2182
|
+
),
|
|
1177
2183
|
exitCode: 0
|
|
1178
2184
|
};
|
|
1179
2185
|
}
|
|
@@ -1184,146 +2190,322 @@ function runAgentCommand(command, context) {
|
|
|
1184
2190
|
...result.files.map(({ path, action }) => ` ${action.padEnd(9)} ${path}`),
|
|
1185
2191
|
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
2192
|
];
|
|
1187
|
-
return {
|
|
1188
|
-
|
|
2193
|
+
return {
|
|
2194
|
+
text: `${lines.join("\n")}
|
|
2195
|
+
`,
|
|
2196
|
+
json: envelope("init", result, contract2),
|
|
2197
|
+
exitCode: 0
|
|
2198
|
+
};
|
|
1189
2199
|
}
|
|
1190
2200
|
case "check": {
|
|
1191
2201
|
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
2202
|
return {
|
|
1201
|
-
text:
|
|
1202
|
-
|
|
1203
|
-
json: envelope("check", report),
|
|
2203
|
+
text: renderCheckText(report),
|
|
2204
|
+
json: envelope("check", report, contract2),
|
|
1204
2205
|
// Warnings fail the run only when asked to: they are house rules, or may be native attributes.
|
|
1205
|
-
exitCode:
|
|
2206
|
+
exitCode: checkExitCode(report, { strict: command.strict })
|
|
1206
2207
|
};
|
|
1207
2208
|
}
|
|
1208
2209
|
case "doctor": {
|
|
1209
|
-
const report = doctor({
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
...
|
|
1213
|
-
|
|
2210
|
+
const report = doctor({
|
|
2211
|
+
cwd,
|
|
2212
|
+
contract: contract2,
|
|
2213
|
+
...context.parser ? { parser: context.parser } : {}
|
|
2214
|
+
});
|
|
2215
|
+
const { lookup, check_code: checking } = report.operations;
|
|
2216
|
+
const lines = [
|
|
2217
|
+
"Readiness",
|
|
2218
|
+
` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
|
|
2219
|
+
` code checking ${checking.status.padEnd(8)} ${checking.message}`,
|
|
2220
|
+
...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
|
|
2221
|
+
"",
|
|
2222
|
+
"Setup"
|
|
2223
|
+
];
|
|
2224
|
+
lines.push(
|
|
2225
|
+
...report.checks.flatMap(({ status, message, fix }) => [
|
|
2226
|
+
`[${status}] ${message}`,
|
|
2227
|
+
...fix === void 0 ? [] : [` fix: ${fix}`]
|
|
2228
|
+
])
|
|
2229
|
+
);
|
|
1214
2230
|
lines.push(
|
|
1215
2231
|
`${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
|
|
1216
2232
|
);
|
|
1217
2233
|
return {
|
|
1218
2234
|
text: `${lines.join("\n")}
|
|
1219
2235
|
`,
|
|
1220
|
-
json: envelope("doctor", report),
|
|
2236
|
+
json: envelope("doctor", report, contract2),
|
|
1221
2237
|
exitCode: report.summary.fail > 0 ? 1 : 0
|
|
1222
2238
|
};
|
|
1223
2239
|
}
|
|
2240
|
+
case "list": {
|
|
2241
|
+
const lines = [`OPERATOR ${VERSION}`, ""];
|
|
2242
|
+
for (const kind of ["component", "recipe"]) {
|
|
2243
|
+
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
2244
|
+
lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
|
|
2245
|
+
for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
|
|
2246
|
+
lines.push("");
|
|
2247
|
+
}
|
|
2248
|
+
return {
|
|
2249
|
+
text: `${lines.join("\n")}
|
|
2250
|
+
`,
|
|
2251
|
+
json: envelope(
|
|
2252
|
+
"list",
|
|
2253
|
+
{ version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
2254
|
+
contract2
|
|
2255
|
+
),
|
|
2256
|
+
exitCode: 0
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
case "add": {
|
|
2260
|
+
const result = installSources(command, { cwd, registry: registry2 });
|
|
2261
|
+
const setup = setupFor(result.dependencies, contract2);
|
|
2262
|
+
return {
|
|
2263
|
+
text: renderInstall(result, setup),
|
|
2264
|
+
json: envelope("add", { ...result, setup }, contract2),
|
|
2265
|
+
exitCode: 0
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
1224
2268
|
}
|
|
1225
2269
|
}
|
|
1226
2270
|
|
|
1227
2271
|
// src/mcp.ts
|
|
1228
2272
|
var import_node_readline = require("readline");
|
|
2273
|
+
|
|
2274
|
+
// src/validate.ts
|
|
2275
|
+
var KNOWN = /* @__PURE__ */ new Set([
|
|
2276
|
+
"type",
|
|
2277
|
+
"description",
|
|
2278
|
+
"default",
|
|
2279
|
+
"enum",
|
|
2280
|
+
"minimum",
|
|
2281
|
+
"maximum",
|
|
2282
|
+
"minLength",
|
|
2283
|
+
"maxLength"
|
|
2284
|
+
]);
|
|
2285
|
+
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;
|
|
2286
|
+
function describeValue(value) {
|
|
2287
|
+
if (typeof value === "string")
|
|
2288
|
+
return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
|
|
2289
|
+
if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
|
|
2290
|
+
if (value === null || typeof value === "boolean" || value === void 0) return String(value);
|
|
2291
|
+
return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
|
|
2292
|
+
}
|
|
2293
|
+
function expectation(schema) {
|
|
2294
|
+
switch (schema.type) {
|
|
2295
|
+
case "boolean":
|
|
2296
|
+
return "true or false";
|
|
2297
|
+
case "integer":
|
|
2298
|
+
case "number": {
|
|
2299
|
+
const noun = schema.type === "integer" ? "a whole number" : "a number";
|
|
2300
|
+
if (schema.minimum !== void 0 && schema.maximum !== void 0)
|
|
2301
|
+
return `${noun} from ${schema.minimum} to ${schema.maximum}`;
|
|
2302
|
+
if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
|
|
2303
|
+
if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
|
|
2304
|
+
return noun;
|
|
2305
|
+
}
|
|
2306
|
+
case "string": {
|
|
2307
|
+
if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
|
|
2308
|
+
const { minLength, maxLength } = schema;
|
|
2309
|
+
if (minLength !== void 0 && maxLength !== void 0)
|
|
2310
|
+
return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
|
|
2311
|
+
if (maxLength !== void 0)
|
|
2312
|
+
return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
|
|
2313
|
+
return "a string";
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function accepts(schema, value) {
|
|
2318
|
+
switch (schema.type) {
|
|
2319
|
+
case "boolean":
|
|
2320
|
+
return typeof value === "boolean";
|
|
2321
|
+
case "string":
|
|
2322
|
+
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));
|
|
2323
|
+
case "integer":
|
|
2324
|
+
case "number":
|
|
2325
|
+
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);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function validateArguments(tool, schema, args) {
|
|
2329
|
+
for (const [field, property] of Object.entries(schema.properties))
|
|
2330
|
+
for (const key of Object.keys(property))
|
|
2331
|
+
if (!KNOWN.has(key))
|
|
2332
|
+
throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
|
|
2333
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
2334
|
+
return `${tool}: arguments must be an object.`;
|
|
2335
|
+
const given = args;
|
|
2336
|
+
const required = schema.required ?? [];
|
|
2337
|
+
const blank = (value) => typeof value === "string" && value.trim() === "";
|
|
2338
|
+
const missing = [];
|
|
2339
|
+
const wrong = [];
|
|
2340
|
+
const unknown = [];
|
|
2341
|
+
for (const field of required)
|
|
2342
|
+
if (given[field] === void 0 || blank(given[field])) missing.push(field);
|
|
2343
|
+
for (const [field, value] of Object.entries(given)) {
|
|
2344
|
+
const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
|
|
2345
|
+
if (property === void 0) unknown.push(field);
|
|
2346
|
+
else if (value !== void 0 && !(required.includes(field) && blank(value))) {
|
|
2347
|
+
if (!accepts(property, value))
|
|
2348
|
+
wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const reject = schema.additionalProperties === false ? unknown : [];
|
|
2352
|
+
const names2 = Object.keys(schema.properties);
|
|
2353
|
+
if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
|
|
2354
|
+
return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
|
|
2355
|
+
const fragments = [
|
|
2356
|
+
...missing.map(
|
|
2357
|
+
(field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
|
|
2358
|
+
),
|
|
2359
|
+
...wrong,
|
|
2360
|
+
...reject.map(
|
|
2361
|
+
(field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
|
|
2362
|
+
)
|
|
2363
|
+
];
|
|
2364
|
+
return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// src/mcp.ts
|
|
1229
2368
|
var VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1230
2369
|
var text = (value) => ({ type: "text", text: value });
|
|
1231
|
-
var
|
|
2370
|
+
var LIMITS = { query: 500, name: 100, code: 2e5 };
|
|
2371
|
+
var ANSWERS = {
|
|
2372
|
+
search: "search",
|
|
2373
|
+
get_component: "component.detail",
|
|
2374
|
+
list_components: "component.list",
|
|
2375
|
+
get_instructions: "instructions",
|
|
2376
|
+
check_code: "check"
|
|
2377
|
+
};
|
|
2378
|
+
var readOnly = { readOnlyHint: true };
|
|
2379
|
+
var define = (tool) => ({
|
|
2380
|
+
...tool,
|
|
2381
|
+
outputSchema: outputSchemaFor(ANSWERS[tool.name]),
|
|
2382
|
+
annotations: readOnly
|
|
2383
|
+
});
|
|
1232
2384
|
var TOOLS = [
|
|
1233
|
-
{
|
|
2385
|
+
define({
|
|
1234
2386
|
name: "search",
|
|
1235
|
-
description: "Find the OPERATOR component or recipe
|
|
2387
|
+
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
2388
|
inputSchema: {
|
|
1237
2389
|
type: "object",
|
|
1238
2390
|
properties: {
|
|
1239
|
-
query:
|
|
1240
|
-
|
|
2391
|
+
query: {
|
|
2392
|
+
type: "string",
|
|
2393
|
+
minLength: 1,
|
|
2394
|
+
maxLength: LIMITS.query,
|
|
2395
|
+
description: "The need, in plain words."
|
|
2396
|
+
},
|
|
2397
|
+
limit: {
|
|
2398
|
+
type: "integer",
|
|
2399
|
+
minimum: 1,
|
|
2400
|
+
maximum: SEARCH_LIMIT_MAX,
|
|
2401
|
+
default: SEARCH_LIMIT_DEFAULT,
|
|
2402
|
+
description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
|
|
2403
|
+
}
|
|
1241
2404
|
},
|
|
1242
|
-
required: ["query"]
|
|
2405
|
+
required: ["query"],
|
|
2406
|
+
additionalProperties: false
|
|
1243
2407
|
}
|
|
1244
|
-
},
|
|
1245
|
-
{
|
|
2408
|
+
}),
|
|
2409
|
+
define({
|
|
1246
2410
|
name: "get_component",
|
|
1247
|
-
description: "One
|
|
2411
|
+
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
2412
|
inputSchema: {
|
|
1249
2413
|
type: "object",
|
|
1250
2414
|
properties: {
|
|
1251
|
-
name:
|
|
2415
|
+
name: {
|
|
2416
|
+
type: "string",
|
|
2417
|
+
minLength: 1,
|
|
2418
|
+
maxLength: LIMITS.name,
|
|
2419
|
+
description: "Component name, such as Button."
|
|
2420
|
+
},
|
|
1252
2421
|
dense: {
|
|
1253
2422
|
type: "boolean",
|
|
1254
|
-
|
|
2423
|
+
default: true,
|
|
2424
|
+
description: "Compact form (every constraint, one sentence per prop). False: full prose."
|
|
1255
2425
|
}
|
|
1256
2426
|
},
|
|
1257
|
-
required: ["name"]
|
|
2427
|
+
required: ["name"],
|
|
2428
|
+
additionalProperties: false
|
|
1258
2429
|
}
|
|
1259
|
-
},
|
|
1260
|
-
{
|
|
2430
|
+
}),
|
|
2431
|
+
define({
|
|
1261
2432
|
name: "list_components",
|
|
1262
2433
|
description: "Every OPERATOR component in one line each, with its parts.",
|
|
1263
|
-
inputSchema: { type: "object", properties: {} }
|
|
1264
|
-
},
|
|
1265
|
-
{
|
|
2434
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2435
|
+
}),
|
|
2436
|
+
define({
|
|
1266
2437
|
name: "get_instructions",
|
|
1267
|
-
description: "OPERATOR's house rules,
|
|
1268
|
-
inputSchema: { type: "object", properties: {} }
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
2438
|
+
description: "OPERATOR's house rules, component index and recipes: read once before building.",
|
|
2439
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2440
|
+
}),
|
|
2441
|
+
define({
|
|
1271
2442
|
name: "check_code",
|
|
1272
|
-
description: "Check TSX
|
|
2443
|
+
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
2444
|
inputSchema: {
|
|
1274
2445
|
type: "object",
|
|
1275
|
-
properties: {
|
|
1276
|
-
|
|
2446
|
+
properties: {
|
|
2447
|
+
code: {
|
|
2448
|
+
type: "string",
|
|
2449
|
+
minLength: 1,
|
|
2450
|
+
maxLength: LIMITS.code,
|
|
2451
|
+
description: "The TSX source of one file."
|
|
2452
|
+
}
|
|
2453
|
+
},
|
|
2454
|
+
required: ["code"],
|
|
2455
|
+
additionalProperties: false
|
|
1277
2456
|
}
|
|
1278
|
-
}
|
|
2457
|
+
})
|
|
1279
2458
|
];
|
|
1280
2459
|
function runTool(name, args, context) {
|
|
1281
2460
|
const { contract: contract2, registry: registry2 } = context;
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
return value;
|
|
1287
|
-
};
|
|
2461
|
+
const definition = TOOLS.find((tool) => tool.name === name);
|
|
2462
|
+
if (definition === void 0) throw new UnknownTool(name);
|
|
2463
|
+
const problem = validateArguments(name, definition.inputSchema, args);
|
|
2464
|
+
if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
|
|
1288
2465
|
switch (name) {
|
|
1289
2466
|
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}]`}
|
|
2467
|
+
const query = args["query"];
|
|
2468
|
+
const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
|
|
2469
|
+
const results = search(contract2, registry2, query, limit);
|
|
2470
|
+
return {
|
|
2471
|
+
text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
2472
|
+
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
1299
2473
|
${next}`
|
|
1300
|
-
|
|
2474
|
+
).join("\n"),
|
|
2475
|
+
structured: envelope(
|
|
2476
|
+
"search",
|
|
2477
|
+
{ query, limit, matchCount: results.length, results },
|
|
2478
|
+
contract2
|
|
2479
|
+
)
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
case "get_component": {
|
|
2483
|
+
const dense = args["dense"] ?? true;
|
|
2484
|
+
const detail = componentDetail(contract2, args["name"]);
|
|
2485
|
+
return {
|
|
2486
|
+
text: renderComponent(detail, { dense }),
|
|
2487
|
+
structured: envelope("component.detail", componentData(detail, dense), contract2)
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
case "list_components": {
|
|
2491
|
+
const components = componentIndex(contract2);
|
|
2492
|
+
return {
|
|
2493
|
+
text: components.map(
|
|
2494
|
+
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
2495
|
+
).join("\n"),
|
|
2496
|
+
structured: envelope("component.list", { components }, contract2)
|
|
2497
|
+
};
|
|
1301
2498
|
}
|
|
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
2499
|
case "get_instructions":
|
|
1311
|
-
return
|
|
2500
|
+
return {
|
|
2501
|
+
text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
|
|
2502
|
+
structured: envelope("instructions", instructionsData(contract2), contract2)
|
|
2503
|
+
};
|
|
1312
2504
|
case "check_code": {
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const findings = checkSource(ts, contract2, "snippet.tsx", code);
|
|
1318
|
-
if (findings.length === 0) return "No findings.";
|
|
1319
|
-
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1320
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1321
|
-
return [
|
|
1322
|
-
...findings.map(
|
|
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");
|
|
2505
|
+
const report = buildCheckReport([
|
|
2506
|
+
checkSource(contract2, "snippet.tsx", args["code"])
|
|
2507
|
+
]);
|
|
2508
|
+
return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
|
|
1327
2509
|
}
|
|
1328
2510
|
default:
|
|
1329
2511
|
throw new UnknownTool(name);
|
|
@@ -1337,24 +2519,28 @@ var UnknownTool = class extends Error {
|
|
|
1337
2519
|
}
|
|
1338
2520
|
};
|
|
1339
2521
|
function handleMessage(raw, context) {
|
|
1340
|
-
const
|
|
2522
|
+
const failure2 = (id2, code, message) => ({
|
|
1341
2523
|
jsonrpc: "2.0",
|
|
1342
2524
|
id: id2 ?? null,
|
|
1343
2525
|
error: { code, message }
|
|
1344
2526
|
});
|
|
1345
|
-
if (typeof raw !== "object" || raw === null) return
|
|
2527
|
+
if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
|
|
1346
2528
|
const { id, method, params } = raw;
|
|
1347
|
-
if (typeof method !== "string") return
|
|
2529
|
+
if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
|
|
1348
2530
|
if (id === void 0) return null;
|
|
1349
2531
|
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
1350
2532
|
switch (method) {
|
|
1351
2533
|
case "initialize": {
|
|
1352
2534
|
const asked = params?.protocolVersion;
|
|
2535
|
+
const identity = identityOf(context.contract);
|
|
1353
2536
|
return result({
|
|
1354
2537
|
protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
|
|
1355
2538
|
capabilities: { tools: {} },
|
|
1356
|
-
|
|
1357
|
-
|
|
2539
|
+
// The program's version, not the contract's: the contract has its own, in `_meta` and in
|
|
2540
|
+
// the identity of every result.
|
|
2541
|
+
serverInfo: { name: "oprt", version: VERSION },
|
|
2542
|
+
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.",
|
|
2543
|
+
_meta: { "dev.oprt/identity": identity }
|
|
1358
2544
|
});
|
|
1359
2545
|
}
|
|
1360
2546
|
case "ping":
|
|
@@ -1364,17 +2550,23 @@ function handleMessage(raw, context) {
|
|
|
1364
2550
|
case "tools/call": {
|
|
1365
2551
|
const { name, arguments: args } = params ?? {};
|
|
1366
2552
|
try {
|
|
1367
|
-
|
|
2553
|
+
const output = runTool(String(name), args ?? {}, context);
|
|
2554
|
+
return result({
|
|
2555
|
+
content: [text(output.text)],
|
|
2556
|
+
structuredContent: output.structured
|
|
2557
|
+
});
|
|
1368
2558
|
} catch (error) {
|
|
1369
|
-
if (error instanceof UnknownTool) return
|
|
2559
|
+
if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
|
|
2560
|
+
const failed = errorEnvelope(error, context.contract);
|
|
1370
2561
|
return result({
|
|
1371
|
-
content: [text(
|
|
2562
|
+
content: [text(failed.error)],
|
|
2563
|
+
structuredContent: failed,
|
|
1372
2564
|
isError: true
|
|
1373
2565
|
});
|
|
1374
2566
|
}
|
|
1375
2567
|
}
|
|
1376
2568
|
default:
|
|
1377
|
-
return
|
|
2569
|
+
return failure2(id, -32601, `Method not found: ${method}`);
|
|
1378
2570
|
}
|
|
1379
2571
|
}
|
|
1380
2572
|
function serve(input, output, context) {
|
|
@@ -1395,7 +2587,6 @@ function serve(input, output, context) {
|
|
|
1395
2587
|
}
|
|
1396
2588
|
|
|
1397
2589
|
// src/index.ts
|
|
1398
|
-
var VERSION = package_default.version;
|
|
1399
2590
|
function loadRegistry() {
|
|
1400
2591
|
const here = (0, import_node_path6.dirname)((0, import_node_url3.fileURLToPath)(importMetaUrl));
|
|
1401
2592
|
const candidates = [(0, import_node_path6.join)(here, "registry.json"), (0, import_node_path6.join)(here, "../generated/registry.json")];
|
|
@@ -1408,8 +2599,8 @@ function loadRegistry() {
|
|
|
1408
2599
|
return JSON.parse((0, import_node_fs6.readFileSync)(found, "utf8"));
|
|
1409
2600
|
}
|
|
1410
2601
|
var registry = loadRegistry();
|
|
1411
|
-
var
|
|
1412
|
-
var contract = () =>
|
|
2602
|
+
var loaded2;
|
|
2603
|
+
var contract = () => loaded2 ??= loadContract();
|
|
1413
2604
|
function install(options, context = {}) {
|
|
1414
2605
|
return installSources(options, {
|
|
1415
2606
|
cwd: context.cwd ?? process.cwd(),
|
|
@@ -1420,26 +2611,38 @@ function install(options, context = {}) {
|
|
|
1420
2611
|
0 && (module.exports = {
|
|
1421
2612
|
CliError,
|
|
1422
2613
|
DEFAULT_OUT,
|
|
2614
|
+
RESULT_SCHEMAS,
|
|
2615
|
+
RESULT_SCHEMA_VERSION,
|
|
2616
|
+
RESULT_TYPES,
|
|
1423
2617
|
TOOLS,
|
|
1424
2618
|
VERSION,
|
|
1425
2619
|
applyBlock,
|
|
1426
2620
|
blockVersion,
|
|
2621
|
+
buildCheckReport,
|
|
2622
|
+
checkExitCode,
|
|
1427
2623
|
checkPaths,
|
|
1428
2624
|
checkSource,
|
|
1429
2625
|
componentDetail,
|
|
1430
2626
|
componentIndex,
|
|
1431
2627
|
contract,
|
|
2628
|
+
contractDigest,
|
|
1432
2629
|
doctor,
|
|
2630
|
+
envelope,
|
|
2631
|
+
errorEnvelope,
|
|
1433
2632
|
handleMessage,
|
|
2633
|
+
identityOf,
|
|
1434
2634
|
init,
|
|
1435
2635
|
install,
|
|
2636
|
+
outputSchemaFor,
|
|
1436
2637
|
parseArgs,
|
|
1437
2638
|
registry,
|
|
1438
2639
|
removeBlock,
|
|
1439
2640
|
renderAgentBlock,
|
|
2641
|
+
renderCheckText,
|
|
1440
2642
|
renderComponent,
|
|
1441
2643
|
resolveEntries,
|
|
1442
2644
|
runAgentCommand,
|
|
2645
|
+
schemaFor,
|
|
1443
2646
|
search,
|
|
1444
2647
|
serve
|
|
1445
2648
|
});
|