@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.js
CHANGED
|
@@ -3,75 +3,6 @@ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
|
3
3
|
import { dirname as dirname5, join as join4 } from "path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5
5
|
|
|
6
|
-
// package.json
|
|
7
|
-
var package_default = {
|
|
8
|
-
name: "@oprt/cli",
|
|
9
|
-
version: "0.1.0",
|
|
10
|
-
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.",
|
|
11
|
-
keywords: [
|
|
12
|
-
"design-system",
|
|
13
|
-
"cli",
|
|
14
|
-
"coding-agents",
|
|
15
|
-
"mcp",
|
|
16
|
-
"model-context-protocol",
|
|
17
|
-
"code-check",
|
|
18
|
-
"components"
|
|
19
|
-
],
|
|
20
|
-
homepage: "https://oprt.dev/docs/agents/cli",
|
|
21
|
-
type: "module",
|
|
22
|
-
main: "./dist/index.cjs",
|
|
23
|
-
module: "./dist/index.js",
|
|
24
|
-
types: "./dist/index.d.ts",
|
|
25
|
-
bin: {
|
|
26
|
-
oprt: "./dist/bin.js"
|
|
27
|
-
},
|
|
28
|
-
engines: {
|
|
29
|
-
node: ">=22"
|
|
30
|
-
},
|
|
31
|
-
exports: {
|
|
32
|
-
".": {
|
|
33
|
-
import: {
|
|
34
|
-
types: "./dist/index.d.ts",
|
|
35
|
-
default: "./dist/index.js"
|
|
36
|
-
},
|
|
37
|
-
require: {
|
|
38
|
-
types: "./dist/index.d.cts",
|
|
39
|
-
default: "./dist/index.cjs"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"./read": {
|
|
43
|
-
import: {
|
|
44
|
-
types: "./dist/read.d.ts",
|
|
45
|
-
default: "./dist/read.js"
|
|
46
|
-
},
|
|
47
|
-
require: {
|
|
48
|
-
types: "./dist/read.d.cts",
|
|
49
|
-
default: "./dist/read.cjs"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
files: [
|
|
54
|
-
"dist"
|
|
55
|
-
],
|
|
56
|
-
scripts: {
|
|
57
|
-
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
58
|
-
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
59
|
-
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
60
|
-
test: "vitest run",
|
|
61
|
-
typecheck: "tsc --noEmit",
|
|
62
|
-
lint: "eslint src scripts"
|
|
63
|
-
},
|
|
64
|
-
devDependencies: {
|
|
65
|
-
tsup: "^8.3.0",
|
|
66
|
-
typescript: "^5.6.3",
|
|
67
|
-
vitest: "^2.1.3"
|
|
68
|
-
},
|
|
69
|
-
license: "MIT",
|
|
70
|
-
publishConfig: {
|
|
71
|
-
access: "public"
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
6
|
// src/contract.ts
|
|
76
7
|
import { existsSync, readFileSync } from "fs";
|
|
77
8
|
import { dirname, join } from "path";
|
|
@@ -115,6 +46,197 @@ var CliError = class extends Error {
|
|
|
115
46
|
suggestions;
|
|
116
47
|
};
|
|
117
48
|
|
|
49
|
+
// src/lookup.ts
|
|
50
|
+
var SEARCH_LIMIT_MAX = 50;
|
|
51
|
+
var SEARCH_LIMIT_DEFAULT = 10;
|
|
52
|
+
var squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
53
|
+
var firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
|
|
54
|
+
function distance(a, b) {
|
|
55
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
56
|
+
for (let i = 1; i <= a.length; i++) {
|
|
57
|
+
let diagonal = row[0];
|
|
58
|
+
row[0] = i;
|
|
59
|
+
for (let j = 1; j <= b.length; j++) {
|
|
60
|
+
const above = row[j];
|
|
61
|
+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
62
|
+
diagonal = above;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return row[b.length];
|
|
66
|
+
}
|
|
67
|
+
function nearest(wanted, names2, limit = 5) {
|
|
68
|
+
const target = squash(wanted);
|
|
69
|
+
return names2.map((name) => {
|
|
70
|
+
const candidate = squash(name);
|
|
71
|
+
const apart = distance(target, candidate);
|
|
72
|
+
const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
|
|
73
|
+
return { name, apart, close };
|
|
74
|
+
}).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
|
|
75
|
+
}
|
|
76
|
+
function resolveComponent(contract2, wanted) {
|
|
77
|
+
const names2 = Object.keys(contract2.components);
|
|
78
|
+
const found = names2.find((name) => name === wanted) ?? names2.find((name) => squash(name) === squash(wanted));
|
|
79
|
+
if (found !== void 0) return found;
|
|
80
|
+
const suggestions = nearest(wanted, names2);
|
|
81
|
+
throw new CliError(
|
|
82
|
+
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
83
|
+
2,
|
|
84
|
+
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function allowedChildren(rules, name) {
|
|
88
|
+
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
89
|
+
if (parent === name) return rule.allowed_children;
|
|
90
|
+
const nested = allowedChildren(rule.children, name);
|
|
91
|
+
if (nested !== void 0) return nested;
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
96
|
+
(part) => contract2.guidance.components[part]?.part_of === name
|
|
97
|
+
);
|
|
98
|
+
function componentIndex(contract2) {
|
|
99
|
+
return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
100
|
+
name,
|
|
101
|
+
purpose: contract2.guidance.components[name].use_for.join("; "),
|
|
102
|
+
parts: partsOf(contract2, name)
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function componentDetail(contract2, wanted) {
|
|
106
|
+
const name = resolveComponent(contract2, wanted);
|
|
107
|
+
const component = contract2.components[name];
|
|
108
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
109
|
+
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
110
|
+
const notes = guide.values?.[prop];
|
|
111
|
+
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
112
|
+
});
|
|
113
|
+
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
description: component.description,
|
|
117
|
+
use_for: guide.use_for ?? [],
|
|
118
|
+
not_for: guide.not_for ?? [],
|
|
119
|
+
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
120
|
+
parts: partsOf(contract2, name),
|
|
121
|
+
props,
|
|
122
|
+
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
123
|
+
composable_with: component.composable_with ?? [],
|
|
124
|
+
never_inside: component.never_inside ?? [],
|
|
125
|
+
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
126
|
+
example: component.example
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function propLine(prop, dense) {
|
|
130
|
+
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)})`);
|
|
131
|
+
const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
|
|
132
|
+
const lines = [
|
|
133
|
+
` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
|
|
134
|
+
];
|
|
135
|
+
for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
function renderComponent(detail, { dense }) {
|
|
139
|
+
const lines = [
|
|
140
|
+
detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
|
|
141
|
+
];
|
|
142
|
+
if (detail.not_for.length > 0)
|
|
143
|
+
lines.push(
|
|
144
|
+
`Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
|
|
145
|
+
);
|
|
146
|
+
if (!dense) lines.push("", detail.description, "");
|
|
147
|
+
if (detail.props.length > 0) {
|
|
148
|
+
lines.push("Props:");
|
|
149
|
+
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
150
|
+
}
|
|
151
|
+
for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
|
|
152
|
+
lines.push(
|
|
153
|
+
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
154
|
+
);
|
|
155
|
+
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
156
|
+
if (detail.allowed_children !== void 0)
|
|
157
|
+
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
158
|
+
if (detail.composable_with.length > 0)
|
|
159
|
+
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
160
|
+
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
161
|
+
lines.push(
|
|
162
|
+
detail.example.includes("\n") ? `Example:
|
|
163
|
+
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
164
|
+
);
|
|
165
|
+
return `${lines.join("\n")}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
var STOP = new Set(
|
|
169
|
+
"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(
|
|
170
|
+
" "
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
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);
|
|
174
|
+
var matched = (query, text2) => {
|
|
175
|
+
const present = new Set(words(text2));
|
|
176
|
+
return query.filter((word) => present.has(word)).length;
|
|
177
|
+
};
|
|
178
|
+
function search(contract2, registry2, query, limit = SEARCH_LIMIT_DEFAULT) {
|
|
179
|
+
const wanted = words(query);
|
|
180
|
+
if (wanted.length === 0) return [];
|
|
181
|
+
const whole = squash(query);
|
|
182
|
+
const scores = /* @__PURE__ */ new Map();
|
|
183
|
+
const add = (name, points, why) => {
|
|
184
|
+
const before = scores.get(name) ?? { score: 0 };
|
|
185
|
+
scores.set(name, {
|
|
186
|
+
score: before.score + points,
|
|
187
|
+
...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
for (const [name, component] of Object.entries(contract2.components)) {
|
|
191
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
192
|
+
const plain = squash(name);
|
|
193
|
+
if (plain === whole) add(name, 100);
|
|
194
|
+
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
195
|
+
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
196
|
+
for (const phrase of guide.use_for ?? []) {
|
|
197
|
+
const hits = matched(wanted, phrase);
|
|
198
|
+
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
199
|
+
}
|
|
200
|
+
for (const { need, use } of guide.not_for ?? []) {
|
|
201
|
+
const hits = matched(wanted, need);
|
|
202
|
+
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
203
|
+
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
204
|
+
}
|
|
205
|
+
const inDescription = matched(wanted, component.description);
|
|
206
|
+
if (inDescription > 0) add(name, 2 * inDescription);
|
|
207
|
+
}
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const [name, { score, why }] of scores) {
|
|
210
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
211
|
+
results.push({
|
|
212
|
+
kind: "component",
|
|
213
|
+
name,
|
|
214
|
+
purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
|
|
215
|
+
score,
|
|
216
|
+
...why === void 0 ? {} : { why },
|
|
217
|
+
next: `oprt component ${name}`
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
|
|
221
|
+
const recipe = contract2.patterns?.[entry.name];
|
|
222
|
+
const plain = squash(entry.name);
|
|
223
|
+
const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
|
|
224
|
+
const hits = matched(wanted, entry.description);
|
|
225
|
+
const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
|
|
226
|
+
if (score > 0)
|
|
227
|
+
results.push({
|
|
228
|
+
kind: "recipe",
|
|
229
|
+
name: entry.name,
|
|
230
|
+
purpose: firstSentence(entry.description),
|
|
231
|
+
score,
|
|
232
|
+
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
236
|
+
const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
|
|
237
|
+
return results.filter(({ score }) => score >= floor).slice(0, limit);
|
|
238
|
+
}
|
|
239
|
+
|
|
118
240
|
// src/args.ts
|
|
119
241
|
var DEFAULT_OUT = "src/components/operator";
|
|
120
242
|
function split(command, rest, spec) {
|
|
@@ -170,9 +292,10 @@ var AGENT_COMMANDS = {
|
|
|
170
292
|
"--json": "flag"
|
|
171
293
|
});
|
|
172
294
|
if (positional.length === 0) throw new CliError("Usage: oprt search <need> [--limit <n>]", 2);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
295
|
+
const given = value("--limit");
|
|
296
|
+
const limit = given === void 0 ? SEARCH_LIMIT_DEFAULT : Number(given);
|
|
297
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > SEARCH_LIMIT_MAX)
|
|
298
|
+
throw new CliError(`--limit takes a whole number from 1 to ${SEARCH_LIMIT_MAX}.`, 2);
|
|
176
299
|
return { command: "search", query: positional.join(" "), limit, json: has("--json") };
|
|
177
300
|
},
|
|
178
301
|
init(rest) {
|
|
@@ -235,7 +358,8 @@ function parseArgs(args) {
|
|
|
235
358
|
names: [],
|
|
236
359
|
out: DEFAULT_OUT,
|
|
237
360
|
dryRun: false,
|
|
238
|
-
overwrite: false
|
|
361
|
+
overwrite: false,
|
|
362
|
+
json: false
|
|
239
363
|
};
|
|
240
364
|
const seen = /* @__PURE__ */ new Set();
|
|
241
365
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -244,7 +368,7 @@ function parseArgs(args) {
|
|
|
244
368
|
parsed.names.push(token);
|
|
245
369
|
continue;
|
|
246
370
|
}
|
|
247
|
-
if (!["--out", "--dry-run", "--overwrite"].includes(token))
|
|
371
|
+
if (!["--out", "--dry-run", "--overwrite", "--json"].includes(token))
|
|
248
372
|
throw new CliError(`Unknown option ${JSON.stringify(token)}. Run oprt --help.`, 2);
|
|
249
373
|
if (seen.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
|
|
250
374
|
seen.add(token);
|
|
@@ -254,6 +378,7 @@ function parseArgs(args) {
|
|
|
254
378
|
throw new CliError("--out requires a relative directory.", 2);
|
|
255
379
|
parsed.out = value;
|
|
256
380
|
} else if (token === "--dry-run") parsed.dryRun = true;
|
|
381
|
+
else if (token === "--json") parsed.json = true;
|
|
257
382
|
else parsed.overwrite = true;
|
|
258
383
|
}
|
|
259
384
|
if (!parsed.names.length)
|
|
@@ -262,10 +387,10 @@ function parseArgs(args) {
|
|
|
262
387
|
}
|
|
263
388
|
|
|
264
389
|
// src/installer.ts
|
|
265
|
-
function resolveEntries(
|
|
266
|
-
if (!
|
|
390
|
+
function resolveEntries(names2, registry2) {
|
|
391
|
+
if (!names2.length) throw new CliError("add requires one or more names.", 2);
|
|
267
392
|
const selected = /* @__PURE__ */ new Map();
|
|
268
|
-
for (const name of
|
|
393
|
+
for (const name of names2) {
|
|
269
394
|
const normalized = name.toLowerCase();
|
|
270
395
|
const entry = registry2.entries.find(
|
|
271
396
|
(candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
|
|
@@ -373,202 +498,362 @@ function installSources(options, context) {
|
|
|
373
498
|
return result;
|
|
374
499
|
}
|
|
375
500
|
|
|
376
|
-
// src/
|
|
377
|
-
import {
|
|
378
|
-
import { createRequire } from "module";
|
|
379
|
-
import { basename, extname, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
380
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
501
|
+
// src/identity.ts
|
|
502
|
+
import { createHash } from "crypto";
|
|
381
503
|
|
|
382
|
-
//
|
|
383
|
-
var
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
504
|
+
// package.json
|
|
505
|
+
var package_default = {
|
|
506
|
+
name: "@oprt/cli",
|
|
507
|
+
version: "0.4.0",
|
|
508
|
+
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.",
|
|
509
|
+
keywords: [
|
|
510
|
+
"design-system",
|
|
511
|
+
"cli",
|
|
512
|
+
"coding-agents",
|
|
513
|
+
"mcp",
|
|
514
|
+
"model-context-protocol",
|
|
515
|
+
"code-check",
|
|
516
|
+
"components"
|
|
517
|
+
],
|
|
518
|
+
homepage: "https://oprt.dev/docs/agents/cli",
|
|
519
|
+
type: "module",
|
|
520
|
+
main: "./dist/index.cjs",
|
|
521
|
+
module: "./dist/index.js",
|
|
522
|
+
types: "./dist/index.d.ts",
|
|
523
|
+
bin: {
|
|
524
|
+
oprt: "./dist/bin.js"
|
|
525
|
+
},
|
|
526
|
+
engines: {
|
|
527
|
+
node: ">=22"
|
|
528
|
+
},
|
|
529
|
+
exports: {
|
|
530
|
+
".": {
|
|
531
|
+
import: {
|
|
532
|
+
types: "./dist/index.d.ts",
|
|
533
|
+
default: "./dist/index.js"
|
|
534
|
+
},
|
|
535
|
+
require: {
|
|
536
|
+
types: "./dist/index.d.cts",
|
|
537
|
+
default: "./dist/index.cjs"
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
"./read": {
|
|
541
|
+
import: {
|
|
542
|
+
types: "./dist/read.d.ts",
|
|
543
|
+
default: "./dist/read.js"
|
|
544
|
+
},
|
|
545
|
+
require: {
|
|
546
|
+
types: "./dist/read.d.cts",
|
|
547
|
+
default: "./dist/read.cjs"
|
|
548
|
+
}
|
|
394
549
|
}
|
|
550
|
+
},
|
|
551
|
+
files: [
|
|
552
|
+
"dist"
|
|
553
|
+
],
|
|
554
|
+
scripts: {
|
|
555
|
+
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
556
|
+
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
557
|
+
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
558
|
+
test: "vitest run",
|
|
559
|
+
typecheck: "tsc --noEmit",
|
|
560
|
+
lint: "eslint src scripts"
|
|
561
|
+
},
|
|
562
|
+
devDependencies: {
|
|
563
|
+
"@babel/parser": "8.0.8",
|
|
564
|
+
"@babel/types": "8.0.8",
|
|
565
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
566
|
+
tsup: "^8.3.0",
|
|
567
|
+
typescript: "^5.6.3",
|
|
568
|
+
vitest: "^2.1.3"
|
|
569
|
+
},
|
|
570
|
+
license: "MIT",
|
|
571
|
+
publishConfig: {
|
|
572
|
+
access: "public"
|
|
395
573
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
function resolveComponent(contract2, wanted) {
|
|
408
|
-
const names = Object.keys(contract2.components);
|
|
409
|
-
const found = names.find((name) => name === wanted) ?? names.find((name) => squash(name) === squash(wanted));
|
|
410
|
-
if (found !== void 0) return found;
|
|
411
|
-
const suggestions = nearest(wanted, names);
|
|
412
|
-
throw new CliError(
|
|
413
|
-
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
414
|
-
2,
|
|
415
|
-
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
function allowedChildren(rules, name) {
|
|
419
|
-
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
420
|
-
if (parent === name) return rule.allowed_children;
|
|
421
|
-
const nested = allowedChildren(rule.children, name);
|
|
422
|
-
if (nested !== void 0) return nested;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/identity.ts
|
|
577
|
+
var VERSION = package_default.version;
|
|
578
|
+
var RESULT_SCHEMA_VERSION = 2;
|
|
579
|
+
var digests = /* @__PURE__ */ new WeakMap();
|
|
580
|
+
function contractDigest(contract2) {
|
|
581
|
+
let digest = digests.get(contract2);
|
|
582
|
+
if (digest === void 0) {
|
|
583
|
+
digest = `sha256:${createHash("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
|
|
584
|
+
digests.set(contract2, digest);
|
|
423
585
|
}
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
427
|
-
(part) => contract2.guidance.components[part]?.part_of === name
|
|
428
|
-
);
|
|
429
|
-
function componentIndex(contract2) {
|
|
430
|
-
return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
431
|
-
name,
|
|
432
|
-
purpose: contract2.guidance.components[name].use_for.join("; "),
|
|
433
|
-
parts: partsOf(contract2, name)
|
|
434
|
-
}));
|
|
586
|
+
return digest;
|
|
435
587
|
}
|
|
436
|
-
function
|
|
437
|
-
const name = resolveComponent(contract2, wanted);
|
|
438
|
-
const component = contract2.components[name];
|
|
439
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
440
|
-
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
441
|
-
const notes = guide.values?.[prop];
|
|
442
|
-
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
443
|
-
});
|
|
444
|
-
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
588
|
+
function identityOf(contract2) {
|
|
445
589
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
451
|
-
parts: partsOf(contract2, name),
|
|
452
|
-
props,
|
|
453
|
-
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
454
|
-
composable_with: component.composable_with ?? [],
|
|
455
|
-
never_inside: component.never_inside ?? [],
|
|
456
|
-
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
457
|
-
example: component.example
|
|
590
|
+
cli_version: VERSION,
|
|
591
|
+
contract_version: contract2.version,
|
|
592
|
+
contract_digest: contractDigest(contract2),
|
|
593
|
+
schema_version: RESULT_SCHEMA_VERSION
|
|
458
594
|
};
|
|
459
595
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
596
|
+
|
|
597
|
+
// src/check.ts
|
|
598
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
599
|
+
import { basename, extname, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
600
|
+
|
|
601
|
+
// src/parser.ts
|
|
602
|
+
import { createRequire } from "module";
|
|
603
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
604
|
+
|
|
605
|
+
// ../../node_modules/.pnpm/@babel+parser@8.0.8/node_modules/@babel/parser/package.json
|
|
606
|
+
var version = "8.0.8";
|
|
607
|
+
|
|
608
|
+
// src/parser.ts
|
|
609
|
+
var PARSER = {
|
|
610
|
+
name: "@babel/parser",
|
|
611
|
+
version,
|
|
612
|
+
source: "bundled"
|
|
613
|
+
};
|
|
614
|
+
function optionsFor(file) {
|
|
615
|
+
const typed = /\.tsx$/.test(file);
|
|
616
|
+
const plugins = [
|
|
617
|
+
"jsx",
|
|
618
|
+
...typed ? ["typescript"] : [],
|
|
619
|
+
"decorators",
|
|
620
|
+
"decoratorAutoAccessors",
|
|
621
|
+
"deferredImportEvaluation",
|
|
622
|
+
"sourcePhaseImports"
|
|
465
623
|
];
|
|
466
|
-
|
|
467
|
-
|
|
624
|
+
return {
|
|
625
|
+
sourceType: "module",
|
|
626
|
+
plugins,
|
|
627
|
+
// Recoverable errors are returned beside the tree, instead of the first one being thrown.
|
|
628
|
+
errorRecovery: true,
|
|
629
|
+
attachComment: false,
|
|
630
|
+
allowReturnOutsideFunction: true,
|
|
631
|
+
allowUndeclaredExports: true,
|
|
632
|
+
allowSuperOutsideMethod: true,
|
|
633
|
+
allowNewTargetOutsideFunction: true
|
|
634
|
+
};
|
|
468
635
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
636
|
+
var FILE = "babel-parser.cjs";
|
|
637
|
+
var REINSTALL = "Install @oprt/cli again: the parser ships beside the executable in dist.";
|
|
638
|
+
var loaded;
|
|
639
|
+
function loadBabel() {
|
|
640
|
+
if (loaded !== void 0) return loaded;
|
|
641
|
+
const fromSource = /\.[cm]?tsx?$/.test(fileURLToPath2(import.meta.url));
|
|
642
|
+
try {
|
|
643
|
+
const require2 = createRequire(import.meta.url);
|
|
644
|
+
loaded = fromSource ? require2("@babel/parser") : require2("./babel-parser.cjs");
|
|
645
|
+
} catch (error) {
|
|
646
|
+
throw new CliError(
|
|
647
|
+
`Code checking cannot load its parser (${FILE}): ${error instanceof Error ? error.message : String(error)} ${REINSTALL}`,
|
|
648
|
+
1,
|
|
649
|
+
{ code: "ERR_PARSER_MISSING" }
|
|
476
650
|
);
|
|
477
|
-
if (!dense) lines.push("", detail.description, "");
|
|
478
|
-
if (detail.props.length > 0) {
|
|
479
|
-
lines.push("Props:");
|
|
480
|
-
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
481
651
|
}
|
|
482
|
-
|
|
483
|
-
lines.push(
|
|
484
|
-
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
485
|
-
);
|
|
486
|
-
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
487
|
-
if (detail.allowed_children !== void 0)
|
|
488
|
-
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
489
|
-
if (detail.composable_with.length > 0)
|
|
490
|
-
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
491
|
-
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
492
|
-
lines.push(
|
|
493
|
-
detail.example.includes("\n") ? `Example:
|
|
494
|
-
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
495
|
-
);
|
|
496
|
-
return `${lines.join("\n")}
|
|
497
|
-
`;
|
|
652
|
+
return loaded;
|
|
498
653
|
}
|
|
499
|
-
var
|
|
500
|
-
|
|
501
|
-
"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
var matched = (query, text2) => {
|
|
506
|
-
const present = new Set(words(text2));
|
|
507
|
-
return query.filter((word) => present.has(word)).length;
|
|
654
|
+
var rejects = (babel, code) => {
|
|
655
|
+
try {
|
|
656
|
+
return babel.parse(code, optionsFor("probe.tsx")).errors.length > 0;
|
|
657
|
+
} catch {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
508
660
|
};
|
|
509
|
-
function
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
});
|
|
520
|
-
};
|
|
521
|
-
for (const [name, component] of Object.entries(contract2.components)) {
|
|
522
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
523
|
-
const plain = squash(name);
|
|
524
|
-
if (plain === whole) add(name, 100);
|
|
525
|
-
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
526
|
-
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
527
|
-
for (const phrase of guide.use_for ?? []) {
|
|
528
|
-
const hits = matched(wanted, phrase);
|
|
529
|
-
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
530
|
-
}
|
|
531
|
-
for (const { need, use } of guide.not_for ?? []) {
|
|
532
|
-
const hits = matched(wanted, need);
|
|
533
|
-
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
534
|
-
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
535
|
-
}
|
|
536
|
-
const inDescription = matched(wanted, component.description);
|
|
537
|
-
if (inDescription > 0) add(name, 2 * inDescription);
|
|
661
|
+
function probeParser(load = loadBabel) {
|
|
662
|
+
let babel;
|
|
663
|
+
try {
|
|
664
|
+
babel = load();
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return {
|
|
667
|
+
ready: false,
|
|
668
|
+
reason: "parser_missing",
|
|
669
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
670
|
+
};
|
|
538
671
|
}
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
672
|
+
if (rejects(babel, "export const a = <a b={1 as number} />;"))
|
|
673
|
+
return {
|
|
674
|
+
ready: false,
|
|
675
|
+
reason: "parser_unusable",
|
|
676
|
+
detail: "it rejected valid code in a probe parse"
|
|
677
|
+
};
|
|
678
|
+
if (!rejects(babel, "export const a = <a>;"))
|
|
679
|
+
return {
|
|
680
|
+
ready: false,
|
|
681
|
+
reason: "parser_unusable",
|
|
682
|
+
detail: "it did not report a syntax error in code that has one"
|
|
683
|
+
};
|
|
684
|
+
return { ready: true, parser: PARSER };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/syntax.ts
|
|
688
|
+
var attempt = (babel, text2, file) => {
|
|
689
|
+
try {
|
|
690
|
+
const tree = babel.parse(text2, optionsFor(file));
|
|
691
|
+
return { tree, errors: tree.errors };
|
|
692
|
+
} catch (error) {
|
|
693
|
+
return { tree: null, fatal: error };
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var sentence = (message) => message.replace(/\s*\(\d+:\d+\)$/, "");
|
|
697
|
+
var problemOf = (failure2) => ({
|
|
698
|
+
line: failure2.loc.line,
|
|
699
|
+
column: failure2.loc.column + 1,
|
|
700
|
+
message: sentence(failure2.message),
|
|
701
|
+
code: failure2.reasonCode ?? "SyntaxError",
|
|
702
|
+
position: failure2.pos
|
|
703
|
+
});
|
|
704
|
+
var tagName = (name) => {
|
|
705
|
+
if (name === void 0) return "";
|
|
706
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
707
|
+
if (name.type === "JSXMemberExpression")
|
|
708
|
+
return `${tagName(name.object)}.${name.property.name}`;
|
|
709
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
710
|
+
};
|
|
711
|
+
var NOT_NODES = /* @__PURE__ */ new Set(["loc", "extra", "errors", "tokens", "comments"]);
|
|
712
|
+
function* childNodes(node) {
|
|
713
|
+
for (const [key, value] of Object.entries(node)) {
|
|
714
|
+
if (NOT_NODES.has(key) || typeof value !== "object" || value === null) continue;
|
|
715
|
+
if (Array.isArray(value)) {
|
|
716
|
+
for (const item of value)
|
|
717
|
+
if (typeof item === "object" && item !== null && typeof item.type === "string")
|
|
718
|
+
yield item;
|
|
719
|
+
} else if (typeof value.type === "string") yield value;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
var walk = (node, ancestors, visit) => {
|
|
723
|
+
visit(node, ancestors);
|
|
724
|
+
ancestors.push(node);
|
|
725
|
+
for (const child of childNodes(node)) walk(child, ancestors, visit);
|
|
726
|
+
ancestors.pop();
|
|
727
|
+
};
|
|
728
|
+
var unclosed = (element) => {
|
|
729
|
+
const name = element.openingElement.name;
|
|
730
|
+
return {
|
|
731
|
+
line: name.loc?.start.line ?? 1,
|
|
732
|
+
column: (name.loc?.start.column ?? 0) + 1,
|
|
733
|
+
message: `JSX element '${tagName(name)}' has no corresponding closing tag.`,
|
|
734
|
+
code: "UnclosedJsxElement",
|
|
735
|
+
position: name.start ?? 0
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
function problemsOf(tree, errors, limit, consumed) {
|
|
739
|
+
const mismatched = /* @__PURE__ */ new Map();
|
|
740
|
+
walk(tree.program, [], (node, ancestors) => {
|
|
741
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) < limit && tagName(node.openingElement.name) !== tagName(node.closingElement.name))
|
|
742
|
+
mismatched.set(node.closingElement.start ?? 0, { element: node, ancestors: [...ancestors] });
|
|
743
|
+
});
|
|
744
|
+
const problems = [];
|
|
745
|
+
for (const error of errors) {
|
|
746
|
+
if (error.pos >= limit) continue;
|
|
747
|
+
const entry = error.reasonCode === "MissingClosingTagElement" && mismatched.get(error.pos);
|
|
748
|
+
if (entry) {
|
|
749
|
+
const closing = tagName(entry.element.closingElement?.name);
|
|
750
|
+
const outer = [...entry.ancestors].reverse().find(
|
|
751
|
+
(ancestor) => ancestor.type === "JSXElement" && tagName(ancestor.openingElement.name) === closing && !consumed.has(ancestor)
|
|
752
|
+
);
|
|
753
|
+
if (outer !== void 0) {
|
|
754
|
+
consumed.add(outer);
|
|
755
|
+
problems.push(unclosed(entry.element));
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
problems.push(problemOf(error));
|
|
760
|
+
}
|
|
761
|
+
return problems;
|
|
762
|
+
}
|
|
763
|
+
var TYPE_ARGUMENTS = /(?<=[\w$\])])<(?:[^<>()]|<(?:[^<>()]|<[^<>()]*>)*>)*>(?=\()/g;
|
|
764
|
+
var LINE_BREAKS = new RegExp(`[^\\r\\n${String.fromCharCode(8232, 8233)}]`, "g");
|
|
765
|
+
var blankedOut = (match) => match.replace(LINE_BREAKS, " ");
|
|
766
|
+
var CLOSING_TAG = "</_>";
|
|
767
|
+
var COMPLETIONS = [CLOSING_TAG, ")", "}", "]"];
|
|
768
|
+
var MAX_COMPLETIONS = 24;
|
|
769
|
+
function complete(babel, prefix, file) {
|
|
770
|
+
let text2 = prefix;
|
|
771
|
+
let first = null;
|
|
772
|
+
for (let step = 0; step <= MAX_COMPLETIONS; step++) {
|
|
773
|
+
const tried = attempt(babel, text2, file);
|
|
774
|
+
if (tried.tree !== null) return { tree: tried.tree, errors: tried.errors, first };
|
|
775
|
+
const wantsMore = tried.fatal.reasonCode === "UnterminatedJsxContent" || tried.fatal.pos >= text2.length;
|
|
776
|
+
if (!wantsMore) return null;
|
|
777
|
+
const next = COMPLETIONS.find((piece) => {
|
|
778
|
+
const extended = attempt(babel, text2 + piece, file);
|
|
779
|
+
return extended.tree !== null || extended.fatal.reasonCode === "UnterminatedJsxContent" || extended.fatal.pos >= text2.length + piece.length;
|
|
549
780
|
});
|
|
781
|
+
if (next === void 0) return null;
|
|
782
|
+
first ??= next;
|
|
783
|
+
text2 += next;
|
|
550
784
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
function shutByCompletion(tree, limit) {
|
|
788
|
+
const shut = [];
|
|
789
|
+
walk(tree.program, [], (node) => {
|
|
790
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) >= limit)
|
|
791
|
+
shut.push(node);
|
|
792
|
+
});
|
|
793
|
+
return shut.sort((a, b) => (a.closingElement?.start ?? 0) - (b.closingElement?.start ?? 0));
|
|
794
|
+
}
|
|
795
|
+
var byPlace = (a, b) => a.position - b.position;
|
|
796
|
+
var MAX_EXPLAINED = 2e5;
|
|
797
|
+
function readSyntax(file, text2, babel = loadBabel()) {
|
|
798
|
+
const first = attempt(babel, text2, file);
|
|
799
|
+
if (first.tree !== null) {
|
|
800
|
+
const problems = problemsOf(first.tree, first.errors, Infinity, /* @__PURE__ */ new Set());
|
|
801
|
+
return problems.length === 0 ? { tree: first.tree, problems: [] } : { tree: null, problems: problems.sort(byPlace) };
|
|
565
802
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
803
|
+
let fatal = first.fatal;
|
|
804
|
+
if (text2.length > MAX_EXPLAINED) return { tree: null, problems: [problemOf(fatal)] };
|
|
805
|
+
if (/\.tsx$/.test(file) && fatal.reasonCode !== "UnterminatedJsxContent") {
|
|
806
|
+
const plain = text2.replace(TYPE_ARGUMENTS, blankedOut);
|
|
807
|
+
if (plain !== text2) {
|
|
808
|
+
const again = attempt(babel, plain, file);
|
|
809
|
+
if (again.tree === null && again.fatal.pos > fatal.pos) fatal = again.fatal;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
const finished = complete(babel, text2.slice(0, fatal.pos), file);
|
|
814
|
+
if (finished !== null) {
|
|
815
|
+
const limit = fatal.pos;
|
|
816
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
817
|
+
const problems = problemsOf(finished.tree, finished.errors, limit, consumed);
|
|
818
|
+
const open = shutByCompletion(finished.tree, limit).filter(
|
|
819
|
+
(element) => !consumed.has(element)
|
|
820
|
+
);
|
|
821
|
+
const atEnd = fatal.reasonCode === "UnterminatedJsxContent" || fatal.pos >= text2.length;
|
|
822
|
+
const hints = (atEnd ? open : finished.first === CLOSING_TAG ? open.slice(0, 1) : []).map(
|
|
823
|
+
unclosed
|
|
824
|
+
);
|
|
825
|
+
const explained = [...problems, ...hints].some(({ code }) => code === "UnclosedJsxElement");
|
|
826
|
+
problems.push(...hints);
|
|
827
|
+
if (!(explained && fatal.reasonCode === "UnterminatedJsxContent"))
|
|
828
|
+
problems.push(problemOf(fatal));
|
|
829
|
+
return { tree: null, problems: problems.sort(byPlace) };
|
|
830
|
+
}
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
return { tree: null, problems: [problemOf(fatal)] };
|
|
569
834
|
}
|
|
570
835
|
|
|
571
836
|
// src/check.ts
|
|
837
|
+
var FINDING_CODES = [
|
|
838
|
+
"INVALID_PROP_VALUE",
|
|
839
|
+
"UNKNOWN_EXPORT",
|
|
840
|
+
"DOT_NOTATION",
|
|
841
|
+
"INVALID_NESTING",
|
|
842
|
+
"INVALID_CHILD",
|
|
843
|
+
"MISSING_REQUIRED_PROP",
|
|
844
|
+
"UNKNOWN_PROP",
|
|
845
|
+
"RAW_COLOR",
|
|
846
|
+
"ARBITRARY_SIZE",
|
|
847
|
+
"ROUNDED_CORNER",
|
|
848
|
+
"NATIVE_CONTROL",
|
|
849
|
+
/** The file does not parse. It was not checked against the contract. */
|
|
850
|
+
"SYNTAX_ERROR"
|
|
851
|
+
];
|
|
852
|
+
var LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
|
|
853
|
+
var SCOPE = {
|
|
854
|
+
checked: ["syntax", "contract", "house_rules"],
|
|
855
|
+
not_checked: ["types", "render"]
|
|
856
|
+
};
|
|
572
857
|
var PACKAGE = "@oprt/react";
|
|
573
858
|
var NATIVE = {
|
|
574
859
|
button: "Button",
|
|
@@ -604,19 +889,48 @@ function allowedChildren2(rules, name) {
|
|
|
604
889
|
}
|
|
605
890
|
return void 0;
|
|
606
891
|
}
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
892
|
+
var quote = (text2) => {
|
|
893
|
+
const flat = text2.replace(/\s+/g, " ").trim();
|
|
894
|
+
return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
|
|
895
|
+
};
|
|
896
|
+
var MAX_SYNTAX_ERRORS = 10;
|
|
897
|
+
function checkSource(contract2, file, text2) {
|
|
898
|
+
const reading = readSyntax(file, text2);
|
|
899
|
+
if (reading.tree === null) {
|
|
900
|
+
const shown = reading.problems.slice(0, MAX_SYNTAX_ERRORS);
|
|
901
|
+
const more = reading.problems.length - shown.length;
|
|
902
|
+
return {
|
|
903
|
+
file,
|
|
904
|
+
status: "not_evaluated",
|
|
905
|
+
limitations: [],
|
|
906
|
+
findings: shown.map(
|
|
907
|
+
({ line, column, message, code }, index) => ({
|
|
908
|
+
code: "SYNTAX_ERROR",
|
|
909
|
+
severity: "error",
|
|
910
|
+
file,
|
|
911
|
+
line,
|
|
912
|
+
column,
|
|
913
|
+
message: `${message} (${code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
|
|
914
|
+
})
|
|
915
|
+
)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
611
918
|
const findings = [];
|
|
919
|
+
const limitations = [];
|
|
920
|
+
const place = (node) => ({
|
|
921
|
+
line: node.loc?.start.line ?? 1,
|
|
922
|
+
column: (node.loc?.start.column ?? 0) + 1
|
|
923
|
+
});
|
|
924
|
+
const textOf = (node) => text2.slice(node.start ?? 0, node.end ?? 0);
|
|
925
|
+
const limit = (node, limitation) => {
|
|
926
|
+
limitations.push({ ...limitation, file, ...place(node) });
|
|
927
|
+
};
|
|
612
928
|
const report = (node, finding) => {
|
|
613
|
-
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
614
929
|
findings.push({
|
|
615
930
|
code: finding.code,
|
|
616
931
|
severity: finding.severity,
|
|
617
932
|
file,
|
|
618
|
-
|
|
619
|
-
column: character + 1,
|
|
933
|
+
...place(node),
|
|
620
934
|
...finding.component === void 0 ? {} : { component: finding.component },
|
|
621
935
|
...finding.prop === void 0 ? {} : { prop: finding.prop },
|
|
622
936
|
message: finding.message,
|
|
@@ -627,21 +941,18 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
627
941
|
const isComponent = (name) => Object.hasOwn(contract2.components, name);
|
|
628
942
|
const named = /* @__PURE__ */ new Map();
|
|
629
943
|
const namespaces = /* @__PURE__ */ new Set();
|
|
630
|
-
for (const statement of
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
const from = statement.moduleSpecifier.text;
|
|
634
|
-
const clause = statement.importClause;
|
|
635
|
-
if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
|
|
944
|
+
for (const statement of reading.tree.program.body) {
|
|
945
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
946
|
+
const from = statement.source.value;
|
|
636
947
|
const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent(basename(from));
|
|
637
948
|
if (from !== PACKAGE && !copied) continue;
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (element2.
|
|
644
|
-
const exported =
|
|
949
|
+
for (const element2 of statement.specifiers) {
|
|
950
|
+
if (element2.type === "ImportNamespaceSpecifier") {
|
|
951
|
+
if (from === PACKAGE) namespaces.add(element2.local.name);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (element2.type !== "ImportSpecifier" || element2.importKind === "type") continue;
|
|
955
|
+
const exported = element2.imported.type === "Identifier" ? element2.imported.name : element2.imported.value;
|
|
645
956
|
if (from === PACKAGE && !contract2.exports.includes(exported)) {
|
|
646
957
|
let suggestions = nearest(exported, Object.keys(contract2.components));
|
|
647
958
|
if (suggestions.length === 0) suggestions = nearest(exported, contract2.exports);
|
|
@@ -666,26 +977,55 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
666
977
|
});
|
|
667
978
|
continue;
|
|
668
979
|
}
|
|
669
|
-
if (isComponent(exported)) named.set(element2.name
|
|
980
|
+
if (isComponent(exported)) named.set(element2.local.name, exported);
|
|
670
981
|
}
|
|
671
982
|
}
|
|
983
|
+
const isString = (node) => node.type === "StringLiteral";
|
|
984
|
+
const plainTemplate = (node) => node.type === "TemplateLiteral" && node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw : void 0;
|
|
672
985
|
const literals = (expression) => {
|
|
673
986
|
if (!expression) return [];
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
if (
|
|
677
|
-
if (
|
|
678
|
-
return [...literals(expression.
|
|
987
|
+
if (isString(expression)) return [expression.value];
|
|
988
|
+
const template = plainTemplate(expression);
|
|
989
|
+
if (template !== void 0) return [template];
|
|
990
|
+
if (expression.type === "ConditionalExpression")
|
|
991
|
+
return [...literals(expression.consequent), ...literals(expression.alternate)];
|
|
679
992
|
return [];
|
|
680
993
|
};
|
|
681
|
-
const valueOf = (attribute) =>
|
|
994
|
+
const valueOf = (attribute) => {
|
|
995
|
+
const value = attribute.value;
|
|
996
|
+
if (value === null || value === void 0) return void 0;
|
|
997
|
+
if (value.type === "JSXExpressionContainer")
|
|
998
|
+
return value.expression.type === "JSXEmptyExpression" ? void 0 : value.expression;
|
|
999
|
+
return value;
|
|
1000
|
+
};
|
|
1001
|
+
const optionValue = (expression) => {
|
|
1002
|
+
const read = [];
|
|
1003
|
+
const unread = [];
|
|
1004
|
+
const walk2 = (node) => {
|
|
1005
|
+
const template = plainTemplate(node);
|
|
1006
|
+
if (isString(node)) read.push(node.value);
|
|
1007
|
+
else if (template !== void 0) read.push(template);
|
|
1008
|
+
else if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "TSSatisfiesExpression")
|
|
1009
|
+
walk2(node.expression);
|
|
1010
|
+
else if (node.type === "ConditionalExpression") {
|
|
1011
|
+
walk2(node.consequent);
|
|
1012
|
+
walk2(node.alternate);
|
|
1013
|
+
} else if (node.type === "LogicalExpression") {
|
|
1014
|
+
if (node.operator === "??" || node.operator === "||") {
|
|
1015
|
+
walk2(node.left);
|
|
1016
|
+
walk2(node.right);
|
|
1017
|
+
} else walk2(node.right);
|
|
1018
|
+
} else if (!// No value: the component's default applies.
|
|
1019
|
+
(node.type === "Identifier" && node.name === "undefined" || node.type === "NullLiteral"))
|
|
1020
|
+
unread.push(node);
|
|
1021
|
+
};
|
|
1022
|
+
if (expression) walk2(expression);
|
|
1023
|
+
return { literals: read, unread };
|
|
1024
|
+
};
|
|
682
1025
|
const classText = (expression) => {
|
|
683
1026
|
if (!expression) return "";
|
|
684
|
-
if (
|
|
685
|
-
return
|
|
686
|
-
expression.head.text,
|
|
687
|
-
...expression.templateSpans.map((span) => span.literal.text)
|
|
688
|
-
].join(" ");
|
|
1027
|
+
if (expression.type === "TemplateLiteral" && expression.expressions.length > 0)
|
|
1028
|
+
return expression.quasis.map((span) => span.value.cooked ?? span.value.raw).join(" ");
|
|
689
1029
|
return literals(expression).join(" ");
|
|
690
1030
|
};
|
|
691
1031
|
const houseRules = (attribute, name) => {
|
|
@@ -711,9 +1051,9 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
711
1051
|
message: "A rounded corner. The radius is 0 everywhere in OPERATOR."
|
|
712
1052
|
});
|
|
713
1053
|
}
|
|
714
|
-
if (name === "style" && value &&
|
|
1054
|
+
if (name === "style" && value && value.type === "ObjectExpression") {
|
|
715
1055
|
for (const property of value.properties)
|
|
716
|
-
if (
|
|
1056
|
+
if (property.type === "ObjectProperty" && literals(property.value).some((literal) => RAW_COLOR_VALUE.test(literal)))
|
|
717
1057
|
report(attribute, {
|
|
718
1058
|
code: "RAW_COLOR",
|
|
719
1059
|
severity: "warning",
|
|
@@ -721,56 +1061,73 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
721
1061
|
});
|
|
722
1062
|
}
|
|
723
1063
|
};
|
|
1064
|
+
const attributeName = (attribute) => attribute.name.type === "JSXIdentifier" ? attribute.name.name : textOf(attribute.name);
|
|
724
1065
|
const element = (opening, ancestors, parent, hasChildren, renderedAs) => {
|
|
725
|
-
const tag = opening.
|
|
1066
|
+
const tag = opening.name;
|
|
726
1067
|
let component = null;
|
|
727
|
-
if (
|
|
728
|
-
component = named.get(tag.
|
|
729
|
-
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.
|
|
1068
|
+
if (tag.type === "JSXIdentifier") {
|
|
1069
|
+
component = named.get(tag.name) ?? null;
|
|
1070
|
+
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.name))
|
|
730
1071
|
report(opening, {
|
|
731
1072
|
code: "NATIVE_CONTROL",
|
|
732
1073
|
severity: "warning",
|
|
733
|
-
message: `A native <${tag.
|
|
1074
|
+
message: `A native <${tag.name}>: use ${NATIVE[tag.name]}.`
|
|
734
1075
|
});
|
|
735
|
-
} else if (
|
|
736
|
-
const owner = tag.
|
|
737
|
-
|
|
1076
|
+
} else if (tag.type === "JSXMemberExpression" && tag.object.type === "JSXIdentifier") {
|
|
1077
|
+
const owner = tag.object.name;
|
|
1078
|
+
const member = tag.property.name;
|
|
1079
|
+
if (namespaces.has(owner) && isComponent(member)) component = member;
|
|
738
1080
|
else if (named.has(owner)) {
|
|
739
|
-
const joined = `${named.get(owner)}${
|
|
1081
|
+
const joined = `${named.get(owner)}${member}`;
|
|
740
1082
|
report(opening, {
|
|
741
1083
|
code: "DOT_NOTATION",
|
|
742
1084
|
severity: "error",
|
|
743
|
-
message: `${owner}.${
|
|
1085
|
+
message: `${owner}.${member} is not a component.` + (isComponent(joined) ? ` Use <${joined}>.` : " OPERATOR's parts are components of their own, imported by name.")
|
|
744
1086
|
});
|
|
745
1087
|
}
|
|
746
1088
|
}
|
|
747
|
-
const attributes = opening.attributes
|
|
1089
|
+
const attributes = opening.attributes;
|
|
748
1090
|
for (const attribute of attributes)
|
|
749
|
-
if (
|
|
750
|
-
houseRules(attribute, attribute.name.
|
|
1091
|
+
if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier")
|
|
1092
|
+
houseRules(attribute, attribute.name.name);
|
|
751
1093
|
if (component === null) return null;
|
|
752
1094
|
const definition = contract2.components[component];
|
|
753
|
-
const spread = attributes.some((attribute) =>
|
|
1095
|
+
const spread = attributes.some((attribute) => attribute.type === "JSXSpreadAttribute");
|
|
1096
|
+
for (const attribute of attributes)
|
|
1097
|
+
if (attribute.type === "JSXSpreadAttribute")
|
|
1098
|
+
limit(attribute, {
|
|
1099
|
+
code: "UNRESOLVED_SPREAD",
|
|
1100
|
+
component,
|
|
1101
|
+
message: `Props spread into <${component}> (${quote(textOf(attribute))}) were not read: option values and required props that arrive through them were not checked.`
|
|
1102
|
+
});
|
|
754
1103
|
const given = /* @__PURE__ */ new Set();
|
|
755
1104
|
for (const attribute of attributes) {
|
|
756
|
-
if (
|
|
757
|
-
const prop =
|
|
1105
|
+
if (attribute.type !== "JSXAttribute") continue;
|
|
1106
|
+
const prop = attributeName(attribute);
|
|
758
1107
|
given.add(prop);
|
|
759
1108
|
const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
|
|
760
1109
|
if (known === void 0) {
|
|
761
1110
|
if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
|
|
762
|
-
const
|
|
1111
|
+
const names2 = Object.keys(definition.props);
|
|
763
1112
|
report(attribute, {
|
|
764
1113
|
code: "UNKNOWN_PROP",
|
|
765
1114
|
severity: "warning",
|
|
766
1115
|
component,
|
|
767
1116
|
prop,
|
|
768
|
-
message: `${component} has no prop ${prop}.` + (
|
|
1117
|
+
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" : ""}.`)
|
|
769
1118
|
});
|
|
770
1119
|
continue;
|
|
771
1120
|
}
|
|
772
1121
|
if (known.type !== "enum" || known.values === void 0) continue;
|
|
773
|
-
|
|
1122
|
+
const { literals: options, unread } = optionValue(valueOf(attribute));
|
|
1123
|
+
for (const expression of unread)
|
|
1124
|
+
limit(attribute, {
|
|
1125
|
+
code: "UNRESOLVED_VALUE",
|
|
1126
|
+
component,
|
|
1127
|
+
prop,
|
|
1128
|
+
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.`
|
|
1129
|
+
});
|
|
1130
|
+
for (const literal of options) {
|
|
774
1131
|
if (known.values.includes(literal)) continue;
|
|
775
1132
|
const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
|
|
776
1133
|
report(attribute, {
|
|
@@ -817,82 +1174,146 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
817
1174
|
return component;
|
|
818
1175
|
};
|
|
819
1176
|
const visit = (node, ancestors, parent, renderedAs = false) => {
|
|
820
|
-
if (
|
|
821
|
-
const opening =
|
|
822
|
-
const hasChildren =
|
|
823
|
-
(child) => !(
|
|
1177
|
+
if (node.type === "JSXElement") {
|
|
1178
|
+
const opening = node.openingElement;
|
|
1179
|
+
const hasChildren = node.children.some(
|
|
1180
|
+
(child) => !(child.type === "JSXText" && /^\s*$/.test(text2.slice(child.start ?? 0, child.end ?? 0)))
|
|
824
1181
|
);
|
|
825
1182
|
const component = element(opening, ancestors, parent, hasChildren, renderedAs);
|
|
826
|
-
const asChild = component !== null && opening.attributes.
|
|
827
|
-
(attribute) =>
|
|
1183
|
+
const asChild = component !== null && opening.attributes.some(
|
|
1184
|
+
(attribute) => attribute.type === "JSXAttribute" && attributeName(attribute) === "asChild"
|
|
828
1185
|
);
|
|
829
|
-
for (const attribute of opening.attributes
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
);
|
|
1186
|
+
for (const attribute of opening.attributes) visit(attribute, [], null);
|
|
1187
|
+
for (const child of node.children)
|
|
1188
|
+
visit(
|
|
1189
|
+
child,
|
|
1190
|
+
component === null ? ancestors : [...ancestors, component],
|
|
1191
|
+
component,
|
|
1192
|
+
asChild
|
|
1193
|
+
);
|
|
838
1194
|
return;
|
|
839
1195
|
}
|
|
840
|
-
|
|
1196
|
+
for (const child of childNodes(node)) visit(child, ancestors, parent);
|
|
1197
|
+
};
|
|
1198
|
+
visit(reading.tree.program, [], null);
|
|
1199
|
+
const byPlace2 = (a, b) => a.line - b.line || a.column - b.column;
|
|
1200
|
+
return {
|
|
1201
|
+
file,
|
|
1202
|
+
status: limitations.length > 0 ? "partial" : "evaluated",
|
|
1203
|
+
findings: findings.sort(byPlace2),
|
|
1204
|
+
limitations: limitations.sort(byPlace2)
|
|
841
1205
|
};
|
|
842
|
-
visit(source, [], null);
|
|
843
|
-
return findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
844
1206
|
}
|
|
845
1207
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
846
1208
|
var READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
847
1209
|
var NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
1210
|
+
var excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
|
|
848
1211
|
function* sources(path) {
|
|
849
|
-
|
|
850
|
-
if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
|
|
1212
|
+
if (excluded(basename(path))) return;
|
|
851
1213
|
if (statSync(path).isDirectory()) {
|
|
852
1214
|
for (const entry of readdirSync(path).sort()) yield* sources(join2(path, entry));
|
|
853
|
-
} else if (READ.has(extname(path)) && !NOT_INTERFACE.test(
|
|
1215
|
+
} else if (READ.has(extname(path)) && !NOT_INTERFACE.test(basename(path))) yield path;
|
|
854
1216
|
}
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1217
|
+
function whyNotRead(path) {
|
|
1218
|
+
const name = basename(path);
|
|
1219
|
+
if (excluded(name)) return "excluded_folder";
|
|
1220
|
+
if (statSync(path).isDirectory()) return null;
|
|
1221
|
+
if (!READ.has(extname(path))) return "unsupported_extension";
|
|
1222
|
+
return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
|
|
1223
|
+
}
|
|
1224
|
+
function buildCheckReport(evaluations, context = {}) {
|
|
1225
|
+
const findings = evaluations.flatMap(({ findings: found }) => found);
|
|
1226
|
+
const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
|
|
1227
|
+
const partial = count("partial");
|
|
1228
|
+
const notEvaluated = count("not_evaluated");
|
|
1229
|
+
const evaluated = evaluations.length - notEvaluated;
|
|
1230
|
+
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1231
|
+
const warnings = findings.length - errors;
|
|
1232
|
+
let status;
|
|
1233
|
+
if (evaluations.length === 0) status = "no_files_evaluated";
|
|
1234
|
+
else if (evaluated === 0) status = "not_evaluated";
|
|
1235
|
+
else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
|
|
1236
|
+
else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
|
|
1237
|
+
return {
|
|
1238
|
+
evaluation_status: status,
|
|
1239
|
+
files: evaluations.length,
|
|
1240
|
+
findings,
|
|
1241
|
+
summary: { errors, warnings },
|
|
1242
|
+
coverage: {
|
|
1243
|
+
files_found: evaluations.length,
|
|
1244
|
+
files_evaluated: evaluated,
|
|
1245
|
+
files_partial: partial,
|
|
1246
|
+
files_not_evaluated: notEvaluated
|
|
1247
|
+
},
|
|
1248
|
+
limitations: evaluations.flatMap(({ limitations }) => limitations),
|
|
1249
|
+
skipped_inputs: context.skipped ?? [],
|
|
1250
|
+
scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
|
|
1251
|
+
parser: context.parser ?? PARSER
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function checkExitCode(report, { strict }) {
|
|
1255
|
+
if (report.evaluation_status === "no_files_evaluated") return 1;
|
|
1256
|
+
if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
|
|
1257
|
+
return strict && report.summary.warnings > 0 ? 1 : 0;
|
|
863
1258
|
}
|
|
864
1259
|
function checkPaths(paths, context) {
|
|
865
1260
|
const root = resolve2(context.cwd);
|
|
866
1261
|
const files = [];
|
|
1262
|
+
const skipped = [];
|
|
867
1263
|
for (const path of paths) {
|
|
868
1264
|
const absolute = resolve2(root, path);
|
|
869
1265
|
if (!existsSync2(absolute))
|
|
870
1266
|
throw new CliError(`No such file or folder: ${path}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
1267
|
+
const reason = whyNotRead(absolute);
|
|
1268
|
+
if (reason !== null) skipped.push({ path, reason });
|
|
871
1269
|
files.push(...sources(absolute));
|
|
872
1270
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
throw new CliError(
|
|
876
|
-
"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.",
|
|
877
|
-
1,
|
|
878
|
-
{ code: "ERR_TYPESCRIPT_MISSING" }
|
|
879
|
-
);
|
|
880
|
-
const findings = [...new Set(files)].flatMap(
|
|
1271
|
+
loadBabel();
|
|
1272
|
+
const evaluations = [...new Set(files)].map(
|
|
881
1273
|
(file) => checkSource(
|
|
882
|
-
ts,
|
|
883
1274
|
context.contract,
|
|
884
1275
|
relative2(root, file).split(sep2).join("/"),
|
|
885
1276
|
readFileSync3(file, "utf8")
|
|
886
1277
|
)
|
|
887
1278
|
);
|
|
888
|
-
return {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
1279
|
+
return buildCheckReport(evaluations, { skipped });
|
|
1280
|
+
}
|
|
1281
|
+
var plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1282
|
+
var MAX_LIMITS_SHOWN = 20;
|
|
1283
|
+
function renderCheckText(report) {
|
|
1284
|
+
const lines = report.findings.map(
|
|
1285
|
+
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1286
|
+
);
|
|
1287
|
+
const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
|
|
1288
|
+
for (const { file, line, column, code, message } of shown)
|
|
1289
|
+
lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
|
|
1290
|
+
if (report.limitations.length > shown.length)
|
|
1291
|
+
lines.push(
|
|
1292
|
+
` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
|
|
1293
|
+
);
|
|
1294
|
+
const { coverage, evaluation_status: status } = report;
|
|
1295
|
+
if (status === "no_files_evaluated") {
|
|
1296
|
+
lines.push("No .tsx or .jsx file was read, so nothing was checked.");
|
|
1297
|
+
const labels = {
|
|
1298
|
+
unsupported_extension: "not a .tsx or .jsx file",
|
|
1299
|
+
test_or_story_file: "a test, spec or story file, which is skipped",
|
|
1300
|
+
excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
|
|
1301
|
+
};
|
|
1302
|
+
for (const { path, reason } of report.skipped_inputs)
|
|
1303
|
+
lines.push(` ${path}: ${labels[reason]}`);
|
|
1304
|
+
} else if (coverage.files_not_evaluated > 0)
|
|
1305
|
+
lines.push(
|
|
1306
|
+
`${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
|
|
1307
|
+
);
|
|
1308
|
+
else if (status === "evaluated_clean")
|
|
1309
|
+
lines.push(
|
|
1310
|
+
"Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
|
|
1311
|
+
);
|
|
1312
|
+
lines.push(
|
|
1313
|
+
`${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
|
|
1314
|
+
);
|
|
1315
|
+
return `${lines.join("\n")}
|
|
1316
|
+
`;
|
|
896
1317
|
}
|
|
897
1318
|
|
|
898
1319
|
// src/doctor.ts
|
|
@@ -979,7 +1400,7 @@ var INSTRUCTION_FILES = [
|
|
|
979
1400
|
".claude/CLAUDE.md",
|
|
980
1401
|
".cursor/rules/operator.mdc"
|
|
981
1402
|
];
|
|
982
|
-
var minor = (
|
|
1403
|
+
var minor = (version2) => version2.split(".").slice(0, 2).join(".");
|
|
983
1404
|
function installedVersion(cwd, name) {
|
|
984
1405
|
for (let folder = cwd; ; folder = dirname3(folder)) {
|
|
985
1406
|
const manifest = join3(folder, "node_modules", name, "package.json");
|
|
@@ -988,26 +1409,30 @@ function installedVersion(cwd, name) {
|
|
|
988
1409
|
if (dirname3(folder) === folder) return void 0;
|
|
989
1410
|
}
|
|
990
1411
|
}
|
|
991
|
-
function doctor({
|
|
1412
|
+
function doctor({
|
|
1413
|
+
cwd,
|
|
1414
|
+
contract: contract2,
|
|
1415
|
+
parser = probeParser()
|
|
1416
|
+
}) {
|
|
992
1417
|
const checks = [];
|
|
993
1418
|
for (const name of PACKAGES) {
|
|
994
|
-
const
|
|
1419
|
+
const version2 = installedVersion(cwd, name);
|
|
995
1420
|
const id = `package:${name}`;
|
|
996
|
-
if (
|
|
1421
|
+
if (version2 === void 0)
|
|
997
1422
|
checks.push({
|
|
998
1423
|
id,
|
|
999
1424
|
status: "warn",
|
|
1000
1425
|
message: `${name} is not installed.`,
|
|
1001
1426
|
fix: `Install ${name}, or copy the sources you need with oprt add.`
|
|
1002
1427
|
});
|
|
1003
|
-
else if (minor(
|
|
1428
|
+
else if (minor(version2) !== minor(contract2.version))
|
|
1004
1429
|
checks.push({
|
|
1005
1430
|
id,
|
|
1006
1431
|
status: "warn",
|
|
1007
|
-
message: `${name} is ${
|
|
1432
|
+
message: `${name} is ${version2}; this installer describes ${contract2.version}.`,
|
|
1008
1433
|
fix: "Install matching versions of the OPERATOR packages and @oprt/cli."
|
|
1009
1434
|
});
|
|
1010
|
-
else checks.push({ id, status: "ok", message: `${name} ${
|
|
1435
|
+
else checks.push({ id, status: "ok", message: `${name} ${version2} is installed.` });
|
|
1011
1436
|
}
|
|
1012
1437
|
const written = INSTRUCTION_FILES.map((path) => {
|
|
1013
1438
|
const file = join3(cwd, path);
|
|
@@ -1015,8 +1440,8 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1015
1440
|
path,
|
|
1016
1441
|
version: existsSync3(file) ? blockVersion(readFileSync4(file, "utf8")) : void 0
|
|
1017
1442
|
};
|
|
1018
|
-
}).filter(({ version }) =>
|
|
1019
|
-
const stale = written.find(({ version }) =>
|
|
1443
|
+
}).filter(({ version: version2 }) => version2 !== void 0);
|
|
1444
|
+
const stale = written.find(({ version: version2 }) => version2 !== contract2.version);
|
|
1020
1445
|
if (written.length === 0)
|
|
1021
1446
|
checks.push({
|
|
1022
1447
|
id: "agent-instructions",
|
|
@@ -1037,8 +1462,48 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1037
1462
|
status: "ok",
|
|
1038
1463
|
message: `${written.map(({ path }) => path).join(", ")} carries the OPERATOR block (${contract2.version}).`
|
|
1039
1464
|
});
|
|
1465
|
+
let checkCode;
|
|
1466
|
+
if (parser.ready) {
|
|
1467
|
+
const { name, version: version2 } = parser.parser;
|
|
1468
|
+
checkCode = {
|
|
1469
|
+
status: "ready",
|
|
1470
|
+
message: `${name} ${version2}, bundled with oprt: nothing to install.`,
|
|
1471
|
+
parser: parser.parser
|
|
1472
|
+
};
|
|
1473
|
+
checks.push({
|
|
1474
|
+
id: "parser",
|
|
1475
|
+
status: "ok",
|
|
1476
|
+
message: `Code checking reads code with ${name} ${version2}, which ships inside oprt.`
|
|
1477
|
+
});
|
|
1478
|
+
} else {
|
|
1479
|
+
const fix = "Install @oprt/cli again, then run oprt doctor again. Lookups work without it.";
|
|
1480
|
+
checkCode = {
|
|
1481
|
+
status: "blocked",
|
|
1482
|
+
message: `${parser.reason}: ${parser.detail}`,
|
|
1483
|
+
reason: parser.reason,
|
|
1484
|
+
fix
|
|
1485
|
+
};
|
|
1486
|
+
checks.push({
|
|
1487
|
+
id: "parser",
|
|
1488
|
+
status: "fail",
|
|
1489
|
+
message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
|
|
1490
|
+
fix
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1040
1493
|
const count = (status) => checks.filter((check) => check.status === status).length;
|
|
1041
|
-
return {
|
|
1494
|
+
return {
|
|
1495
|
+
consumer_directory: cwd,
|
|
1496
|
+
operations: {
|
|
1497
|
+
lookup: {
|
|
1498
|
+
status: "ready",
|
|
1499
|
+
message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
|
|
1500
|
+
contract_version: contract2.version
|
|
1501
|
+
},
|
|
1502
|
+
check_code: checkCode
|
|
1503
|
+
},
|
|
1504
|
+
checks,
|
|
1505
|
+
summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
|
|
1506
|
+
};
|
|
1042
1507
|
}
|
|
1043
1508
|
|
|
1044
1509
|
// src/init.ts
|
|
@@ -1096,8 +1561,532 @@ function init(options, context) {
|
|
|
1096
1561
|
return { version: context.contract.version, dryRun, files };
|
|
1097
1562
|
}
|
|
1098
1563
|
|
|
1564
|
+
// src/results.ts
|
|
1565
|
+
var RESULT_TYPES = [
|
|
1566
|
+
"check",
|
|
1567
|
+
"search",
|
|
1568
|
+
"component.detail",
|
|
1569
|
+
"component.batch",
|
|
1570
|
+
"component.list",
|
|
1571
|
+
"init",
|
|
1572
|
+
"doctor",
|
|
1573
|
+
"add",
|
|
1574
|
+
"list",
|
|
1575
|
+
"instructions"
|
|
1576
|
+
];
|
|
1577
|
+
var envelope = (type, data, contract2) => ({
|
|
1578
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1579
|
+
type,
|
|
1580
|
+
identity: identityOf(contract2),
|
|
1581
|
+
data
|
|
1582
|
+
});
|
|
1583
|
+
function errorEnvelope(error, contract2) {
|
|
1584
|
+
const known = error instanceof CliError ? error : void 0;
|
|
1585
|
+
const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
|
|
1586
|
+
cli_version,
|
|
1587
|
+
schema_version,
|
|
1588
|
+
contract_version,
|
|
1589
|
+
contract_digest
|
|
1590
|
+
}))(identityOf(contract2));
|
|
1591
|
+
return {
|
|
1592
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1593
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1594
|
+
code: known?.code ?? "ERR_FAILED",
|
|
1595
|
+
...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
|
|
1596
|
+
identity
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function componentData(detail, dense) {
|
|
1600
|
+
if (!dense) return { ...detail, dense };
|
|
1601
|
+
const { description: _description, ...rest } = detail;
|
|
1602
|
+
return {
|
|
1603
|
+
...rest,
|
|
1604
|
+
props: detail.props.map(
|
|
1605
|
+
(prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
|
|
1606
|
+
),
|
|
1607
|
+
dense
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function instructionsData(contract2) {
|
|
1611
|
+
return {
|
|
1612
|
+
rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
|
|
1613
|
+
id,
|
|
1614
|
+
rule,
|
|
1615
|
+
...wrong === void 0 ? {} : { wrong },
|
|
1616
|
+
...right === void 0 ? {} : { right }
|
|
1617
|
+
})),
|
|
1618
|
+
components: componentIndex(contract2),
|
|
1619
|
+
recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
|
|
1620
|
+
name,
|
|
1621
|
+
description,
|
|
1622
|
+
install: install2
|
|
1623
|
+
}))
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
var str = (extra = {}) => ({ type: "string", ...extra });
|
|
1627
|
+
var int = () => ({ type: "integer", minimum: 0 });
|
|
1628
|
+
var bool = () => ({ type: "boolean" });
|
|
1629
|
+
var arr = (items) => ({ type: "array", items });
|
|
1630
|
+
var names = () => arr(str());
|
|
1631
|
+
var words2 = (...values) => ({ type: "string", enum: [...values] });
|
|
1632
|
+
var ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
1633
|
+
var obj = (properties, optional = [], open = false) => ({
|
|
1634
|
+
type: "object",
|
|
1635
|
+
properties,
|
|
1636
|
+
required: Object.keys(properties).filter((key) => !optional.includes(key)),
|
|
1637
|
+
additionalProperties: open
|
|
1638
|
+
});
|
|
1639
|
+
var scalar = { type: ["string", "number", "boolean"] };
|
|
1640
|
+
var DEFS = {
|
|
1641
|
+
identity: obj({
|
|
1642
|
+
cli_version: str(),
|
|
1643
|
+
contract_version: str(),
|
|
1644
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
|
|
1645
|
+
schema_version: { type: "integer", minimum: 1 }
|
|
1646
|
+
}),
|
|
1647
|
+
error_identity: obj(
|
|
1648
|
+
{
|
|
1649
|
+
cli_version: str(),
|
|
1650
|
+
schema_version: { type: "integer", minimum: 1 },
|
|
1651
|
+
contract_version: str(),
|
|
1652
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
|
|
1653
|
+
},
|
|
1654
|
+
["contract_version", "contract_digest"]
|
|
1655
|
+
),
|
|
1656
|
+
finding: obj(
|
|
1657
|
+
{
|
|
1658
|
+
code: words2(...FINDING_CODES),
|
|
1659
|
+
severity: words2("error", "warning"),
|
|
1660
|
+
file: str(),
|
|
1661
|
+
line: { type: "integer", minimum: 1 },
|
|
1662
|
+
column: { type: "integer", minimum: 1 },
|
|
1663
|
+
component: str(),
|
|
1664
|
+
prop: str(),
|
|
1665
|
+
message: str(),
|
|
1666
|
+
valid_values: names(),
|
|
1667
|
+
suggestions: names()
|
|
1668
|
+
},
|
|
1669
|
+
["component", "prop", "valid_values", "suggestions"]
|
|
1670
|
+
),
|
|
1671
|
+
limitation: obj(
|
|
1672
|
+
{
|
|
1673
|
+
code: words2(...LIMITATION_CODES),
|
|
1674
|
+
file: str(),
|
|
1675
|
+
line: { type: "integer", minimum: 1 },
|
|
1676
|
+
column: { type: "integer", minimum: 1 },
|
|
1677
|
+
component: str(),
|
|
1678
|
+
prop: str(),
|
|
1679
|
+
message: str()
|
|
1680
|
+
},
|
|
1681
|
+
["prop"]
|
|
1682
|
+
),
|
|
1683
|
+
skipped_input: obj({
|
|
1684
|
+
path: str(),
|
|
1685
|
+
reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
|
|
1686
|
+
}),
|
|
1687
|
+
parser: obj({
|
|
1688
|
+
name: words2("@babel/parser"),
|
|
1689
|
+
version: str(),
|
|
1690
|
+
source: words2("bundled")
|
|
1691
|
+
}),
|
|
1692
|
+
check_data: obj({
|
|
1693
|
+
evaluation_status: words2(
|
|
1694
|
+
"evaluated_clean",
|
|
1695
|
+
"evaluated_with_findings",
|
|
1696
|
+
"partially_evaluated",
|
|
1697
|
+
"not_evaluated",
|
|
1698
|
+
"no_files_evaluated"
|
|
1699
|
+
),
|
|
1700
|
+
files: int(),
|
|
1701
|
+
findings: arr(ref("finding")),
|
|
1702
|
+
summary: obj({ errors: int(), warnings: int() }),
|
|
1703
|
+
coverage: obj({
|
|
1704
|
+
files_found: int(),
|
|
1705
|
+
files_evaluated: int(),
|
|
1706
|
+
files_partial: int(),
|
|
1707
|
+
files_not_evaluated: int()
|
|
1708
|
+
}),
|
|
1709
|
+
limitations: arr(ref("limitation")),
|
|
1710
|
+
skipped_inputs: arr(ref("skipped_input")),
|
|
1711
|
+
scope: obj({ checked: names(), not_checked: names() }),
|
|
1712
|
+
parser: ref("parser")
|
|
1713
|
+
}),
|
|
1714
|
+
search_result: obj(
|
|
1715
|
+
{
|
|
1716
|
+
kind: words2("component", "recipe"),
|
|
1717
|
+
name: str(),
|
|
1718
|
+
purpose: str(),
|
|
1719
|
+
score: { type: "number" },
|
|
1720
|
+
why: str(),
|
|
1721
|
+
next: str()
|
|
1722
|
+
},
|
|
1723
|
+
["why"]
|
|
1724
|
+
),
|
|
1725
|
+
search_data: obj({
|
|
1726
|
+
query: str(),
|
|
1727
|
+
limit: { type: "integer", minimum: 1 },
|
|
1728
|
+
matchCount: int(),
|
|
1729
|
+
results: arr(ref("search_result"))
|
|
1730
|
+
}),
|
|
1731
|
+
index_entry: obj({ name: str(), purpose: str(), parts: names() }),
|
|
1732
|
+
component_list_data: obj({ components: arr(ref("index_entry")) }),
|
|
1733
|
+
detail_prop: obj(
|
|
1734
|
+
{
|
|
1735
|
+
name: str(),
|
|
1736
|
+
type: str(),
|
|
1737
|
+
values: names(),
|
|
1738
|
+
types: names(),
|
|
1739
|
+
default: scalar,
|
|
1740
|
+
required: bool(),
|
|
1741
|
+
description: str(),
|
|
1742
|
+
notes: { type: "object", additionalProperties: str() }
|
|
1743
|
+
},
|
|
1744
|
+
["values", "types", "default", "required", "description", "notes"],
|
|
1745
|
+
// The contract may describe a prop with more than these (an array's item type, for one).
|
|
1746
|
+
true
|
|
1747
|
+
),
|
|
1748
|
+
component_detail: obj(
|
|
1749
|
+
{
|
|
1750
|
+
name: str(),
|
|
1751
|
+
description: str(),
|
|
1752
|
+
use_for: names(),
|
|
1753
|
+
not_for: arr(obj({ need: str(), use: str() })),
|
|
1754
|
+
part_of: str(),
|
|
1755
|
+
parts: names(),
|
|
1756
|
+
props: arr(ref("detail_prop")),
|
|
1757
|
+
slots: {
|
|
1758
|
+
type: "object",
|
|
1759
|
+
additionalProperties: obj({ type: str(), required: bool() }, ["required"])
|
|
1760
|
+
},
|
|
1761
|
+
composable_with: names(),
|
|
1762
|
+
never_inside: names(),
|
|
1763
|
+
allowed_children: names(),
|
|
1764
|
+
example: str(),
|
|
1765
|
+
dense: bool()
|
|
1766
|
+
},
|
|
1767
|
+
["description", "part_of", "slots", "allowed_children"]
|
|
1768
|
+
),
|
|
1769
|
+
component_batch_data: obj({ components: arr(ref("component_detail")) }),
|
|
1770
|
+
doctor_check: obj(
|
|
1771
|
+
{ id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
|
|
1772
|
+
["fix"]
|
|
1773
|
+
),
|
|
1774
|
+
doctor_data: obj({
|
|
1775
|
+
consumer_directory: str(),
|
|
1776
|
+
operations: obj({
|
|
1777
|
+
lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
|
|
1778
|
+
check_code: obj(
|
|
1779
|
+
{
|
|
1780
|
+
status: words2("ready", "blocked"),
|
|
1781
|
+
message: str(),
|
|
1782
|
+
reason: words2("parser_missing", "parser_unusable"),
|
|
1783
|
+
fix: str(),
|
|
1784
|
+
parser: ref("parser")
|
|
1785
|
+
},
|
|
1786
|
+
["reason", "fix", "parser"]
|
|
1787
|
+
)
|
|
1788
|
+
}),
|
|
1789
|
+
checks: arr(ref("doctor_check")),
|
|
1790
|
+
summary: obj({ ok: int(), warn: int(), fail: int() })
|
|
1791
|
+
}),
|
|
1792
|
+
init_data: obj({
|
|
1793
|
+
version: str(),
|
|
1794
|
+
dryRun: bool(),
|
|
1795
|
+
files: arr(
|
|
1796
|
+
obj({
|
|
1797
|
+
path: str(),
|
|
1798
|
+
action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
|
|
1799
|
+
})
|
|
1800
|
+
)
|
|
1801
|
+
}),
|
|
1802
|
+
install_file: obj({
|
|
1803
|
+
path: str(),
|
|
1804
|
+
destination: str(),
|
|
1805
|
+
action: words2("create", "overwrite", "skip")
|
|
1806
|
+
}),
|
|
1807
|
+
add_data: obj({
|
|
1808
|
+
outputDirectory: str(),
|
|
1809
|
+
entries: names(),
|
|
1810
|
+
dependencies: names(),
|
|
1811
|
+
devDependencies: names(),
|
|
1812
|
+
ranges: { type: "object", additionalProperties: str() },
|
|
1813
|
+
files: arr(ref("install_file")),
|
|
1814
|
+
dryRun: bool(),
|
|
1815
|
+
setup: ref("setup")
|
|
1816
|
+
}),
|
|
1817
|
+
setup: obj({
|
|
1818
|
+
tailwind: str(),
|
|
1819
|
+
packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
|
|
1820
|
+
stylesheet: arr(obj({ line: str(), why: str() })),
|
|
1821
|
+
root_import: obj({ line: str(), note: str() }),
|
|
1822
|
+
not_needed: arr(obj({ name: str(), why: str() }))
|
|
1823
|
+
}),
|
|
1824
|
+
list_data: obj({
|
|
1825
|
+
version: str(),
|
|
1826
|
+
entries: arr(
|
|
1827
|
+
obj(
|
|
1828
|
+
{
|
|
1829
|
+
name: str(),
|
|
1830
|
+
slug: str(),
|
|
1831
|
+
kind: words2("component", "recipe"),
|
|
1832
|
+
description: str(),
|
|
1833
|
+
files: names(),
|
|
1834
|
+
dependencies: names(),
|
|
1835
|
+
devDependencies: names()
|
|
1836
|
+
},
|
|
1837
|
+
["devDependencies"]
|
|
1838
|
+
)
|
|
1839
|
+
),
|
|
1840
|
+
ranges: { type: "object", additionalProperties: str() }
|
|
1841
|
+
}),
|
|
1842
|
+
instructions_data: obj({
|
|
1843
|
+
rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
|
|
1844
|
+
components: arr(ref("index_entry")),
|
|
1845
|
+
recipes: arr(obj({ name: str(), description: str(), install: str() }))
|
|
1846
|
+
})
|
|
1847
|
+
};
|
|
1848
|
+
var DATA = {
|
|
1849
|
+
check: "check_data",
|
|
1850
|
+
search: "search_data",
|
|
1851
|
+
"component.detail": "component_detail",
|
|
1852
|
+
"component.batch": "component_batch_data",
|
|
1853
|
+
"component.list": "component_list_data",
|
|
1854
|
+
init: "init_data",
|
|
1855
|
+
doctor: "doctor_data",
|
|
1856
|
+
add: "add_data",
|
|
1857
|
+
list: "list_data",
|
|
1858
|
+
instructions: "instructions_data"
|
|
1859
|
+
};
|
|
1860
|
+
var success = (type) => obj({
|
|
1861
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1862
|
+
type: { const: type },
|
|
1863
|
+
identity: ref("identity"),
|
|
1864
|
+
data: ref(DATA[type])
|
|
1865
|
+
});
|
|
1866
|
+
var failure = () => obj(
|
|
1867
|
+
{
|
|
1868
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1869
|
+
error: str(),
|
|
1870
|
+
code: str({ pattern: "^ERR_[A-Z_]+$" }),
|
|
1871
|
+
suggestions: names(),
|
|
1872
|
+
identity: ref("error_identity")
|
|
1873
|
+
},
|
|
1874
|
+
["suggestions"]
|
|
1875
|
+
);
|
|
1876
|
+
function bundle(root) {
|
|
1877
|
+
const used = {};
|
|
1878
|
+
const visit = (node) => {
|
|
1879
|
+
if (Array.isArray(node)) node.forEach(visit);
|
|
1880
|
+
else if (typeof node === "object" && node !== null) {
|
|
1881
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1882
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1883
|
+
const name = value.replace("#/$defs/", "");
|
|
1884
|
+
if (used[name] === void 0) {
|
|
1885
|
+
used[name] = DEFS[name];
|
|
1886
|
+
visit(DEFS[name]);
|
|
1887
|
+
}
|
|
1888
|
+
} else visit(value);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
visit(root);
|
|
1893
|
+
return { ...root, $defs: used };
|
|
1894
|
+
}
|
|
1895
|
+
function schemaFor(type) {
|
|
1896
|
+
return bundle(type === "error" ? failure() : success(type));
|
|
1897
|
+
}
|
|
1898
|
+
var S = { type: "string" };
|
|
1899
|
+
var I = { type: "integer" };
|
|
1900
|
+
var B = { type: "boolean" };
|
|
1901
|
+
var list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
|
|
1902
|
+
var shape = (properties, required) => ({
|
|
1903
|
+
type: "object",
|
|
1904
|
+
properties,
|
|
1905
|
+
...required === void 0 ? {} : { required }
|
|
1906
|
+
});
|
|
1907
|
+
var strings = list(S);
|
|
1908
|
+
var SUMMARIES = {
|
|
1909
|
+
check: shape(
|
|
1910
|
+
{
|
|
1911
|
+
evaluation_status: words2(
|
|
1912
|
+
"evaluated_clean",
|
|
1913
|
+
"evaluated_with_findings",
|
|
1914
|
+
"partially_evaluated",
|
|
1915
|
+
"not_evaluated",
|
|
1916
|
+
"no_files_evaluated"
|
|
1917
|
+
),
|
|
1918
|
+
findings: list(
|
|
1919
|
+
shape({
|
|
1920
|
+
code: S,
|
|
1921
|
+
severity: words2("error", "warning"),
|
|
1922
|
+
file: S,
|
|
1923
|
+
line: I,
|
|
1924
|
+
column: I,
|
|
1925
|
+
message: S
|
|
1926
|
+
})
|
|
1927
|
+
),
|
|
1928
|
+
summary: shape({ errors: I, warnings: I }),
|
|
1929
|
+
coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
|
|
1930
|
+
limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
|
|
1931
|
+
skipped_inputs: list(),
|
|
1932
|
+
scope: { type: "object" },
|
|
1933
|
+
parser: { type: "object" }
|
|
1934
|
+
},
|
|
1935
|
+
["evaluation_status", "findings", "summary", "coverage", "limitations"]
|
|
1936
|
+
),
|
|
1937
|
+
search: shape(
|
|
1938
|
+
{
|
|
1939
|
+
matchCount: I,
|
|
1940
|
+
results: list(
|
|
1941
|
+
shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
|
|
1942
|
+
)
|
|
1943
|
+
},
|
|
1944
|
+
["results"]
|
|
1945
|
+
),
|
|
1946
|
+
"component.detail": shape(
|
|
1947
|
+
{
|
|
1948
|
+
name: S,
|
|
1949
|
+
description: S,
|
|
1950
|
+
use_for: strings,
|
|
1951
|
+
not_for: list(),
|
|
1952
|
+
part_of: S,
|
|
1953
|
+
parts: strings,
|
|
1954
|
+
props: list(
|
|
1955
|
+
shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
|
|
1956
|
+
),
|
|
1957
|
+
slots: { type: "object" },
|
|
1958
|
+
composable_with: strings,
|
|
1959
|
+
never_inside: strings,
|
|
1960
|
+
allowed_children: strings,
|
|
1961
|
+
example: S,
|
|
1962
|
+
dense: B
|
|
1963
|
+
},
|
|
1964
|
+
["name", "props", "example", "dense"]
|
|
1965
|
+
),
|
|
1966
|
+
"component.list": shape({ components: list() }, ["components"]),
|
|
1967
|
+
"component.batch": shape({ components: list() }, ["components"]),
|
|
1968
|
+
instructions: shape(
|
|
1969
|
+
{
|
|
1970
|
+
rules: list(),
|
|
1971
|
+
components: list(),
|
|
1972
|
+
recipes: list()
|
|
1973
|
+
},
|
|
1974
|
+
["rules", "components", "recipes"]
|
|
1975
|
+
),
|
|
1976
|
+
init: shape({ files: list() }),
|
|
1977
|
+
doctor: shape({ operations: { type: "object" }, checks: list() }),
|
|
1978
|
+
add: shape({ files: list(), setup: { type: "object" } }),
|
|
1979
|
+
list: shape({ entries: list() })
|
|
1980
|
+
};
|
|
1981
|
+
function shareRepeats(schema) {
|
|
1982
|
+
const count = /* @__PURE__ */ new Map();
|
|
1983
|
+
const tally = (node) => {
|
|
1984
|
+
if (Array.isArray(node)) node.forEach(tally);
|
|
1985
|
+
else if (typeof node === "object" && node !== null) {
|
|
1986
|
+
const text2 = JSON.stringify(node);
|
|
1987
|
+
if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
|
|
1988
|
+
Object.values(node).forEach(tally);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
tally(schema);
|
|
1992
|
+
const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
|
|
1993
|
+
if (repeated.length === 0) return schema;
|
|
1994
|
+
const names2 = new Map(
|
|
1995
|
+
repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
|
|
1996
|
+
);
|
|
1997
|
+
const defs = {};
|
|
1998
|
+
const rewrite = (node) => {
|
|
1999
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
2000
|
+
if (typeof node !== "object" || node === null) return node;
|
|
2001
|
+
const name = names2.get(JSON.stringify(node));
|
|
2002
|
+
if (name !== void 0) {
|
|
2003
|
+
defs[name] = node;
|
|
2004
|
+
return { $ref: `#/$defs/${name}` };
|
|
2005
|
+
}
|
|
2006
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
|
|
2007
|
+
};
|
|
2008
|
+
const rewritten = rewrite(schema);
|
|
2009
|
+
return { ...rewritten, $defs: defs };
|
|
2010
|
+
}
|
|
2011
|
+
function outputSchemaFor(type) {
|
|
2012
|
+
return shareRepeats({
|
|
2013
|
+
type: "object",
|
|
2014
|
+
properties: {
|
|
2015
|
+
type: { const: type },
|
|
2016
|
+
// The four versions and the digest; the docs name them, and so does every answer.
|
|
2017
|
+
identity: { type: "object" },
|
|
2018
|
+
data: SUMMARIES[type]
|
|
2019
|
+
},
|
|
2020
|
+
required: ["identity"],
|
|
2021
|
+
anyOf: [{ required: ["data"] }, { required: ["error"] }]
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
var RESULT_SCHEMAS = Object.freeze(
|
|
2025
|
+
Object.fromEntries(
|
|
2026
|
+
[...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
// src/setup.ts
|
|
2031
|
+
function setupFor(dependencies, contract2) {
|
|
2032
|
+
const { setup } = contract2.guidance;
|
|
2033
|
+
const scansPackage = dependencies.includes(setup.package_source.when);
|
|
2034
|
+
return {
|
|
2035
|
+
tailwind: setup.tailwind,
|
|
2036
|
+
packages: setup.packages.map(({ name, range, dev, why }) => ({
|
|
2037
|
+
name,
|
|
2038
|
+
range: range ?? "latest",
|
|
2039
|
+
dev,
|
|
2040
|
+
why
|
|
2041
|
+
})),
|
|
2042
|
+
stylesheet: [
|
|
2043
|
+
...setup.stylesheet,
|
|
2044
|
+
...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
|
|
2045
|
+
],
|
|
2046
|
+
root_import: { line: setup.root_import, note: setup.root_import_note },
|
|
2047
|
+
not_needed: setup.not_needed
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function renderSetup(setup, copiedPackageImports) {
|
|
2051
|
+
const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
|
|
2052
|
+
const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
|
|
2053
|
+
const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
|
|
2054
|
+
return [
|
|
2055
|
+
`Setup, once per project: ${setup.tailwind}`,
|
|
2056
|
+
` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
|
|
2057
|
+
" In the stylesheet that imports tailwindcss, after the import:",
|
|
2058
|
+
...setup.stylesheet.map(({ line }) => ` ${line}`),
|
|
2059
|
+
...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
|
|
2060
|
+
" (source copied into the project is scanned by Tailwind 4 without an @source line)"
|
|
2061
|
+
],
|
|
2062
|
+
` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
|
|
2063
|
+
...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
|
|
2064
|
+
];
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1099
2067
|
// src/agent-commands.ts
|
|
1100
|
-
|
|
2068
|
+
function renderInstall(result, setup) {
|
|
2069
|
+
const lines = [
|
|
2070
|
+
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
|
|
2071
|
+
`Output: ${result.outputDirectory}`,
|
|
2072
|
+
...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
|
|
2073
|
+
];
|
|
2074
|
+
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
2075
|
+
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
2076
|
+
lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
|
|
2077
|
+
if (result.devDependencies.length > 0)
|
|
2078
|
+
lines.push(
|
|
2079
|
+
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
|
|
2080
|
+
);
|
|
2081
|
+
lines.push(
|
|
2082
|
+
"",
|
|
2083
|
+
...renderSetup(setup, setup.stylesheet.length > 1),
|
|
2084
|
+
"",
|
|
2085
|
+
"Install what is missing yourself. No package configuration or stylesheet was changed."
|
|
2086
|
+
);
|
|
2087
|
+
return `${lines.join("\n")}
|
|
2088
|
+
`;
|
|
2089
|
+
}
|
|
1101
2090
|
function runAgentCommand(command, context) {
|
|
1102
2091
|
const { contract: contract2, registry: registry2, cwd } = context;
|
|
1103
2092
|
switch (command.command) {
|
|
@@ -1109,14 +2098,15 @@ function runAgentCommand(command, context) {
|
|
|
1109
2098
|
({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
|
|
1110
2099
|
`
|
|
1111
2100
|
).join(""),
|
|
1112
|
-
json: envelope("component.list", { components }),
|
|
2101
|
+
json: envelope("component.list", { components }, contract2),
|
|
1113
2102
|
exitCode: 0
|
|
1114
2103
|
};
|
|
1115
2104
|
}
|
|
1116
2105
|
const details = command.names.map((name) => componentDetail(contract2, name));
|
|
2106
|
+
const data = details.map((detail) => componentData(detail, command.dense));
|
|
1117
2107
|
return {
|
|
1118
2108
|
text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
|
|
1119
|
-
json:
|
|
2109
|
+
json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
|
|
1120
2110
|
exitCode: 0
|
|
1121
2111
|
};
|
|
1122
2112
|
}
|
|
@@ -1129,7 +2119,11 @@ function runAgentCommand(command, context) {
|
|
|
1129
2119
|
${next}
|
|
1130
2120
|
`
|
|
1131
2121
|
).join(""),
|
|
1132
|
-
json: envelope(
|
|
2122
|
+
json: envelope(
|
|
2123
|
+
"search",
|
|
2124
|
+
{ query: command.query, limit: command.limit, matchCount: results.length, results },
|
|
2125
|
+
contract2
|
|
2126
|
+
),
|
|
1133
2127
|
exitCode: 0
|
|
1134
2128
|
};
|
|
1135
2129
|
}
|
|
@@ -1140,146 +2134,322 @@ function runAgentCommand(command, context) {
|
|
|
1140
2134
|
...result.files.map(({ path, action }) => ` ${action.padEnd(9)} ${path}`),
|
|
1141
2135
|
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."
|
|
1142
2136
|
];
|
|
1143
|
-
return {
|
|
1144
|
-
|
|
2137
|
+
return {
|
|
2138
|
+
text: `${lines.join("\n")}
|
|
2139
|
+
`,
|
|
2140
|
+
json: envelope("init", result, contract2),
|
|
2141
|
+
exitCode: 0
|
|
2142
|
+
};
|
|
1145
2143
|
}
|
|
1146
2144
|
case "check": {
|
|
1147
2145
|
const report = checkPaths(command.paths, { cwd, contract: contract2 });
|
|
1148
|
-
const { errors, warnings } = report.summary;
|
|
1149
|
-
const lines = report.findings.map(
|
|
1150
|
-
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1151
|
-
);
|
|
1152
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1153
|
-
lines.push(
|
|
1154
|
-
`${count(errors, "error")}, ${count(warnings, "warning")} in ${count(report.files, "file")}.`
|
|
1155
|
-
);
|
|
1156
2146
|
return {
|
|
1157
|
-
text:
|
|
1158
|
-
|
|
1159
|
-
json: envelope("check", report),
|
|
2147
|
+
text: renderCheckText(report),
|
|
2148
|
+
json: envelope("check", report, contract2),
|
|
1160
2149
|
// Warnings fail the run only when asked to: they are house rules, or may be native attributes.
|
|
1161
|
-
exitCode:
|
|
2150
|
+
exitCode: checkExitCode(report, { strict: command.strict })
|
|
1162
2151
|
};
|
|
1163
2152
|
}
|
|
1164
2153
|
case "doctor": {
|
|
1165
|
-
const report = doctor({
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
...
|
|
1169
|
-
|
|
2154
|
+
const report = doctor({
|
|
2155
|
+
cwd,
|
|
2156
|
+
contract: contract2,
|
|
2157
|
+
...context.parser ? { parser: context.parser } : {}
|
|
2158
|
+
});
|
|
2159
|
+
const { lookup, check_code: checking } = report.operations;
|
|
2160
|
+
const lines = [
|
|
2161
|
+
"Readiness",
|
|
2162
|
+
` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
|
|
2163
|
+
` code checking ${checking.status.padEnd(8)} ${checking.message}`,
|
|
2164
|
+
...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
|
|
2165
|
+
"",
|
|
2166
|
+
"Setup"
|
|
2167
|
+
];
|
|
2168
|
+
lines.push(
|
|
2169
|
+
...report.checks.flatMap(({ status, message, fix }) => [
|
|
2170
|
+
`[${status}] ${message}`,
|
|
2171
|
+
...fix === void 0 ? [] : [` fix: ${fix}`]
|
|
2172
|
+
])
|
|
2173
|
+
);
|
|
1170
2174
|
lines.push(
|
|
1171
2175
|
`${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
|
|
1172
2176
|
);
|
|
1173
2177
|
return {
|
|
1174
2178
|
text: `${lines.join("\n")}
|
|
1175
2179
|
`,
|
|
1176
|
-
json: envelope("doctor", report),
|
|
2180
|
+
json: envelope("doctor", report, contract2),
|
|
1177
2181
|
exitCode: report.summary.fail > 0 ? 1 : 0
|
|
1178
2182
|
};
|
|
1179
2183
|
}
|
|
2184
|
+
case "list": {
|
|
2185
|
+
const lines = [`OPERATOR ${VERSION}`, ""];
|
|
2186
|
+
for (const kind of ["component", "recipe"]) {
|
|
2187
|
+
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
2188
|
+
lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
|
|
2189
|
+
for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
|
|
2190
|
+
lines.push("");
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
text: `${lines.join("\n")}
|
|
2194
|
+
`,
|
|
2195
|
+
json: envelope(
|
|
2196
|
+
"list",
|
|
2197
|
+
{ version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
2198
|
+
contract2
|
|
2199
|
+
),
|
|
2200
|
+
exitCode: 0
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
case "add": {
|
|
2204
|
+
const result = installSources(command, { cwd, registry: registry2 });
|
|
2205
|
+
const setup = setupFor(result.dependencies, contract2);
|
|
2206
|
+
return {
|
|
2207
|
+
text: renderInstall(result, setup),
|
|
2208
|
+
json: envelope("add", { ...result, setup }, contract2),
|
|
2209
|
+
exitCode: 0
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
1180
2212
|
}
|
|
1181
2213
|
}
|
|
1182
2214
|
|
|
1183
2215
|
// src/mcp.ts
|
|
1184
2216
|
import { createInterface } from "readline";
|
|
2217
|
+
|
|
2218
|
+
// src/validate.ts
|
|
2219
|
+
var KNOWN = /* @__PURE__ */ new Set([
|
|
2220
|
+
"type",
|
|
2221
|
+
"description",
|
|
2222
|
+
"default",
|
|
2223
|
+
"enum",
|
|
2224
|
+
"minimum",
|
|
2225
|
+
"maximum",
|
|
2226
|
+
"minLength",
|
|
2227
|
+
"maxLength"
|
|
2228
|
+
]);
|
|
2229
|
+
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;
|
|
2230
|
+
function describeValue(value) {
|
|
2231
|
+
if (typeof value === "string")
|
|
2232
|
+
return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
|
|
2233
|
+
if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
|
|
2234
|
+
if (value === null || typeof value === "boolean" || value === void 0) return String(value);
|
|
2235
|
+
return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
|
|
2236
|
+
}
|
|
2237
|
+
function expectation(schema) {
|
|
2238
|
+
switch (schema.type) {
|
|
2239
|
+
case "boolean":
|
|
2240
|
+
return "true or false";
|
|
2241
|
+
case "integer":
|
|
2242
|
+
case "number": {
|
|
2243
|
+
const noun = schema.type === "integer" ? "a whole number" : "a number";
|
|
2244
|
+
if (schema.minimum !== void 0 && schema.maximum !== void 0)
|
|
2245
|
+
return `${noun} from ${schema.minimum} to ${schema.maximum}`;
|
|
2246
|
+
if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
|
|
2247
|
+
if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
|
|
2248
|
+
return noun;
|
|
2249
|
+
}
|
|
2250
|
+
case "string": {
|
|
2251
|
+
if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
|
|
2252
|
+
const { minLength, maxLength } = schema;
|
|
2253
|
+
if (minLength !== void 0 && maxLength !== void 0)
|
|
2254
|
+
return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
|
|
2255
|
+
if (maxLength !== void 0)
|
|
2256
|
+
return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
|
|
2257
|
+
return "a string";
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function accepts(schema, value) {
|
|
2262
|
+
switch (schema.type) {
|
|
2263
|
+
case "boolean":
|
|
2264
|
+
return typeof value === "boolean";
|
|
2265
|
+
case "string":
|
|
2266
|
+
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));
|
|
2267
|
+
case "integer":
|
|
2268
|
+
case "number":
|
|
2269
|
+
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);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function validateArguments(tool, schema, args) {
|
|
2273
|
+
for (const [field, property] of Object.entries(schema.properties))
|
|
2274
|
+
for (const key of Object.keys(property))
|
|
2275
|
+
if (!KNOWN.has(key))
|
|
2276
|
+
throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
|
|
2277
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
2278
|
+
return `${tool}: arguments must be an object.`;
|
|
2279
|
+
const given = args;
|
|
2280
|
+
const required = schema.required ?? [];
|
|
2281
|
+
const blank = (value) => typeof value === "string" && value.trim() === "";
|
|
2282
|
+
const missing = [];
|
|
2283
|
+
const wrong = [];
|
|
2284
|
+
const unknown = [];
|
|
2285
|
+
for (const field of required)
|
|
2286
|
+
if (given[field] === void 0 || blank(given[field])) missing.push(field);
|
|
2287
|
+
for (const [field, value] of Object.entries(given)) {
|
|
2288
|
+
const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
|
|
2289
|
+
if (property === void 0) unknown.push(field);
|
|
2290
|
+
else if (value !== void 0 && !(required.includes(field) && blank(value))) {
|
|
2291
|
+
if (!accepts(property, value))
|
|
2292
|
+
wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const reject = schema.additionalProperties === false ? unknown : [];
|
|
2296
|
+
const names2 = Object.keys(schema.properties);
|
|
2297
|
+
if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
|
|
2298
|
+
return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
|
|
2299
|
+
const fragments = [
|
|
2300
|
+
...missing.map(
|
|
2301
|
+
(field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
|
|
2302
|
+
),
|
|
2303
|
+
...wrong,
|
|
2304
|
+
...reject.map(
|
|
2305
|
+
(field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
|
|
2306
|
+
)
|
|
2307
|
+
];
|
|
2308
|
+
return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/mcp.ts
|
|
1185
2312
|
var VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1186
2313
|
var text = (value) => ({ type: "text", text: value });
|
|
1187
|
-
var
|
|
2314
|
+
var LIMITS = { query: 500, name: 100, code: 2e5 };
|
|
2315
|
+
var ANSWERS = {
|
|
2316
|
+
search: "search",
|
|
2317
|
+
get_component: "component.detail",
|
|
2318
|
+
list_components: "component.list",
|
|
2319
|
+
get_instructions: "instructions",
|
|
2320
|
+
check_code: "check"
|
|
2321
|
+
};
|
|
2322
|
+
var readOnly = { readOnlyHint: true };
|
|
2323
|
+
var define = (tool) => ({
|
|
2324
|
+
...tool,
|
|
2325
|
+
outputSchema: outputSchemaFor(ANSWERS[tool.name]),
|
|
2326
|
+
annotations: readOnly
|
|
2327
|
+
});
|
|
1188
2328
|
var TOOLS = [
|
|
1189
|
-
{
|
|
2329
|
+
define({
|
|
1190
2330
|
name: "search",
|
|
1191
|
-
description: "Find the OPERATOR component or recipe
|
|
2331
|
+
description: "Find the OPERATOR component or recipe for a need, such as 'show a status' or 'table with selection'. Call before writing interface code.",
|
|
1192
2332
|
inputSchema: {
|
|
1193
2333
|
type: "object",
|
|
1194
2334
|
properties: {
|
|
1195
|
-
query:
|
|
1196
|
-
|
|
2335
|
+
query: {
|
|
2336
|
+
type: "string",
|
|
2337
|
+
minLength: 1,
|
|
2338
|
+
maxLength: LIMITS.query,
|
|
2339
|
+
description: "The need, in plain words."
|
|
2340
|
+
},
|
|
2341
|
+
limit: {
|
|
2342
|
+
type: "integer",
|
|
2343
|
+
minimum: 1,
|
|
2344
|
+
maximum: SEARCH_LIMIT_MAX,
|
|
2345
|
+
default: SEARCH_LIMIT_DEFAULT,
|
|
2346
|
+
description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
|
|
2347
|
+
}
|
|
1197
2348
|
},
|
|
1198
|
-
required: ["query"]
|
|
2349
|
+
required: ["query"],
|
|
2350
|
+
additionalProperties: false
|
|
1199
2351
|
}
|
|
1200
|
-
},
|
|
1201
|
-
{
|
|
2352
|
+
}),
|
|
2353
|
+
define({
|
|
1202
2354
|
name: "get_component",
|
|
1203
|
-
description: "One
|
|
2355
|
+
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.",
|
|
1204
2356
|
inputSchema: {
|
|
1205
2357
|
type: "object",
|
|
1206
2358
|
properties: {
|
|
1207
|
-
name:
|
|
2359
|
+
name: {
|
|
2360
|
+
type: "string",
|
|
2361
|
+
minLength: 1,
|
|
2362
|
+
maxLength: LIMITS.name,
|
|
2363
|
+
description: "Component name, such as Button."
|
|
2364
|
+
},
|
|
1208
2365
|
dense: {
|
|
1209
2366
|
type: "boolean",
|
|
1210
|
-
|
|
2367
|
+
default: true,
|
|
2368
|
+
description: "Compact form (every constraint, one sentence per prop). False: full prose."
|
|
1211
2369
|
}
|
|
1212
2370
|
},
|
|
1213
|
-
required: ["name"]
|
|
2371
|
+
required: ["name"],
|
|
2372
|
+
additionalProperties: false
|
|
1214
2373
|
}
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
2374
|
+
}),
|
|
2375
|
+
define({
|
|
1217
2376
|
name: "list_components",
|
|
1218
2377
|
description: "Every OPERATOR component in one line each, with its parts.",
|
|
1219
|
-
inputSchema: { type: "object", properties: {} }
|
|
1220
|
-
},
|
|
1221
|
-
{
|
|
2378
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2379
|
+
}),
|
|
2380
|
+
define({
|
|
1222
2381
|
name: "get_instructions",
|
|
1223
|
-
description: "OPERATOR's house rules,
|
|
1224
|
-
inputSchema: { type: "object", properties: {} }
|
|
1225
|
-
},
|
|
1226
|
-
{
|
|
2382
|
+
description: "OPERATOR's house rules, component index and recipes: read once before building.",
|
|
2383
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2384
|
+
}),
|
|
2385
|
+
define({
|
|
1227
2386
|
name: "check_code",
|
|
1228
|
-
description: "Check TSX
|
|
2387
|
+
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.",
|
|
1229
2388
|
inputSchema: {
|
|
1230
2389
|
type: "object",
|
|
1231
|
-
properties: {
|
|
1232
|
-
|
|
2390
|
+
properties: {
|
|
2391
|
+
code: {
|
|
2392
|
+
type: "string",
|
|
2393
|
+
minLength: 1,
|
|
2394
|
+
maxLength: LIMITS.code,
|
|
2395
|
+
description: "The TSX source of one file."
|
|
2396
|
+
}
|
|
2397
|
+
},
|
|
2398
|
+
required: ["code"],
|
|
2399
|
+
additionalProperties: false
|
|
1233
2400
|
}
|
|
1234
|
-
}
|
|
2401
|
+
})
|
|
1235
2402
|
];
|
|
1236
2403
|
function runTool(name, args, context) {
|
|
1237
2404
|
const { contract: contract2, registry: registry2 } = context;
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return value;
|
|
1243
|
-
};
|
|
2405
|
+
const definition = TOOLS.find((tool) => tool.name === name);
|
|
2406
|
+
if (definition === void 0) throw new UnknownTool(name);
|
|
2407
|
+
const problem = validateArguments(name, definition.inputSchema, args);
|
|
2408
|
+
if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
|
|
1244
2409
|
switch (name) {
|
|
1245
2410
|
case "search": {
|
|
1246
|
-
const query =
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
query
|
|
1251
|
-
|
|
1252
|
-
);
|
|
1253
|
-
return results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
1254
|
-
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
2411
|
+
const query = args["query"];
|
|
2412
|
+
const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
|
|
2413
|
+
const results = search(contract2, registry2, query, limit);
|
|
2414
|
+
return {
|
|
2415
|
+
text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
2416
|
+
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
1255
2417
|
${next}`
|
|
1256
|
-
|
|
2418
|
+
).join("\n"),
|
|
2419
|
+
structured: envelope(
|
|
2420
|
+
"search",
|
|
2421
|
+
{ query, limit, matchCount: results.length, results },
|
|
2422
|
+
contract2
|
|
2423
|
+
)
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
case "get_component": {
|
|
2427
|
+
const dense = args["dense"] ?? true;
|
|
2428
|
+
const detail = componentDetail(contract2, args["name"]);
|
|
2429
|
+
return {
|
|
2430
|
+
text: renderComponent(detail, { dense }),
|
|
2431
|
+
structured: envelope("component.detail", componentData(detail, dense), contract2)
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
case "list_components": {
|
|
2435
|
+
const components = componentIndex(contract2);
|
|
2436
|
+
return {
|
|
2437
|
+
text: components.map(
|
|
2438
|
+
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
2439
|
+
).join("\n"),
|
|
2440
|
+
structured: envelope("component.list", { components }, contract2)
|
|
2441
|
+
};
|
|
1257
2442
|
}
|
|
1258
|
-
case "get_component":
|
|
1259
|
-
return renderComponent(componentDetail(contract2, required("name")), {
|
|
1260
|
-
dense: args.dense !== false
|
|
1261
|
-
});
|
|
1262
|
-
case "list_components":
|
|
1263
|
-
return componentIndex(contract2).map(
|
|
1264
|
-
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
1265
|
-
).join("\n");
|
|
1266
2443
|
case "get_instructions":
|
|
1267
|
-
return
|
|
2444
|
+
return {
|
|
2445
|
+
text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
|
|
2446
|
+
structured: envelope("instructions", instructionsData(contract2), contract2)
|
|
2447
|
+
};
|
|
1268
2448
|
case "check_code": {
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const findings = checkSource(ts, contract2, "snippet.tsx", code);
|
|
1274
|
-
if (findings.length === 0) return "No findings.";
|
|
1275
|
-
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1276
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1277
|
-
return [
|
|
1278
|
-
...findings.map(
|
|
1279
|
-
({ line, column, severity, code: finding, message }) => `${line}:${column} ${severity} ${finding} ${message}`
|
|
1280
|
-
),
|
|
1281
|
-
`${count(errors, "error")}, ${count(findings.length - errors, "warning")}.`
|
|
1282
|
-
].join("\n");
|
|
2449
|
+
const report = buildCheckReport([
|
|
2450
|
+
checkSource(contract2, "snippet.tsx", args["code"])
|
|
2451
|
+
]);
|
|
2452
|
+
return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
|
|
1283
2453
|
}
|
|
1284
2454
|
default:
|
|
1285
2455
|
throw new UnknownTool(name);
|
|
@@ -1293,24 +2463,28 @@ var UnknownTool = class extends Error {
|
|
|
1293
2463
|
}
|
|
1294
2464
|
};
|
|
1295
2465
|
function handleMessage(raw, context) {
|
|
1296
|
-
const
|
|
2466
|
+
const failure2 = (id2, code, message) => ({
|
|
1297
2467
|
jsonrpc: "2.0",
|
|
1298
2468
|
id: id2 ?? null,
|
|
1299
2469
|
error: { code, message }
|
|
1300
2470
|
});
|
|
1301
|
-
if (typeof raw !== "object" || raw === null) return
|
|
2471
|
+
if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
|
|
1302
2472
|
const { id, method, params } = raw;
|
|
1303
|
-
if (typeof method !== "string") return
|
|
2473
|
+
if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
|
|
1304
2474
|
if (id === void 0) return null;
|
|
1305
2475
|
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
1306
2476
|
switch (method) {
|
|
1307
2477
|
case "initialize": {
|
|
1308
2478
|
const asked = params?.protocolVersion;
|
|
2479
|
+
const identity = identityOf(context.contract);
|
|
1309
2480
|
return result({
|
|
1310
2481
|
protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
|
|
1311
2482
|
capabilities: { tools: {} },
|
|
1312
|
-
|
|
1313
|
-
|
|
2483
|
+
// The program's version, not the contract's: the contract has its own, in `_meta` and in
|
|
2484
|
+
// the identity of every result.
|
|
2485
|
+
serverInfo: { name: "oprt", version: VERSION },
|
|
2486
|
+
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.",
|
|
2487
|
+
_meta: { "dev.oprt/identity": identity }
|
|
1314
2488
|
});
|
|
1315
2489
|
}
|
|
1316
2490
|
case "ping":
|
|
@@ -1320,17 +2494,23 @@ function handleMessage(raw, context) {
|
|
|
1320
2494
|
case "tools/call": {
|
|
1321
2495
|
const { name, arguments: args } = params ?? {};
|
|
1322
2496
|
try {
|
|
1323
|
-
|
|
2497
|
+
const output = runTool(String(name), args ?? {}, context);
|
|
2498
|
+
return result({
|
|
2499
|
+
content: [text(output.text)],
|
|
2500
|
+
structuredContent: output.structured
|
|
2501
|
+
});
|
|
1324
2502
|
} catch (error) {
|
|
1325
|
-
if (error instanceof UnknownTool) return
|
|
2503
|
+
if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
|
|
2504
|
+
const failed = errorEnvelope(error, context.contract);
|
|
1326
2505
|
return result({
|
|
1327
|
-
content: [text(
|
|
2506
|
+
content: [text(failed.error)],
|
|
2507
|
+
structuredContent: failed,
|
|
1328
2508
|
isError: true
|
|
1329
2509
|
});
|
|
1330
2510
|
}
|
|
1331
2511
|
}
|
|
1332
2512
|
default:
|
|
1333
|
-
return
|
|
2513
|
+
return failure2(id, -32601, `Method not found: ${method}`);
|
|
1334
2514
|
}
|
|
1335
2515
|
}
|
|
1336
2516
|
function serve(input, output, context) {
|
|
@@ -1351,7 +2531,6 @@ function serve(input, output, context) {
|
|
|
1351
2531
|
}
|
|
1352
2532
|
|
|
1353
2533
|
// src/index.ts
|
|
1354
|
-
var VERSION = package_default.version;
|
|
1355
2534
|
function loadRegistry() {
|
|
1356
2535
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
1357
2536
|
const candidates = [join4(here, "registry.json"), join4(here, "../generated/registry.json")];
|
|
@@ -1364,8 +2543,8 @@ function loadRegistry() {
|
|
|
1364
2543
|
return JSON.parse(readFileSync6(found, "utf8"));
|
|
1365
2544
|
}
|
|
1366
2545
|
var registry = loadRegistry();
|
|
1367
|
-
var
|
|
1368
|
-
var contract = () =>
|
|
2546
|
+
var loaded2;
|
|
2547
|
+
var contract = () => loaded2 ??= loadContract();
|
|
1369
2548
|
function install(options, context = {}) {
|
|
1370
2549
|
return installSources(options, {
|
|
1371
2550
|
cwd: context.cwd ?? process.cwd(),
|
|
@@ -1375,26 +2554,38 @@ function install(options, context = {}) {
|
|
|
1375
2554
|
export {
|
|
1376
2555
|
CliError,
|
|
1377
2556
|
DEFAULT_OUT,
|
|
2557
|
+
RESULT_SCHEMAS,
|
|
2558
|
+
RESULT_SCHEMA_VERSION,
|
|
2559
|
+
RESULT_TYPES,
|
|
1378
2560
|
TOOLS,
|
|
1379
2561
|
VERSION,
|
|
1380
2562
|
applyBlock,
|
|
1381
2563
|
blockVersion,
|
|
2564
|
+
buildCheckReport,
|
|
2565
|
+
checkExitCode,
|
|
1382
2566
|
checkPaths,
|
|
1383
2567
|
checkSource,
|
|
1384
2568
|
componentDetail,
|
|
1385
2569
|
componentIndex,
|
|
1386
2570
|
contract,
|
|
2571
|
+
contractDigest,
|
|
1387
2572
|
doctor,
|
|
2573
|
+
envelope,
|
|
2574
|
+
errorEnvelope,
|
|
1388
2575
|
handleMessage,
|
|
2576
|
+
identityOf,
|
|
1389
2577
|
init,
|
|
1390
2578
|
install,
|
|
2579
|
+
outputSchemaFor,
|
|
1391
2580
|
parseArgs,
|
|
1392
2581
|
registry,
|
|
1393
2582
|
removeBlock,
|
|
1394
2583
|
renderAgentBlock,
|
|
2584
|
+
renderCheckText,
|
|
1395
2585
|
renderComponent,
|
|
1396
2586
|
resolveEntries,
|
|
1397
2587
|
runAgentCommand,
|
|
2588
|
+
schemaFor,
|
|
1398
2589
|
search,
|
|
1399
2590
|
serve
|
|
1400
2591
|
};
|