@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/bin.js
CHANGED
|
@@ -39,6 +39,205 @@ var init_types = __esm({
|
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// src/lookup.ts
|
|
43
|
+
function distance(a, b) {
|
|
44
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
45
|
+
for (let i = 1; i <= a.length; i++) {
|
|
46
|
+
let diagonal = row[0];
|
|
47
|
+
row[0] = i;
|
|
48
|
+
for (let j = 1; j <= b.length; j++) {
|
|
49
|
+
const above = row[j];
|
|
50
|
+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
51
|
+
diagonal = above;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return row[b.length];
|
|
55
|
+
}
|
|
56
|
+
function nearest(wanted, names2, limit = 5) {
|
|
57
|
+
const target = squash(wanted);
|
|
58
|
+
return names2.map((name) => {
|
|
59
|
+
const candidate = squash(name);
|
|
60
|
+
const apart = distance(target, candidate);
|
|
61
|
+
const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
|
|
62
|
+
return { name, apart, close };
|
|
63
|
+
}).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
|
|
64
|
+
}
|
|
65
|
+
function resolveComponent(contract2, wanted) {
|
|
66
|
+
const names2 = Object.keys(contract2.components);
|
|
67
|
+
const found = names2.find((name) => name === wanted) ?? names2.find((name) => squash(name) === squash(wanted));
|
|
68
|
+
if (found !== void 0) return found;
|
|
69
|
+
const suggestions = nearest(wanted, names2);
|
|
70
|
+
throw new CliError(
|
|
71
|
+
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
72
|
+
2,
|
|
73
|
+
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function allowedChildren(rules, name) {
|
|
77
|
+
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
78
|
+
if (parent === name) return rule.allowed_children;
|
|
79
|
+
const nested = allowedChildren(rule.children, name);
|
|
80
|
+
if (nested !== void 0) return nested;
|
|
81
|
+
}
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
function componentIndex(contract2) {
|
|
85
|
+
return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
|
|
86
|
+
name,
|
|
87
|
+
purpose: contract2.guidance.components[name].use_for.join("; "),
|
|
88
|
+
parts: partsOf(contract2, name)
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
function componentDetail(contract2, wanted) {
|
|
92
|
+
const name = resolveComponent(contract2, wanted);
|
|
93
|
+
const component = contract2.components[name];
|
|
94
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
95
|
+
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
96
|
+
const notes = guide.values?.[prop];
|
|
97
|
+
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
98
|
+
});
|
|
99
|
+
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
description: component.description,
|
|
103
|
+
use_for: guide.use_for ?? [],
|
|
104
|
+
not_for: guide.not_for ?? [],
|
|
105
|
+
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
106
|
+
parts: partsOf(contract2, name),
|
|
107
|
+
props,
|
|
108
|
+
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
109
|
+
composable_with: component.composable_with ?? [],
|
|
110
|
+
never_inside: component.never_inside ?? [],
|
|
111
|
+
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
112
|
+
example: component.example
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function propLine(prop, dense) {
|
|
116
|
+
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)})`);
|
|
117
|
+
const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
|
|
118
|
+
const lines = [
|
|
119
|
+
` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
|
|
120
|
+
];
|
|
121
|
+
for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
function renderComponent(detail, { dense }) {
|
|
125
|
+
const lines = [
|
|
126
|
+
detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
|
|
127
|
+
];
|
|
128
|
+
if (detail.not_for.length > 0)
|
|
129
|
+
lines.push(
|
|
130
|
+
`Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
|
|
131
|
+
);
|
|
132
|
+
if (!dense) lines.push("", detail.description, "");
|
|
133
|
+
if (detail.props.length > 0) {
|
|
134
|
+
lines.push("Props:");
|
|
135
|
+
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
136
|
+
}
|
|
137
|
+
for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
|
|
138
|
+
lines.push(
|
|
139
|
+
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
140
|
+
);
|
|
141
|
+
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
142
|
+
if (detail.allowed_children !== void 0)
|
|
143
|
+
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
144
|
+
if (detail.composable_with.length > 0)
|
|
145
|
+
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
146
|
+
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
147
|
+
lines.push(
|
|
148
|
+
detail.example.includes("\n") ? `Example:
|
|
149
|
+
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
150
|
+
);
|
|
151
|
+
return `${lines.join("\n")}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
function search(contract2, registry2, query, limit = SEARCH_LIMIT_DEFAULT) {
|
|
155
|
+
const wanted = words(query);
|
|
156
|
+
if (wanted.length === 0) return [];
|
|
157
|
+
const whole = squash(query);
|
|
158
|
+
const scores = /* @__PURE__ */ new Map();
|
|
159
|
+
const add = (name, points, why) => {
|
|
160
|
+
const before = scores.get(name) ?? { score: 0 };
|
|
161
|
+
scores.set(name, {
|
|
162
|
+
score: before.score + points,
|
|
163
|
+
...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
for (const [name, component] of Object.entries(contract2.components)) {
|
|
167
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
168
|
+
const plain = squash(name);
|
|
169
|
+
if (plain === whole) add(name, 100);
|
|
170
|
+
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
171
|
+
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
172
|
+
for (const phrase of guide.use_for ?? []) {
|
|
173
|
+
const hits = matched(wanted, phrase);
|
|
174
|
+
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
175
|
+
}
|
|
176
|
+
for (const { need, use } of guide.not_for ?? []) {
|
|
177
|
+
const hits = matched(wanted, need);
|
|
178
|
+
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
179
|
+
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
180
|
+
}
|
|
181
|
+
const inDescription = matched(wanted, component.description);
|
|
182
|
+
if (inDescription > 0) add(name, 2 * inDescription);
|
|
183
|
+
}
|
|
184
|
+
const results = [];
|
|
185
|
+
for (const [name, { score, why }] of scores) {
|
|
186
|
+
const guide = contract2.guidance.components[name] ?? {};
|
|
187
|
+
results.push({
|
|
188
|
+
kind: "component",
|
|
189
|
+
name,
|
|
190
|
+
purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
|
|
191
|
+
score,
|
|
192
|
+
...why === void 0 ? {} : { why },
|
|
193
|
+
next: `oprt component ${name}`
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
|
|
197
|
+
const recipe = contract2.patterns?.[entry.name];
|
|
198
|
+
const plain = squash(entry.name);
|
|
199
|
+
const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
|
|
200
|
+
const hits = matched(wanted, entry.description);
|
|
201
|
+
const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
|
|
202
|
+
if (score > 0)
|
|
203
|
+
results.push({
|
|
204
|
+
kind: "recipe",
|
|
205
|
+
name: entry.name,
|
|
206
|
+
purpose: firstSentence(entry.description),
|
|
207
|
+
score,
|
|
208
|
+
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
212
|
+
const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
|
|
213
|
+
return results.filter(({ score }) => score >= floor).slice(0, limit);
|
|
214
|
+
}
|
|
215
|
+
var SEARCH_LIMIT_MAX, SEARCH_LIMIT_DEFAULT, squash, firstSentence, partsOf, STOP, words, matched;
|
|
216
|
+
var init_lookup = __esm({
|
|
217
|
+
"src/lookup.ts"() {
|
|
218
|
+
"use strict";
|
|
219
|
+
init_esm_shims();
|
|
220
|
+
init_types();
|
|
221
|
+
SEARCH_LIMIT_MAX = 50;
|
|
222
|
+
SEARCH_LIMIT_DEFAULT = 10;
|
|
223
|
+
squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
224
|
+
firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
|
|
225
|
+
partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
226
|
+
(part) => contract2.guidance.components[part]?.part_of === name
|
|
227
|
+
);
|
|
228
|
+
STOP = new Set(
|
|
229
|
+
"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(
|
|
230
|
+
" "
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
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);
|
|
234
|
+
matched = (query, text2) => {
|
|
235
|
+
const present = new Set(words(text2));
|
|
236
|
+
return query.filter((word) => present.has(word)).length;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
42
241
|
// src/args.ts
|
|
43
242
|
function split(command, rest, spec) {
|
|
44
243
|
const flags = /* @__PURE__ */ new Set();
|
|
@@ -90,7 +289,8 @@ function parseArgs(args) {
|
|
|
90
289
|
names: [],
|
|
91
290
|
out: DEFAULT_OUT,
|
|
92
291
|
dryRun: false,
|
|
93
|
-
overwrite: false
|
|
292
|
+
overwrite: false,
|
|
293
|
+
json: false
|
|
94
294
|
};
|
|
95
295
|
const seen = /* @__PURE__ */ new Set();
|
|
96
296
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -99,7 +299,7 @@ function parseArgs(args) {
|
|
|
99
299
|
parsed.names.push(token);
|
|
100
300
|
continue;
|
|
101
301
|
}
|
|
102
|
-
if (!["--out", "--dry-run", "--overwrite"].includes(token))
|
|
302
|
+
if (!["--out", "--dry-run", "--overwrite", "--json"].includes(token))
|
|
103
303
|
throw new CliError(`Unknown option ${JSON.stringify(token)}. Run oprt --help.`, 2);
|
|
104
304
|
if (seen.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
|
|
105
305
|
seen.add(token);
|
|
@@ -109,6 +309,7 @@ function parseArgs(args) {
|
|
|
109
309
|
throw new CliError("--out requires a relative directory.", 2);
|
|
110
310
|
parsed.out = value;
|
|
111
311
|
} else if (token === "--dry-run") parsed.dryRun = true;
|
|
312
|
+
else if (token === "--json") parsed.json = true;
|
|
112
313
|
else parsed.overwrite = true;
|
|
113
314
|
}
|
|
114
315
|
if (!parsed.names.length)
|
|
@@ -120,6 +321,7 @@ var init_args = __esm({
|
|
|
120
321
|
"src/args.ts"() {
|
|
121
322
|
"use strict";
|
|
122
323
|
init_esm_shims();
|
|
324
|
+
init_lookup();
|
|
123
325
|
init_types();
|
|
124
326
|
DEFAULT_OUT = "src/components/operator";
|
|
125
327
|
AGENT_COMMANDS = {
|
|
@@ -145,9 +347,10 @@ var init_args = __esm({
|
|
|
145
347
|
"--json": "flag"
|
|
146
348
|
});
|
|
147
349
|
if (positional.length === 0) throw new CliError("Usage: oprt search <need> [--limit <n>]", 2);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
350
|
+
const given = value("--limit");
|
|
351
|
+
const limit = given === void 0 ? SEARCH_LIMIT_DEFAULT : Number(given);
|
|
352
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > SEARCH_LIMIT_MAX)
|
|
353
|
+
throw new CliError(`--limit takes a whole number from 1 to ${SEARCH_LIMIT_MAX}.`, 2);
|
|
151
354
|
return { command: "search", query: positional.join(" "), limit, json: has("--json") };
|
|
152
355
|
},
|
|
153
356
|
init(rest) {
|
|
@@ -193,456 +396,308 @@ var init_args = __esm({
|
|
|
193
396
|
}
|
|
194
397
|
});
|
|
195
398
|
|
|
196
|
-
// package.json
|
|
197
|
-
var
|
|
399
|
+
// ../../node_modules/.pnpm/@babel+parser@8.0.8/node_modules/@babel/parser/package.json
|
|
400
|
+
var version;
|
|
198
401
|
var init_package = __esm({
|
|
199
|
-
"package.json"() {
|
|
200
|
-
|
|
201
|
-
name: "@oprt/cli",
|
|
202
|
-
version: "0.1.0",
|
|
203
|
-
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.",
|
|
204
|
-
keywords: [
|
|
205
|
-
"design-system",
|
|
206
|
-
"cli",
|
|
207
|
-
"coding-agents",
|
|
208
|
-
"mcp",
|
|
209
|
-
"model-context-protocol",
|
|
210
|
-
"code-check",
|
|
211
|
-
"components"
|
|
212
|
-
],
|
|
213
|
-
homepage: "https://oprt.dev/docs/agents/cli",
|
|
214
|
-
type: "module",
|
|
215
|
-
main: "./dist/index.cjs",
|
|
216
|
-
module: "./dist/index.js",
|
|
217
|
-
types: "./dist/index.d.ts",
|
|
218
|
-
bin: {
|
|
219
|
-
oprt: "./dist/bin.js"
|
|
220
|
-
},
|
|
221
|
-
engines: {
|
|
222
|
-
node: ">=22"
|
|
223
|
-
},
|
|
224
|
-
exports: {
|
|
225
|
-
".": {
|
|
226
|
-
import: {
|
|
227
|
-
types: "./dist/index.d.ts",
|
|
228
|
-
default: "./dist/index.js"
|
|
229
|
-
},
|
|
230
|
-
require: {
|
|
231
|
-
types: "./dist/index.d.cts",
|
|
232
|
-
default: "./dist/index.cjs"
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
"./read": {
|
|
236
|
-
import: {
|
|
237
|
-
types: "./dist/read.d.ts",
|
|
238
|
-
default: "./dist/read.js"
|
|
239
|
-
},
|
|
240
|
-
require: {
|
|
241
|
-
types: "./dist/read.d.cts",
|
|
242
|
-
default: "./dist/read.cjs"
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
files: [
|
|
247
|
-
"dist"
|
|
248
|
-
],
|
|
249
|
-
scripts: {
|
|
250
|
-
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
251
|
-
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
252
|
-
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
253
|
-
test: "vitest run",
|
|
254
|
-
typecheck: "tsc --noEmit",
|
|
255
|
-
lint: "eslint src scripts"
|
|
256
|
-
},
|
|
257
|
-
devDependencies: {
|
|
258
|
-
tsup: "^8.3.0",
|
|
259
|
-
typescript: "^5.6.3",
|
|
260
|
-
vitest: "^2.1.3"
|
|
261
|
-
},
|
|
262
|
-
license: "MIT",
|
|
263
|
-
publishConfig: {
|
|
264
|
-
access: "public"
|
|
265
|
-
}
|
|
266
|
-
};
|
|
402
|
+
"../../node_modules/.pnpm/@babel+parser@8.0.8/node_modules/@babel/parser/package.json"() {
|
|
403
|
+
version = "8.0.8";
|
|
267
404
|
}
|
|
268
405
|
});
|
|
269
406
|
|
|
270
|
-
// src/
|
|
271
|
-
import {
|
|
272
|
-
import { dirname, join } from "path";
|
|
407
|
+
// src/parser.ts
|
|
408
|
+
import { createRequire } from "module";
|
|
273
409
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
410
|
+
function optionsFor(file) {
|
|
411
|
+
const typed = /\.tsx$/.test(file);
|
|
412
|
+
const plugins = [
|
|
413
|
+
"jsx",
|
|
414
|
+
...typed ? ["typescript"] : [],
|
|
415
|
+
"decorators",
|
|
416
|
+
"decoratorAutoAccessors",
|
|
417
|
+
"deferredImportEvaluation",
|
|
418
|
+
"sourcePhaseImports"
|
|
419
|
+
];
|
|
420
|
+
return {
|
|
421
|
+
sourceType: "module",
|
|
422
|
+
plugins,
|
|
423
|
+
// Recoverable errors are returned beside the tree, instead of the first one being thrown.
|
|
424
|
+
errorRecovery: true,
|
|
425
|
+
attachComment: false,
|
|
426
|
+
allowReturnOutsideFunction: true,
|
|
427
|
+
allowUndeclaredExports: true,
|
|
428
|
+
allowSuperOutsideMethod: true,
|
|
429
|
+
allowNewTargetOutsideFunction: true
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function loadBabel() {
|
|
433
|
+
if (loaded !== void 0) return loaded;
|
|
434
|
+
const fromSource = /\.[cm]?tsx?$/.test(fileURLToPath2(import.meta.url));
|
|
435
|
+
try {
|
|
436
|
+
const require2 = createRequire(import.meta.url);
|
|
437
|
+
loaded = fromSource ? require2("@babel/parser") : require2("./babel-parser.cjs");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throw new CliError(
|
|
440
|
+
`Code checking cannot load its parser (${FILE}): ${error instanceof Error ? error.message : String(error)} ${REINSTALL}`,
|
|
441
|
+
1,
|
|
442
|
+
{ code: "ERR_PARSER_MISSING" }
|
|
281
443
|
);
|
|
282
444
|
}
|
|
283
|
-
return
|
|
445
|
+
return loaded;
|
|
284
446
|
}
|
|
285
|
-
|
|
286
|
-
|
|
447
|
+
function probeParser(load = loadBabel) {
|
|
448
|
+
let babel;
|
|
449
|
+
try {
|
|
450
|
+
babel = load();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return {
|
|
453
|
+
ready: false,
|
|
454
|
+
reason: "parser_missing",
|
|
455
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (rejects(babel, "export const a = <a b={1 as number} />;"))
|
|
459
|
+
return {
|
|
460
|
+
ready: false,
|
|
461
|
+
reason: "parser_unusable",
|
|
462
|
+
detail: "it rejected valid code in a probe parse"
|
|
463
|
+
};
|
|
464
|
+
if (!rejects(babel, "export const a = <a>;"))
|
|
465
|
+
return {
|
|
466
|
+
ready: false,
|
|
467
|
+
reason: "parser_unusable",
|
|
468
|
+
detail: "it did not report a syntax error in code that has one"
|
|
469
|
+
};
|
|
470
|
+
return { ready: true, parser: PARSER };
|
|
471
|
+
}
|
|
472
|
+
var PARSER, FILE, REINSTALL, loaded, rejects;
|
|
473
|
+
var init_parser = __esm({
|
|
474
|
+
"src/parser.ts"() {
|
|
287
475
|
"use strict";
|
|
288
476
|
init_esm_shims();
|
|
477
|
+
init_package();
|
|
478
|
+
init_types();
|
|
479
|
+
PARSER = {
|
|
480
|
+
name: "@babel/parser",
|
|
481
|
+
version,
|
|
482
|
+
source: "bundled"
|
|
483
|
+
};
|
|
484
|
+
FILE = "babel-parser.cjs";
|
|
485
|
+
REINSTALL = "Install @oprt/cli again: the parser ships beside the executable in dist.";
|
|
486
|
+
rejects = (babel, code) => {
|
|
487
|
+
try {
|
|
488
|
+
return babel.parse(code, optionsFor("probe.tsx")).errors.length > 0;
|
|
489
|
+
} catch {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
289
493
|
}
|
|
290
494
|
});
|
|
291
495
|
|
|
292
|
-
// src/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
writeFileSync
|
|
302
|
-
} from "fs";
|
|
303
|
-
import { dirname as dirname2, isAbsolute, relative, resolve, sep } from "path";
|
|
304
|
-
function resolveEntries(names, registry2) {
|
|
305
|
-
if (!names.length) throw new CliError("add requires one or more names.", 2);
|
|
306
|
-
const selected = /* @__PURE__ */ new Map();
|
|
307
|
-
for (const name of names) {
|
|
308
|
-
const normalized = name.toLowerCase();
|
|
309
|
-
const entry = registry2.entries.find(
|
|
310
|
-
(candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
|
|
311
|
-
);
|
|
312
|
-
if (!entry)
|
|
313
|
-
throw new CliError(`Unknown component or recipe ${JSON.stringify(name)}. Run oprt list.`, 2);
|
|
314
|
-
selected.set(entry.name, entry);
|
|
496
|
+
// src/syntax.ts
|
|
497
|
+
function* childNodes(node) {
|
|
498
|
+
for (const [key, value] of Object.entries(node)) {
|
|
499
|
+
if (NOT_NODES.has(key) || typeof value !== "object" || value === null) continue;
|
|
500
|
+
if (Array.isArray(value)) {
|
|
501
|
+
for (const item of value)
|
|
502
|
+
if (typeof item === "object" && item !== null && typeof item.type === "string")
|
|
503
|
+
yield item;
|
|
504
|
+
} else if (typeof value.type === "string") yield value;
|
|
315
505
|
}
|
|
316
|
-
return [...selected.values()];
|
|
317
|
-
}
|
|
318
|
-
function validateRelative(path2) {
|
|
319
|
-
if (!path2 || isAbsolute(path2) || /[\\:\0]/.test(path2) || path2.split("/").includes(".."))
|
|
320
|
-
throw new CliError(
|
|
321
|
-
`Unsafe path ${JSON.stringify(path2)}: use a nonempty relative path without traversal.`,
|
|
322
|
-
2
|
|
323
|
-
);
|
|
324
506
|
}
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
507
|
+
function problemsOf(tree, errors, limit, consumed) {
|
|
508
|
+
const mismatched = /* @__PURE__ */ new Map();
|
|
509
|
+
walk(tree.program, [], (node, ancestors) => {
|
|
510
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) < limit && tagName(node.openingElement.name) !== tagName(node.closingElement.name))
|
|
511
|
+
mismatched.set(node.closingElement.start ?? 0, { element: node, ancestors: [...ancestors] });
|
|
512
|
+
});
|
|
513
|
+
const problems = [];
|
|
514
|
+
for (const error of errors) {
|
|
515
|
+
if (error.pos >= limit) continue;
|
|
516
|
+
const entry = error.reasonCode === "MissingClosingTagElement" && mismatched.get(error.pos);
|
|
517
|
+
if (entry) {
|
|
518
|
+
const closing = tagName(entry.element.closingElement?.name);
|
|
519
|
+
const outer = [...entry.ancestors].reverse().find(
|
|
520
|
+
(ancestor) => ancestor.type === "JSXElement" && tagName(ancestor.openingElement.name) === closing && !consumed.has(ancestor)
|
|
521
|
+
);
|
|
522
|
+
if (outer !== void 0) {
|
|
523
|
+
consumed.add(outer);
|
|
524
|
+
problems.push(unclosed(entry.element));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
problems.push(problemOf(error));
|
|
331
529
|
}
|
|
530
|
+
return problems;
|
|
332
531
|
}
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
throw new CliError(`Output target is not a regular file: ${cursor}`);
|
|
532
|
+
function complete(babel, prefix, file) {
|
|
533
|
+
let text2 = prefix;
|
|
534
|
+
let first = null;
|
|
535
|
+
for (let step = 0; step <= MAX_COMPLETIONS; step++) {
|
|
536
|
+
const tried = attempt(babel, text2, file);
|
|
537
|
+
if (tried.tree !== null) return { tree: tried.tree, errors: tried.errors, first };
|
|
538
|
+
const wantsMore = tried.fatal.reasonCode === "UnterminatedJsxContent" || tried.fatal.pos >= text2.length;
|
|
539
|
+
if (!wantsMore) return null;
|
|
540
|
+
const next = COMPLETIONS.find((piece) => {
|
|
541
|
+
const extended = attempt(babel, text2 + piece, file);
|
|
542
|
+
return extended.tree !== null || extended.fatal.reasonCode === "UnterminatedJsxContent" || extended.fatal.pos >= text2.length + piece.length;
|
|
543
|
+
});
|
|
544
|
+
if (next === void 0) return null;
|
|
545
|
+
first ??= next;
|
|
546
|
+
text2 += next;
|
|
349
547
|
}
|
|
350
|
-
return
|
|
548
|
+
return null;
|
|
351
549
|
}
|
|
352
|
-
function
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const outputDirectory = resolve(root, out);
|
|
358
|
-
inspectPath(root, outputDirectory, true);
|
|
359
|
-
const paths = [...new Set(entries.flatMap((entry) => entry.files))].sort();
|
|
360
|
-
const files = paths.map((path2) => {
|
|
361
|
-
validateRelative(path2);
|
|
362
|
-
const content = context.registry.files[path2];
|
|
363
|
-
if (!Object.hasOwn(context.registry.files, path2) || typeof content !== "string")
|
|
364
|
-
throw new CliError(`Registry is missing source ${path2}.`);
|
|
365
|
-
const destination = resolve(outputDirectory, path2);
|
|
366
|
-
const info = inspectPath(root, destination);
|
|
367
|
-
if (!info) return { path: path2, destination, action: "create" };
|
|
368
|
-
if (readFileSync2(destination, "utf8") === content) return { path: path2, destination, action: "skip" };
|
|
369
|
-
if (!options.overwrite)
|
|
370
|
-
throw new CliError(
|
|
371
|
-
`Refusing to overwrite modified file ${destination}. Use --overwrite to replace it explicitly.`
|
|
372
|
-
);
|
|
373
|
-
return { path: path2, destination, action: "overwrite" };
|
|
550
|
+
function shutByCompletion(tree, limit) {
|
|
551
|
+
const shut = [];
|
|
552
|
+
walk(tree.program, [], (node) => {
|
|
553
|
+
if (node.type === "JSXElement" && node.closingElement && (node.closingElement.start ?? 0) >= limit)
|
|
554
|
+
shut.push(node);
|
|
374
555
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const range = context.registry.ranges?.[name];
|
|
391
|
-
return range === void 0 ? [] : [[name, range]];
|
|
392
|
-
})
|
|
393
|
-
),
|
|
394
|
-
files,
|
|
395
|
-
dryRun: options.dryRun ?? false
|
|
396
|
-
};
|
|
397
|
-
if (result.dryRun) return result;
|
|
398
|
-
for (const file of files) {
|
|
399
|
-
if (file.action === "skip") continue;
|
|
400
|
-
inspectPath(root, file.destination);
|
|
401
|
-
const parent = dirname2(file.destination);
|
|
402
|
-
mkdirSync(parent, { recursive: true });
|
|
403
|
-
inspectPath(root, parent, true);
|
|
404
|
-
const flags = constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (file.action === "create" ? constants.O_EXCL : constants.O_TRUNC);
|
|
405
|
-
const descriptor = openSync(file.destination, flags, 420);
|
|
406
|
-
try {
|
|
407
|
-
writeFileSync(descriptor, context.registry.files[file.path]);
|
|
408
|
-
} finally {
|
|
409
|
-
closeSync(descriptor);
|
|
556
|
+
return shut.sort((a, b) => (a.closingElement?.start ?? 0) - (b.closingElement?.start ?? 0));
|
|
557
|
+
}
|
|
558
|
+
function readSyntax(file, text2, babel = loadBabel()) {
|
|
559
|
+
const first = attempt(babel, text2, file);
|
|
560
|
+
if (first.tree !== null) {
|
|
561
|
+
const problems = problemsOf(first.tree, first.errors, Infinity, /* @__PURE__ */ new Set());
|
|
562
|
+
return problems.length === 0 ? { tree: first.tree, problems: [] } : { tree: null, problems: problems.sort(byPlace) };
|
|
563
|
+
}
|
|
564
|
+
let fatal = first.fatal;
|
|
565
|
+
if (text2.length > MAX_EXPLAINED) return { tree: null, problems: [problemOf(fatal)] };
|
|
566
|
+
if (/\.tsx$/.test(file) && fatal.reasonCode !== "UnterminatedJsxContent") {
|
|
567
|
+
const plain = text2.replace(TYPE_ARGUMENTS, blankedOut);
|
|
568
|
+
if (plain !== text2) {
|
|
569
|
+
const again = attempt(babel, plain, file);
|
|
570
|
+
if (again.tree === null && again.fatal.pos > fatal.pos) fatal = again.fatal;
|
|
410
571
|
}
|
|
411
572
|
}
|
|
412
|
-
|
|
573
|
+
try {
|
|
574
|
+
const finished = complete(babel, text2.slice(0, fatal.pos), file);
|
|
575
|
+
if (finished !== null) {
|
|
576
|
+
const limit = fatal.pos;
|
|
577
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
578
|
+
const problems = problemsOf(finished.tree, finished.errors, limit, consumed);
|
|
579
|
+
const open = shutByCompletion(finished.tree, limit).filter(
|
|
580
|
+
(element) => !consumed.has(element)
|
|
581
|
+
);
|
|
582
|
+
const atEnd = fatal.reasonCode === "UnterminatedJsxContent" || fatal.pos >= text2.length;
|
|
583
|
+
const hints = (atEnd ? open : finished.first === CLOSING_TAG ? open.slice(0, 1) : []).map(
|
|
584
|
+
unclosed
|
|
585
|
+
);
|
|
586
|
+
const explained = [...problems, ...hints].some(({ code }) => code === "UnclosedJsxElement");
|
|
587
|
+
problems.push(...hints);
|
|
588
|
+
if (!(explained && fatal.reasonCode === "UnterminatedJsxContent"))
|
|
589
|
+
problems.push(problemOf(fatal));
|
|
590
|
+
return { tree: null, problems: problems.sort(byPlace) };
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
return { tree: null, problems: [problemOf(fatal)] };
|
|
413
595
|
}
|
|
414
|
-
var
|
|
415
|
-
|
|
596
|
+
var attempt, sentence, problemOf, tagName, NOT_NODES, walk, unclosed, TYPE_ARGUMENTS, LINE_BREAKS, blankedOut, CLOSING_TAG, COMPLETIONS, MAX_COMPLETIONS, byPlace, MAX_EXPLAINED;
|
|
597
|
+
var init_syntax = __esm({
|
|
598
|
+
"src/syntax.ts"() {
|
|
416
599
|
"use strict";
|
|
417
600
|
init_esm_shims();
|
|
418
|
-
|
|
419
|
-
|
|
601
|
+
init_parser();
|
|
602
|
+
attempt = (babel, text2, file) => {
|
|
603
|
+
try {
|
|
604
|
+
const tree = babel.parse(text2, optionsFor(file));
|
|
605
|
+
return { tree, errors: tree.errors };
|
|
606
|
+
} catch (error) {
|
|
607
|
+
return { tree: null, fatal: error };
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
sentence = (message) => message.replace(/\s*\(\d+:\d+\)$/, "");
|
|
611
|
+
problemOf = (failure2) => ({
|
|
612
|
+
line: failure2.loc.line,
|
|
613
|
+
column: failure2.loc.column + 1,
|
|
614
|
+
message: sentence(failure2.message),
|
|
615
|
+
code: failure2.reasonCode ?? "SyntaxError",
|
|
616
|
+
position: failure2.pos
|
|
617
|
+
});
|
|
618
|
+
tagName = (name) => {
|
|
619
|
+
if (name === void 0) return "";
|
|
620
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
621
|
+
if (name.type === "JSXMemberExpression")
|
|
622
|
+
return `${tagName(name.object)}.${name.property.name}`;
|
|
623
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
624
|
+
};
|
|
625
|
+
NOT_NODES = /* @__PURE__ */ new Set(["loc", "extra", "errors", "tokens", "comments"]);
|
|
626
|
+
walk = (node, ancestors, visit) => {
|
|
627
|
+
visit(node, ancestors);
|
|
628
|
+
ancestors.push(node);
|
|
629
|
+
for (const child of childNodes(node)) walk(child, ancestors, visit);
|
|
630
|
+
ancestors.pop();
|
|
631
|
+
};
|
|
632
|
+
unclosed = (element) => {
|
|
633
|
+
const name = element.openingElement.name;
|
|
634
|
+
return {
|
|
635
|
+
line: name.loc?.start.line ?? 1,
|
|
636
|
+
column: (name.loc?.start.column ?? 0) + 1,
|
|
637
|
+
message: `JSX element '${tagName(name)}' has no corresponding closing tag.`,
|
|
638
|
+
code: "UnclosedJsxElement",
|
|
639
|
+
position: name.start ?? 0
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
TYPE_ARGUMENTS = /(?<=[\w$\])])<(?:[^<>()]|<(?:[^<>()]|<[^<>()]*>)*>)*>(?=\()/g;
|
|
643
|
+
LINE_BREAKS = new RegExp(`[^\\r\\n${String.fromCharCode(8232, 8233)}]`, "g");
|
|
644
|
+
blankedOut = (match) => match.replace(LINE_BREAKS, " ");
|
|
645
|
+
CLOSING_TAG = "</_>";
|
|
646
|
+
COMPLETIONS = [CLOSING_TAG, ")", "}", "]"];
|
|
647
|
+
MAX_COMPLETIONS = 24;
|
|
648
|
+
byPlace = (a, b) => a.position - b.position;
|
|
649
|
+
MAX_EXPLAINED = 2e5;
|
|
420
650
|
}
|
|
421
651
|
});
|
|
422
652
|
|
|
423
|
-
// src/
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
let diagonal = row[0];
|
|
428
|
-
row[0] = i;
|
|
429
|
-
for (let j = 1; j <= b.length; j++) {
|
|
430
|
-
const above = row[j];
|
|
431
|
-
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
432
|
-
diagonal = above;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return row[b.length];
|
|
436
|
-
}
|
|
437
|
-
function nearest(wanted, names, limit = 5) {
|
|
438
|
-
const target = squash(wanted);
|
|
439
|
-
return names.map((name) => {
|
|
440
|
-
const candidate = squash(name);
|
|
441
|
-
const apart = distance(target, candidate);
|
|
442
|
-
const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
|
|
443
|
-
return { name, apart, close };
|
|
444
|
-
}).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
|
|
445
|
-
}
|
|
446
|
-
function resolveComponent(contract2, wanted) {
|
|
447
|
-
const names = Object.keys(contract2.components);
|
|
448
|
-
const found = names.find((name) => name === wanted) ?? names.find((name) => squash(name) === squash(wanted));
|
|
449
|
-
if (found !== void 0) return found;
|
|
450
|
-
const suggestions = nearest(wanted, names);
|
|
451
|
-
throw new CliError(
|
|
452
|
-
`Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
|
|
453
|
-
2,
|
|
454
|
-
{ code: "ERR_UNKNOWN_COMPONENT", suggestions }
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
function allowedChildren(rules, name) {
|
|
653
|
+
// src/check.ts
|
|
654
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
655
|
+
import { basename, extname, join, relative, resolve, sep } from "path";
|
|
656
|
+
function allowedChildren2(rules, name) {
|
|
458
657
|
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
459
658
|
if (parent === name) return rule.allowed_children;
|
|
460
|
-
const nested =
|
|
659
|
+
const nested = allowedChildren2(rule.children, name);
|
|
461
660
|
if (nested !== void 0) return nested;
|
|
462
661
|
}
|
|
463
662
|
return void 0;
|
|
464
663
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
664
|
+
function checkSource(contract2, file, text2) {
|
|
665
|
+
const reading = readSyntax(file, text2);
|
|
666
|
+
if (reading.tree === null) {
|
|
667
|
+
const shown = reading.problems.slice(0, MAX_SYNTAX_ERRORS);
|
|
668
|
+
const more = reading.problems.length - shown.length;
|
|
669
|
+
return {
|
|
670
|
+
file,
|
|
671
|
+
status: "not_evaluated",
|
|
672
|
+
limitations: [],
|
|
673
|
+
findings: shown.map(
|
|
674
|
+
({ line, column, message, code }, index) => ({
|
|
675
|
+
code: "SYNTAX_ERROR",
|
|
676
|
+
severity: "error",
|
|
677
|
+
file,
|
|
678
|
+
line,
|
|
679
|
+
column,
|
|
680
|
+
message: `${message} (${code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
|
|
681
|
+
})
|
|
682
|
+
)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const findings = [];
|
|
686
|
+
const limitations = [];
|
|
687
|
+
const place = (node) => ({
|
|
688
|
+
line: node.loc?.start.line ?? 1,
|
|
689
|
+
column: (node.loc?.start.column ?? 0) + 1
|
|
479
690
|
});
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
description: component.description,
|
|
484
|
-
use_for: guide.use_for ?? [],
|
|
485
|
-
not_for: guide.not_for ?? [],
|
|
486
|
-
...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
|
|
487
|
-
parts: partsOf(contract2, name),
|
|
488
|
-
props,
|
|
489
|
-
...component.slots === void 0 ? {} : { slots: component.slots },
|
|
490
|
-
composable_with: component.composable_with ?? [],
|
|
491
|
-
never_inside: component.never_inside ?? [],
|
|
492
|
-
...allowed === void 0 ? {} : { allowed_children: allowed },
|
|
493
|
-
example: component.example
|
|
691
|
+
const textOf = (node) => text2.slice(node.start ?? 0, node.end ?? 0);
|
|
692
|
+
const limit = (node, limitation) => {
|
|
693
|
+
limitations.push({ ...limitation, file, ...place(node) });
|
|
494
694
|
};
|
|
495
|
-
}
|
|
496
|
-
function propLine(prop, dense) {
|
|
497
|
-
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)})`);
|
|
498
|
-
const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
|
|
499
|
-
const lines = [
|
|
500
|
-
` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
|
|
501
|
-
];
|
|
502
|
-
for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
|
|
503
|
-
return lines;
|
|
504
|
-
}
|
|
505
|
-
function renderComponent(detail, { dense }) {
|
|
506
|
-
const lines = [
|
|
507
|
-
detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
|
|
508
|
-
];
|
|
509
|
-
if (detail.not_for.length > 0)
|
|
510
|
-
lines.push(
|
|
511
|
-
`Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
|
|
512
|
-
);
|
|
513
|
-
if (!dense) lines.push("", detail.description, "");
|
|
514
|
-
if (detail.props.length > 0) {
|
|
515
|
-
lines.push("Props:");
|
|
516
|
-
for (const prop of detail.props) lines.push(...propLine(prop, dense));
|
|
517
|
-
}
|
|
518
|
-
for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
|
|
519
|
-
lines.push(
|
|
520
|
-
`${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
|
|
521
|
-
);
|
|
522
|
-
if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
|
|
523
|
-
if (detail.allowed_children !== void 0)
|
|
524
|
-
lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
|
|
525
|
-
if (detail.composable_with.length > 0)
|
|
526
|
-
lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
|
|
527
|
-
if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
|
|
528
|
-
lines.push(
|
|
529
|
-
detail.example.includes("\n") ? `Example:
|
|
530
|
-
${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
|
|
531
|
-
);
|
|
532
|
-
return `${lines.join("\n")}
|
|
533
|
-
`;
|
|
534
|
-
}
|
|
535
|
-
function search(contract2, registry2, query, limit = 10) {
|
|
536
|
-
const wanted = words(query);
|
|
537
|
-
if (wanted.length === 0) return [];
|
|
538
|
-
const whole = squash(query);
|
|
539
|
-
const scores = /* @__PURE__ */ new Map();
|
|
540
|
-
const add = (name, points, why) => {
|
|
541
|
-
const before = scores.get(name) ?? { score: 0 };
|
|
542
|
-
scores.set(name, {
|
|
543
|
-
score: before.score + points,
|
|
544
|
-
...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
|
|
545
|
-
});
|
|
546
|
-
};
|
|
547
|
-
for (const [name, component] of Object.entries(contract2.components)) {
|
|
548
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
549
|
-
const plain = squash(name);
|
|
550
|
-
if (plain === whole) add(name, 100);
|
|
551
|
-
else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
|
|
552
|
-
else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
|
|
553
|
-
for (const phrase of guide.use_for ?? []) {
|
|
554
|
-
const hits = matched(wanted, phrase);
|
|
555
|
-
if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
|
|
556
|
-
}
|
|
557
|
-
for (const { need, use } of guide.not_for ?? []) {
|
|
558
|
-
const hits = matched(wanted, need);
|
|
559
|
-
if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
|
|
560
|
-
add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
|
|
561
|
-
}
|
|
562
|
-
const inDescription = matched(wanted, component.description);
|
|
563
|
-
if (inDescription > 0) add(name, 2 * inDescription);
|
|
564
|
-
}
|
|
565
|
-
const results = [];
|
|
566
|
-
for (const [name, { score, why }] of scores) {
|
|
567
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
568
|
-
results.push({
|
|
569
|
-
kind: "component",
|
|
570
|
-
name,
|
|
571
|
-
purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
|
|
572
|
-
score,
|
|
573
|
-
...why === void 0 ? {} : { why },
|
|
574
|
-
next: `oprt component ${name}`
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
|
|
578
|
-
const recipe = contract2.patterns?.[entry.name];
|
|
579
|
-
const plain = squash(entry.name);
|
|
580
|
-
const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
|
|
581
|
-
const hits = matched(wanted, entry.description);
|
|
582
|
-
const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
|
|
583
|
-
if (score > 0)
|
|
584
|
-
results.push({
|
|
585
|
-
kind: "recipe",
|
|
586
|
-
name: entry.name,
|
|
587
|
-
purpose: firstSentence(entry.description),
|
|
588
|
-
score,
|
|
589
|
-
next: recipe?.install ?? `oprt add ${entry.slug}`
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
593
|
-
const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
|
|
594
|
-
return results.filter(({ score }) => score >= floor).slice(0, limit);
|
|
595
|
-
}
|
|
596
|
-
var squash, firstSentence, partsOf, STOP, words, matched;
|
|
597
|
-
var init_lookup = __esm({
|
|
598
|
-
"src/lookup.ts"() {
|
|
599
|
-
"use strict";
|
|
600
|
-
init_esm_shims();
|
|
601
|
-
init_types();
|
|
602
|
-
squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
603
|
-
firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
|
|
604
|
-
partsOf = (contract2, name) => Object.keys(contract2.components).filter(
|
|
605
|
-
(part) => contract2.guidance.components[part]?.part_of === name
|
|
606
|
-
);
|
|
607
|
-
STOP = new Set(
|
|
608
|
-
"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(
|
|
609
|
-
" "
|
|
610
|
-
)
|
|
611
|
-
);
|
|
612
|
-
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);
|
|
613
|
-
matched = (query, text2) => {
|
|
614
|
-
const present = new Set(words(text2));
|
|
615
|
-
return query.filter((word) => present.has(word)).length;
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// src/check.ts
|
|
621
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
622
|
-
import { createRequire } from "module";
|
|
623
|
-
import { basename, extname, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
624
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
625
|
-
function allowedChildren2(rules, name) {
|
|
626
|
-
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
627
|
-
if (parent === name) return rule.allowed_children;
|
|
628
|
-
const nested = allowedChildren2(rule.children, name);
|
|
629
|
-
if (nested !== void 0) return nested;
|
|
630
|
-
}
|
|
631
|
-
return void 0;
|
|
632
|
-
}
|
|
633
|
-
function checkSource(typescript, contract2, file, text2) {
|
|
634
|
-
const ts = typescript;
|
|
635
|
-
const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
|
|
636
|
-
const source = ts.createSourceFile(file, text2, ts.ScriptTarget.Latest, true, kind);
|
|
637
|
-
const findings = [];
|
|
638
695
|
const report = (node, finding) => {
|
|
639
|
-
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
640
696
|
findings.push({
|
|
641
697
|
code: finding.code,
|
|
642
698
|
severity: finding.severity,
|
|
643
699
|
file,
|
|
644
|
-
|
|
645
|
-
column: character + 1,
|
|
700
|
+
...place(node),
|
|
646
701
|
...finding.component === void 0 ? {} : { component: finding.component },
|
|
647
702
|
...finding.prop === void 0 ? {} : { prop: finding.prop },
|
|
648
703
|
message: finding.message,
|
|
@@ -653,21 +708,18 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
653
708
|
const isComponent = (name) => Object.hasOwn(contract2.components, name);
|
|
654
709
|
const named = /* @__PURE__ */ new Map();
|
|
655
710
|
const namespaces = /* @__PURE__ */ new Set();
|
|
656
|
-
for (const statement of
|
|
657
|
-
if (
|
|
658
|
-
|
|
659
|
-
const from = statement.moduleSpecifier.text;
|
|
660
|
-
const clause = statement.importClause;
|
|
661
|
-
if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
|
|
711
|
+
for (const statement of reading.tree.program.body) {
|
|
712
|
+
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
713
|
+
const from = statement.source.value;
|
|
662
714
|
const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent(basename(from));
|
|
663
715
|
if (from !== PACKAGE && !copied) continue;
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (element2.
|
|
670
|
-
const exported =
|
|
716
|
+
for (const element2 of statement.specifiers) {
|
|
717
|
+
if (element2.type === "ImportNamespaceSpecifier") {
|
|
718
|
+
if (from === PACKAGE) namespaces.add(element2.local.name);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (element2.type !== "ImportSpecifier" || element2.importKind === "type") continue;
|
|
722
|
+
const exported = element2.imported.type === "Identifier" ? element2.imported.name : element2.imported.value;
|
|
671
723
|
if (from === PACKAGE && !contract2.exports.includes(exported)) {
|
|
672
724
|
let suggestions = nearest(exported, Object.keys(contract2.components));
|
|
673
725
|
if (suggestions.length === 0) suggestions = nearest(exported, contract2.exports);
|
|
@@ -692,26 +744,55 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
692
744
|
});
|
|
693
745
|
continue;
|
|
694
746
|
}
|
|
695
|
-
if (isComponent(exported)) named.set(element2.name
|
|
747
|
+
if (isComponent(exported)) named.set(element2.local.name, exported);
|
|
696
748
|
}
|
|
697
749
|
}
|
|
750
|
+
const isString = (node) => node.type === "StringLiteral";
|
|
751
|
+
const plainTemplate = (node) => node.type === "TemplateLiteral" && node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw : void 0;
|
|
698
752
|
const literals = (expression) => {
|
|
699
753
|
if (!expression) return [];
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
if (
|
|
704
|
-
return [...literals(expression.
|
|
754
|
+
if (isString(expression)) return [expression.value];
|
|
755
|
+
const template = plainTemplate(expression);
|
|
756
|
+
if (template !== void 0) return [template];
|
|
757
|
+
if (expression.type === "ConditionalExpression")
|
|
758
|
+
return [...literals(expression.consequent), ...literals(expression.alternate)];
|
|
705
759
|
return [];
|
|
706
760
|
};
|
|
707
|
-
const valueOf = (attribute) =>
|
|
761
|
+
const valueOf = (attribute) => {
|
|
762
|
+
const value = attribute.value;
|
|
763
|
+
if (value === null || value === void 0) return void 0;
|
|
764
|
+
if (value.type === "JSXExpressionContainer")
|
|
765
|
+
return value.expression.type === "JSXEmptyExpression" ? void 0 : value.expression;
|
|
766
|
+
return value;
|
|
767
|
+
};
|
|
768
|
+
const optionValue = (expression) => {
|
|
769
|
+
const read = [];
|
|
770
|
+
const unread = [];
|
|
771
|
+
const walk2 = (node) => {
|
|
772
|
+
const template = plainTemplate(node);
|
|
773
|
+
if (isString(node)) read.push(node.value);
|
|
774
|
+
else if (template !== void 0) read.push(template);
|
|
775
|
+
else if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "TSSatisfiesExpression")
|
|
776
|
+
walk2(node.expression);
|
|
777
|
+
else if (node.type === "ConditionalExpression") {
|
|
778
|
+
walk2(node.consequent);
|
|
779
|
+
walk2(node.alternate);
|
|
780
|
+
} else if (node.type === "LogicalExpression") {
|
|
781
|
+
if (node.operator === "??" || node.operator === "||") {
|
|
782
|
+
walk2(node.left);
|
|
783
|
+
walk2(node.right);
|
|
784
|
+
} else walk2(node.right);
|
|
785
|
+
} else if (!// No value: the component's default applies.
|
|
786
|
+
(node.type === "Identifier" && node.name === "undefined" || node.type === "NullLiteral"))
|
|
787
|
+
unread.push(node);
|
|
788
|
+
};
|
|
789
|
+
if (expression) walk2(expression);
|
|
790
|
+
return { literals: read, unread };
|
|
791
|
+
};
|
|
708
792
|
const classText = (expression) => {
|
|
709
793
|
if (!expression) return "";
|
|
710
|
-
if (
|
|
711
|
-
return
|
|
712
|
-
expression.head.text,
|
|
713
|
-
...expression.templateSpans.map((span) => span.literal.text)
|
|
714
|
-
].join(" ");
|
|
794
|
+
if (expression.type === "TemplateLiteral" && expression.expressions.length > 0)
|
|
795
|
+
return expression.quasis.map((span) => span.value.cooked ?? span.value.raw).join(" ");
|
|
715
796
|
return literals(expression).join(" ");
|
|
716
797
|
};
|
|
717
798
|
const houseRules = (attribute, name) => {
|
|
@@ -737,9 +818,9 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
737
818
|
message: "A rounded corner. The radius is 0 everywhere in OPERATOR."
|
|
738
819
|
});
|
|
739
820
|
}
|
|
740
|
-
if (name === "style" && value &&
|
|
821
|
+
if (name === "style" && value && value.type === "ObjectExpression") {
|
|
741
822
|
for (const property of value.properties)
|
|
742
|
-
if (
|
|
823
|
+
if (property.type === "ObjectProperty" && literals(property.value).some((literal) => RAW_COLOR_VALUE.test(literal)))
|
|
743
824
|
report(attribute, {
|
|
744
825
|
code: "RAW_COLOR",
|
|
745
826
|
severity: "warning",
|
|
@@ -747,56 +828,73 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
747
828
|
});
|
|
748
829
|
}
|
|
749
830
|
};
|
|
831
|
+
const attributeName = (attribute) => attribute.name.type === "JSXIdentifier" ? attribute.name.name : textOf(attribute.name);
|
|
750
832
|
const element = (opening, ancestors, parent, hasChildren, renderedAs) => {
|
|
751
|
-
const tag = opening.
|
|
833
|
+
const tag = opening.name;
|
|
752
834
|
let component = null;
|
|
753
|
-
if (
|
|
754
|
-
component = named.get(tag.
|
|
755
|
-
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.
|
|
835
|
+
if (tag.type === "JSXIdentifier") {
|
|
836
|
+
component = named.get(tag.name) ?? null;
|
|
837
|
+
if (component === null && !renderedAs && Object.hasOwn(NATIVE, tag.name))
|
|
756
838
|
report(opening, {
|
|
757
839
|
code: "NATIVE_CONTROL",
|
|
758
840
|
severity: "warning",
|
|
759
|
-
message: `A native <${tag.
|
|
841
|
+
message: `A native <${tag.name}>: use ${NATIVE[tag.name]}.`
|
|
760
842
|
});
|
|
761
|
-
} else if (
|
|
762
|
-
const owner = tag.
|
|
763
|
-
|
|
843
|
+
} else if (tag.type === "JSXMemberExpression" && tag.object.type === "JSXIdentifier") {
|
|
844
|
+
const owner = tag.object.name;
|
|
845
|
+
const member = tag.property.name;
|
|
846
|
+
if (namespaces.has(owner) && isComponent(member)) component = member;
|
|
764
847
|
else if (named.has(owner)) {
|
|
765
|
-
const joined = `${named.get(owner)}${
|
|
848
|
+
const joined = `${named.get(owner)}${member}`;
|
|
766
849
|
report(opening, {
|
|
767
850
|
code: "DOT_NOTATION",
|
|
768
851
|
severity: "error",
|
|
769
|
-
message: `${owner}.${
|
|
852
|
+
message: `${owner}.${member} is not a component.` + (isComponent(joined) ? ` Use <${joined}>.` : " OPERATOR's parts are components of their own, imported by name.")
|
|
770
853
|
});
|
|
771
854
|
}
|
|
772
855
|
}
|
|
773
|
-
const attributes = opening.attributes
|
|
856
|
+
const attributes = opening.attributes;
|
|
774
857
|
for (const attribute of attributes)
|
|
775
|
-
if (
|
|
776
|
-
houseRules(attribute, attribute.name.
|
|
858
|
+
if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier")
|
|
859
|
+
houseRules(attribute, attribute.name.name);
|
|
777
860
|
if (component === null) return null;
|
|
778
861
|
const definition = contract2.components[component];
|
|
779
|
-
const spread = attributes.some((attribute) =>
|
|
862
|
+
const spread = attributes.some((attribute) => attribute.type === "JSXSpreadAttribute");
|
|
863
|
+
for (const attribute of attributes)
|
|
864
|
+
if (attribute.type === "JSXSpreadAttribute")
|
|
865
|
+
limit(attribute, {
|
|
866
|
+
code: "UNRESOLVED_SPREAD",
|
|
867
|
+
component,
|
|
868
|
+
message: `Props spread into <${component}> (${quote(textOf(attribute))}) were not read: option values and required props that arrive through them were not checked.`
|
|
869
|
+
});
|
|
780
870
|
const given = /* @__PURE__ */ new Set();
|
|
781
871
|
for (const attribute of attributes) {
|
|
782
|
-
if (
|
|
783
|
-
const prop =
|
|
872
|
+
if (attribute.type !== "JSXAttribute") continue;
|
|
873
|
+
const prop = attributeName(attribute);
|
|
784
874
|
given.add(prop);
|
|
785
875
|
const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
|
|
786
876
|
if (known === void 0) {
|
|
787
877
|
if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
|
|
788
|
-
const
|
|
878
|
+
const names2 = Object.keys(definition.props);
|
|
789
879
|
report(attribute, {
|
|
790
880
|
code: "UNKNOWN_PROP",
|
|
791
881
|
severity: "warning",
|
|
792
882
|
component,
|
|
793
883
|
prop,
|
|
794
|
-
message: `${component} has no prop ${prop}.` + (
|
|
884
|
+
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" : ""}.`)
|
|
795
885
|
});
|
|
796
886
|
continue;
|
|
797
887
|
}
|
|
798
888
|
if (known.type !== "enum" || known.values === void 0) continue;
|
|
799
|
-
|
|
889
|
+
const { literals: options, unread } = optionValue(valueOf(attribute));
|
|
890
|
+
for (const expression of unread)
|
|
891
|
+
limit(attribute, {
|
|
892
|
+
code: "UNRESOLVED_VALUE",
|
|
893
|
+
component,
|
|
894
|
+
prop,
|
|
895
|
+
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.`
|
|
896
|
+
});
|
|
897
|
+
for (const literal of options) {
|
|
800
898
|
if (known.values.includes(literal)) continue;
|
|
801
899
|
const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
|
|
802
900
|
report(attribute, {
|
|
@@ -843,117 +941,945 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
843
941
|
return component;
|
|
844
942
|
};
|
|
845
943
|
const visit = (node, ancestors, parent, renderedAs = false) => {
|
|
846
|
-
if (
|
|
847
|
-
const opening =
|
|
848
|
-
const hasChildren =
|
|
849
|
-
(child) => !(
|
|
944
|
+
if (node.type === "JSXElement") {
|
|
945
|
+
const opening = node.openingElement;
|
|
946
|
+
const hasChildren = node.children.some(
|
|
947
|
+
(child) => !(child.type === "JSXText" && /^\s*$/.test(text2.slice(child.start ?? 0, child.end ?? 0)))
|
|
850
948
|
);
|
|
851
949
|
const component = element(opening, ancestors, parent, hasChildren, renderedAs);
|
|
852
|
-
const asChild = component !== null && opening.attributes.
|
|
853
|
-
(attribute) =>
|
|
950
|
+
const asChild = component !== null && opening.attributes.some(
|
|
951
|
+
(attribute) => attribute.type === "JSXAttribute" && attributeName(attribute) === "asChild"
|
|
952
|
+
);
|
|
953
|
+
for (const attribute of opening.attributes) visit(attribute, [], null);
|
|
954
|
+
for (const child of node.children)
|
|
955
|
+
visit(
|
|
956
|
+
child,
|
|
957
|
+
component === null ? ancestors : [...ancestors, component],
|
|
958
|
+
component,
|
|
959
|
+
asChild
|
|
960
|
+
);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
for (const child of childNodes(node)) visit(child, ancestors, parent);
|
|
964
|
+
};
|
|
965
|
+
visit(reading.tree.program, [], null);
|
|
966
|
+
const byPlace2 = (a, b) => a.line - b.line || a.column - b.column;
|
|
967
|
+
return {
|
|
968
|
+
file,
|
|
969
|
+
status: limitations.length > 0 ? "partial" : "evaluated",
|
|
970
|
+
findings: findings.sort(byPlace2),
|
|
971
|
+
limitations: limitations.sort(byPlace2)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function* sources(path2) {
|
|
975
|
+
if (excluded(basename(path2))) return;
|
|
976
|
+
if (statSync(path2).isDirectory()) {
|
|
977
|
+
for (const entry of readdirSync(path2).sort()) yield* sources(join(path2, entry));
|
|
978
|
+
} else if (READ.has(extname(path2)) && !NOT_INTERFACE.test(basename(path2))) yield path2;
|
|
979
|
+
}
|
|
980
|
+
function whyNotRead(path2) {
|
|
981
|
+
const name = basename(path2);
|
|
982
|
+
if (excluded(name)) return "excluded_folder";
|
|
983
|
+
if (statSync(path2).isDirectory()) return null;
|
|
984
|
+
if (!READ.has(extname(path2))) return "unsupported_extension";
|
|
985
|
+
return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
|
|
986
|
+
}
|
|
987
|
+
function buildCheckReport(evaluations, context = {}) {
|
|
988
|
+
const findings = evaluations.flatMap(({ findings: found }) => found);
|
|
989
|
+
const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
|
|
990
|
+
const partial = count("partial");
|
|
991
|
+
const notEvaluated = count("not_evaluated");
|
|
992
|
+
const evaluated = evaluations.length - notEvaluated;
|
|
993
|
+
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
994
|
+
const warnings = findings.length - errors;
|
|
995
|
+
let status;
|
|
996
|
+
if (evaluations.length === 0) status = "no_files_evaluated";
|
|
997
|
+
else if (evaluated === 0) status = "not_evaluated";
|
|
998
|
+
else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
|
|
999
|
+
else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
|
|
1000
|
+
return {
|
|
1001
|
+
evaluation_status: status,
|
|
1002
|
+
files: evaluations.length,
|
|
1003
|
+
findings,
|
|
1004
|
+
summary: { errors, warnings },
|
|
1005
|
+
coverage: {
|
|
1006
|
+
files_found: evaluations.length,
|
|
1007
|
+
files_evaluated: evaluated,
|
|
1008
|
+
files_partial: partial,
|
|
1009
|
+
files_not_evaluated: notEvaluated
|
|
1010
|
+
},
|
|
1011
|
+
limitations: evaluations.flatMap(({ limitations }) => limitations),
|
|
1012
|
+
skipped_inputs: context.skipped ?? [],
|
|
1013
|
+
scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
|
|
1014
|
+
parser: context.parser ?? PARSER
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function checkExitCode(report, { strict }) {
|
|
1018
|
+
if (report.evaluation_status === "no_files_evaluated") return 1;
|
|
1019
|
+
if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
|
|
1020
|
+
return strict && report.summary.warnings > 0 ? 1 : 0;
|
|
1021
|
+
}
|
|
1022
|
+
function checkPaths(paths, context) {
|
|
1023
|
+
const root = resolve(context.cwd);
|
|
1024
|
+
const files = [];
|
|
1025
|
+
const skipped = [];
|
|
1026
|
+
for (const path2 of paths) {
|
|
1027
|
+
const absolute = resolve(root, path2);
|
|
1028
|
+
if (!existsSync(absolute))
|
|
1029
|
+
throw new CliError(`No such file or folder: ${path2}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
1030
|
+
const reason = whyNotRead(absolute);
|
|
1031
|
+
if (reason !== null) skipped.push({ path: path2, reason });
|
|
1032
|
+
files.push(...sources(absolute));
|
|
1033
|
+
}
|
|
1034
|
+
loadBabel();
|
|
1035
|
+
const evaluations = [...new Set(files)].map(
|
|
1036
|
+
(file) => checkSource(
|
|
1037
|
+
context.contract,
|
|
1038
|
+
relative(root, file).split(sep).join("/"),
|
|
1039
|
+
readFileSync(file, "utf8")
|
|
1040
|
+
)
|
|
1041
|
+
);
|
|
1042
|
+
return buildCheckReport(evaluations, { skipped });
|
|
1043
|
+
}
|
|
1044
|
+
function renderCheckText(report) {
|
|
1045
|
+
const lines = report.findings.map(
|
|
1046
|
+
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1047
|
+
);
|
|
1048
|
+
const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
|
|
1049
|
+
for (const { file, line, column, code, message } of shown)
|
|
1050
|
+
lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
|
|
1051
|
+
if (report.limitations.length > shown.length)
|
|
1052
|
+
lines.push(
|
|
1053
|
+
` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
|
|
1054
|
+
);
|
|
1055
|
+
const { coverage, evaluation_status: status } = report;
|
|
1056
|
+
if (status === "no_files_evaluated") {
|
|
1057
|
+
lines.push("No .tsx or .jsx file was read, so nothing was checked.");
|
|
1058
|
+
const labels = {
|
|
1059
|
+
unsupported_extension: "not a .tsx or .jsx file",
|
|
1060
|
+
test_or_story_file: "a test, spec or story file, which is skipped",
|
|
1061
|
+
excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
|
|
1062
|
+
};
|
|
1063
|
+
for (const { path: path2, reason } of report.skipped_inputs)
|
|
1064
|
+
lines.push(` ${path2}: ${labels[reason]}`);
|
|
1065
|
+
} else if (coverage.files_not_evaluated > 0)
|
|
1066
|
+
lines.push(
|
|
1067
|
+
`${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
|
|
1068
|
+
);
|
|
1069
|
+
else if (status === "evaluated_clean")
|
|
1070
|
+
lines.push(
|
|
1071
|
+
"Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
|
|
1072
|
+
);
|
|
1073
|
+
lines.push(
|
|
1074
|
+
`${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
|
|
1075
|
+
);
|
|
1076
|
+
return `${lines.join("\n")}
|
|
1077
|
+
`;
|
|
1078
|
+
}
|
|
1079
|
+
var FINDING_CODES, LIMITATION_CODES, SCOPE, PACKAGE, NATIVE, NATIVE_ATTRIBUTES, nativeAttribute, RAW_COLOR_CLASS, RAW_COLOR_VALUE, ARBITRARY_SIZE, ROUNDED_CORNER, quote, MAX_SYNTAX_ERRORS, SKIPPED, READ, NOT_INTERFACE, excluded, plural, MAX_LIMITS_SHOWN;
|
|
1080
|
+
var init_check = __esm({
|
|
1081
|
+
"src/check.ts"() {
|
|
1082
|
+
"use strict";
|
|
1083
|
+
init_esm_shims();
|
|
1084
|
+
init_lookup();
|
|
1085
|
+
init_parser();
|
|
1086
|
+
init_syntax();
|
|
1087
|
+
init_types();
|
|
1088
|
+
FINDING_CODES = [
|
|
1089
|
+
"INVALID_PROP_VALUE",
|
|
1090
|
+
"UNKNOWN_EXPORT",
|
|
1091
|
+
"DOT_NOTATION",
|
|
1092
|
+
"INVALID_NESTING",
|
|
1093
|
+
"INVALID_CHILD",
|
|
1094
|
+
"MISSING_REQUIRED_PROP",
|
|
1095
|
+
"UNKNOWN_PROP",
|
|
1096
|
+
"RAW_COLOR",
|
|
1097
|
+
"ARBITRARY_SIZE",
|
|
1098
|
+
"ROUNDED_CORNER",
|
|
1099
|
+
"NATIVE_CONTROL",
|
|
1100
|
+
/** The file does not parse. It was not checked against the contract. */
|
|
1101
|
+
"SYNTAX_ERROR"
|
|
1102
|
+
];
|
|
1103
|
+
LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
|
|
1104
|
+
SCOPE = {
|
|
1105
|
+
checked: ["syntax", "contract", "house_rules"],
|
|
1106
|
+
not_checked: ["types", "render"]
|
|
1107
|
+
};
|
|
1108
|
+
PACKAGE = "@oprt/react";
|
|
1109
|
+
NATIVE = {
|
|
1110
|
+
button: "Button",
|
|
1111
|
+
input: "Input, or the control for its type (Checkbox, RadioGroup, NumberInput, Slider, DatePicker, FileUpload)",
|
|
1112
|
+
select: "Select",
|
|
1113
|
+
textarea: "Textarea",
|
|
1114
|
+
table: "DataTable or DataGrid",
|
|
1115
|
+
dialog: "Dialog",
|
|
1116
|
+
progress: "Progress",
|
|
1117
|
+
hr: "Divider"
|
|
1118
|
+
};
|
|
1119
|
+
NATIVE_ATTRIBUTES = new Set(
|
|
1120
|
+
`key ref children className style id title role tabIndex hidden lang dir slot draggable spellCheck
|
|
1121
|
+
translate inert accessKey autoCapitalize contentEditable enterKeyHint inputMode
|
|
1122
|
+
dangerouslySetInnerHTML suppressHydrationWarning suppressContentEditableWarning htmlFor type name
|
|
1123
|
+
value defaultValue checked defaultChecked disabled readOnly required placeholder autoComplete
|
|
1124
|
+
autoFocus min max step minLength maxLength pattern multiple accept form formAction href target rel
|
|
1125
|
+
download src alt width height loading rows cols wrap open colSpan rowSpan scope headers label list
|
|
1126
|
+
size method action encType noValidate popover popoverTarget popoverTargetAction asChild`.split(
|
|
1127
|
+
/\s+/
|
|
1128
|
+
)
|
|
1129
|
+
);
|
|
1130
|
+
nativeAttribute = (name) => NATIVE_ATTRIBUTES.has(name) || /^(aria-|data-)/.test(name) || /^on[A-Z]/.test(name);
|
|
1131
|
+
RAW_COLOR_CLASS = /\[(?:#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\()/;
|
|
1132
|
+
RAW_COLOR_VALUE = /^#[0-9a-fA-F]{3,8}$|^(?:rgb|hsl)a?\(/;
|
|
1133
|
+
ARBITRARY_SIZE = /(?:^|[\s:])-?[a-z][a-z0-9-]*-\[-?\d*\.?\d+px\]/;
|
|
1134
|
+
ROUNDED_CORNER = /(?:^|[\s:])rounded(?!-none(?:\s|$))(?:-[^\s]+)?(?=\s|$)/;
|
|
1135
|
+
quote = (text2) => {
|
|
1136
|
+
const flat = text2.replace(/\s+/g, " ").trim();
|
|
1137
|
+
return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
|
|
1138
|
+
};
|
|
1139
|
+
MAX_SYNTAX_ERRORS = 10;
|
|
1140
|
+
SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
1141
|
+
READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
1142
|
+
NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
1143
|
+
excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
|
|
1144
|
+
plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1145
|
+
MAX_LIMITS_SHOWN = 20;
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// package.json
|
|
1150
|
+
var package_default;
|
|
1151
|
+
var init_package2 = __esm({
|
|
1152
|
+
"package.json"() {
|
|
1153
|
+
package_default = {
|
|
1154
|
+
name: "@oprt/cli",
|
|
1155
|
+
version: "0.4.0",
|
|
1156
|
+
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.",
|
|
1157
|
+
keywords: [
|
|
1158
|
+
"design-system",
|
|
1159
|
+
"cli",
|
|
1160
|
+
"coding-agents",
|
|
1161
|
+
"mcp",
|
|
1162
|
+
"model-context-protocol",
|
|
1163
|
+
"code-check",
|
|
1164
|
+
"components"
|
|
1165
|
+
],
|
|
1166
|
+
homepage: "https://oprt.dev/docs/agents/cli",
|
|
1167
|
+
type: "module",
|
|
1168
|
+
main: "./dist/index.cjs",
|
|
1169
|
+
module: "./dist/index.js",
|
|
1170
|
+
types: "./dist/index.d.ts",
|
|
1171
|
+
bin: {
|
|
1172
|
+
oprt: "./dist/bin.js"
|
|
1173
|
+
},
|
|
1174
|
+
engines: {
|
|
1175
|
+
node: ">=22"
|
|
1176
|
+
},
|
|
1177
|
+
exports: {
|
|
1178
|
+
".": {
|
|
1179
|
+
import: {
|
|
1180
|
+
types: "./dist/index.d.ts",
|
|
1181
|
+
default: "./dist/index.js"
|
|
1182
|
+
},
|
|
1183
|
+
require: {
|
|
1184
|
+
types: "./dist/index.d.cts",
|
|
1185
|
+
default: "./dist/index.cjs"
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
"./read": {
|
|
1189
|
+
import: {
|
|
1190
|
+
types: "./dist/read.d.ts",
|
|
1191
|
+
default: "./dist/read.js"
|
|
1192
|
+
},
|
|
1193
|
+
require: {
|
|
1194
|
+
types: "./dist/read.d.cts",
|
|
1195
|
+
default: "./dist/read.cjs"
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
files: [
|
|
1200
|
+
"dist"
|
|
1201
|
+
],
|
|
1202
|
+
scripts: {
|
|
1203
|
+
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
1204
|
+
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
1205
|
+
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
1206
|
+
test: "vitest run",
|
|
1207
|
+
typecheck: "tsc --noEmit",
|
|
1208
|
+
lint: "eslint src scripts"
|
|
1209
|
+
},
|
|
1210
|
+
devDependencies: {
|
|
1211
|
+
"@babel/parser": "8.0.8",
|
|
1212
|
+
"@babel/types": "8.0.8",
|
|
1213
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
1214
|
+
tsup: "^8.3.0",
|
|
1215
|
+
typescript: "^5.6.3",
|
|
1216
|
+
vitest: "^2.1.3"
|
|
1217
|
+
},
|
|
1218
|
+
license: "MIT",
|
|
1219
|
+
publishConfig: {
|
|
1220
|
+
access: "public"
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
// src/identity.ts
|
|
1227
|
+
import { createHash } from "crypto";
|
|
1228
|
+
function contractDigest(contract2) {
|
|
1229
|
+
let digest = digests.get(contract2);
|
|
1230
|
+
if (digest === void 0) {
|
|
1231
|
+
digest = `sha256:${createHash("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
|
|
1232
|
+
digests.set(contract2, digest);
|
|
1233
|
+
}
|
|
1234
|
+
return digest;
|
|
1235
|
+
}
|
|
1236
|
+
function identityOf(contract2) {
|
|
1237
|
+
return {
|
|
1238
|
+
cli_version: VERSION,
|
|
1239
|
+
contract_version: contract2.version,
|
|
1240
|
+
contract_digest: contractDigest(contract2),
|
|
1241
|
+
schema_version: RESULT_SCHEMA_VERSION
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
var VERSION, RESULT_SCHEMA_VERSION, digests;
|
|
1245
|
+
var init_identity = __esm({
|
|
1246
|
+
"src/identity.ts"() {
|
|
1247
|
+
"use strict";
|
|
1248
|
+
init_esm_shims();
|
|
1249
|
+
init_package2();
|
|
1250
|
+
VERSION = package_default.version;
|
|
1251
|
+
RESULT_SCHEMA_VERSION = 2;
|
|
1252
|
+
digests = /* @__PURE__ */ new WeakMap();
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// src/results.ts
|
|
1257
|
+
function errorEnvelope(error, contract2) {
|
|
1258
|
+
const known = error instanceof CliError ? error : void 0;
|
|
1259
|
+
const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
|
|
1260
|
+
cli_version,
|
|
1261
|
+
schema_version,
|
|
1262
|
+
contract_version,
|
|
1263
|
+
contract_digest
|
|
1264
|
+
}))(identityOf(contract2));
|
|
1265
|
+
return {
|
|
1266
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1268
|
+
code: known?.code ?? "ERR_FAILED",
|
|
1269
|
+
...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
|
|
1270
|
+
identity
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function componentData(detail, dense) {
|
|
1274
|
+
if (!dense) return { ...detail, dense };
|
|
1275
|
+
const { description: _description, ...rest } = detail;
|
|
1276
|
+
return {
|
|
1277
|
+
...rest,
|
|
1278
|
+
props: detail.props.map(
|
|
1279
|
+
(prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
|
|
1280
|
+
),
|
|
1281
|
+
dense
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function instructionsData(contract2) {
|
|
1285
|
+
return {
|
|
1286
|
+
rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
|
|
1287
|
+
id,
|
|
1288
|
+
rule,
|
|
1289
|
+
...wrong === void 0 ? {} : { wrong },
|
|
1290
|
+
...right === void 0 ? {} : { right }
|
|
1291
|
+
})),
|
|
1292
|
+
components: componentIndex(contract2),
|
|
1293
|
+
recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
|
|
1294
|
+
name,
|
|
1295
|
+
description,
|
|
1296
|
+
install: install2
|
|
1297
|
+
}))
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function bundle(root) {
|
|
1301
|
+
const used = {};
|
|
1302
|
+
const visit = (node) => {
|
|
1303
|
+
if (Array.isArray(node)) node.forEach(visit);
|
|
1304
|
+
else if (typeof node === "object" && node !== null) {
|
|
1305
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1306
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1307
|
+
const name = value.replace("#/$defs/", "");
|
|
1308
|
+
if (used[name] === void 0) {
|
|
1309
|
+
used[name] = DEFS[name];
|
|
1310
|
+
visit(DEFS[name]);
|
|
1311
|
+
}
|
|
1312
|
+
} else visit(value);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
visit(root);
|
|
1317
|
+
return { ...root, $defs: used };
|
|
1318
|
+
}
|
|
1319
|
+
function schemaFor(type) {
|
|
1320
|
+
return bundle(type === "error" ? failure() : success(type));
|
|
1321
|
+
}
|
|
1322
|
+
function shareRepeats(schema) {
|
|
1323
|
+
const count = /* @__PURE__ */ new Map();
|
|
1324
|
+
const tally = (node) => {
|
|
1325
|
+
if (Array.isArray(node)) node.forEach(tally);
|
|
1326
|
+
else if (typeof node === "object" && node !== null) {
|
|
1327
|
+
const text2 = JSON.stringify(node);
|
|
1328
|
+
if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
|
|
1329
|
+
Object.values(node).forEach(tally);
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
tally(schema);
|
|
1333
|
+
const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
|
|
1334
|
+
if (repeated.length === 0) return schema;
|
|
1335
|
+
const names2 = new Map(
|
|
1336
|
+
repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
|
|
1337
|
+
);
|
|
1338
|
+
const defs = {};
|
|
1339
|
+
const rewrite = (node) => {
|
|
1340
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
1341
|
+
if (typeof node !== "object" || node === null) return node;
|
|
1342
|
+
const name = names2.get(JSON.stringify(node));
|
|
1343
|
+
if (name !== void 0) {
|
|
1344
|
+
defs[name] = node;
|
|
1345
|
+
return { $ref: `#/$defs/${name}` };
|
|
1346
|
+
}
|
|
1347
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
|
|
1348
|
+
};
|
|
1349
|
+
const rewritten = rewrite(schema);
|
|
1350
|
+
return { ...rewritten, $defs: defs };
|
|
1351
|
+
}
|
|
1352
|
+
function outputSchemaFor(type) {
|
|
1353
|
+
return shareRepeats({
|
|
1354
|
+
type: "object",
|
|
1355
|
+
properties: {
|
|
1356
|
+
type: { const: type },
|
|
1357
|
+
// The four versions and the digest; the docs name them, and so does every answer.
|
|
1358
|
+
identity: { type: "object" },
|
|
1359
|
+
data: SUMMARIES[type]
|
|
1360
|
+
},
|
|
1361
|
+
required: ["identity"],
|
|
1362
|
+
anyOf: [{ required: ["data"] }, { required: ["error"] }]
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
var RESULT_TYPES, envelope, str, int, bool, arr, names, words2, ref, obj, scalar, DEFS, DATA, success, failure, S, I, B, list, shape, strings, SUMMARIES, RESULT_SCHEMAS;
|
|
1366
|
+
var init_results = __esm({
|
|
1367
|
+
"src/results.ts"() {
|
|
1368
|
+
"use strict";
|
|
1369
|
+
init_esm_shims();
|
|
1370
|
+
init_check();
|
|
1371
|
+
init_identity();
|
|
1372
|
+
init_lookup();
|
|
1373
|
+
init_types();
|
|
1374
|
+
RESULT_TYPES = [
|
|
1375
|
+
"check",
|
|
1376
|
+
"search",
|
|
1377
|
+
"component.detail",
|
|
1378
|
+
"component.batch",
|
|
1379
|
+
"component.list",
|
|
1380
|
+
"init",
|
|
1381
|
+
"doctor",
|
|
1382
|
+
"add",
|
|
1383
|
+
"list",
|
|
1384
|
+
"instructions"
|
|
1385
|
+
];
|
|
1386
|
+
envelope = (type, data, contract2) => ({
|
|
1387
|
+
apiVersion: RESULT_SCHEMA_VERSION,
|
|
1388
|
+
type,
|
|
1389
|
+
identity: identityOf(contract2),
|
|
1390
|
+
data
|
|
1391
|
+
});
|
|
1392
|
+
str = (extra = {}) => ({ type: "string", ...extra });
|
|
1393
|
+
int = () => ({ type: "integer", minimum: 0 });
|
|
1394
|
+
bool = () => ({ type: "boolean" });
|
|
1395
|
+
arr = (items) => ({ type: "array", items });
|
|
1396
|
+
names = () => arr(str());
|
|
1397
|
+
words2 = (...values) => ({ type: "string", enum: [...values] });
|
|
1398
|
+
ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
1399
|
+
obj = (properties, optional = [], open = false) => ({
|
|
1400
|
+
type: "object",
|
|
1401
|
+
properties,
|
|
1402
|
+
required: Object.keys(properties).filter((key) => !optional.includes(key)),
|
|
1403
|
+
additionalProperties: open
|
|
1404
|
+
});
|
|
1405
|
+
scalar = { type: ["string", "number", "boolean"] };
|
|
1406
|
+
DEFS = {
|
|
1407
|
+
identity: obj({
|
|
1408
|
+
cli_version: str(),
|
|
1409
|
+
contract_version: str(),
|
|
1410
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
|
|
1411
|
+
schema_version: { type: "integer", minimum: 1 }
|
|
1412
|
+
}),
|
|
1413
|
+
error_identity: obj(
|
|
1414
|
+
{
|
|
1415
|
+
cli_version: str(),
|
|
1416
|
+
schema_version: { type: "integer", minimum: 1 },
|
|
1417
|
+
contract_version: str(),
|
|
1418
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
|
|
1419
|
+
},
|
|
1420
|
+
["contract_version", "contract_digest"]
|
|
1421
|
+
),
|
|
1422
|
+
finding: obj(
|
|
1423
|
+
{
|
|
1424
|
+
code: words2(...FINDING_CODES),
|
|
1425
|
+
severity: words2("error", "warning"),
|
|
1426
|
+
file: str(),
|
|
1427
|
+
line: { type: "integer", minimum: 1 },
|
|
1428
|
+
column: { type: "integer", minimum: 1 },
|
|
1429
|
+
component: str(),
|
|
1430
|
+
prop: str(),
|
|
1431
|
+
message: str(),
|
|
1432
|
+
valid_values: names(),
|
|
1433
|
+
suggestions: names()
|
|
1434
|
+
},
|
|
1435
|
+
["component", "prop", "valid_values", "suggestions"]
|
|
1436
|
+
),
|
|
1437
|
+
limitation: obj(
|
|
1438
|
+
{
|
|
1439
|
+
code: words2(...LIMITATION_CODES),
|
|
1440
|
+
file: str(),
|
|
1441
|
+
line: { type: "integer", minimum: 1 },
|
|
1442
|
+
column: { type: "integer", minimum: 1 },
|
|
1443
|
+
component: str(),
|
|
1444
|
+
prop: str(),
|
|
1445
|
+
message: str()
|
|
1446
|
+
},
|
|
1447
|
+
["prop"]
|
|
1448
|
+
),
|
|
1449
|
+
skipped_input: obj({
|
|
1450
|
+
path: str(),
|
|
1451
|
+
reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
|
|
1452
|
+
}),
|
|
1453
|
+
parser: obj({
|
|
1454
|
+
name: words2("@babel/parser"),
|
|
1455
|
+
version: str(),
|
|
1456
|
+
source: words2("bundled")
|
|
1457
|
+
}),
|
|
1458
|
+
check_data: obj({
|
|
1459
|
+
evaluation_status: words2(
|
|
1460
|
+
"evaluated_clean",
|
|
1461
|
+
"evaluated_with_findings",
|
|
1462
|
+
"partially_evaluated",
|
|
1463
|
+
"not_evaluated",
|
|
1464
|
+
"no_files_evaluated"
|
|
1465
|
+
),
|
|
1466
|
+
files: int(),
|
|
1467
|
+
findings: arr(ref("finding")),
|
|
1468
|
+
summary: obj({ errors: int(), warnings: int() }),
|
|
1469
|
+
coverage: obj({
|
|
1470
|
+
files_found: int(),
|
|
1471
|
+
files_evaluated: int(),
|
|
1472
|
+
files_partial: int(),
|
|
1473
|
+
files_not_evaluated: int()
|
|
1474
|
+
}),
|
|
1475
|
+
limitations: arr(ref("limitation")),
|
|
1476
|
+
skipped_inputs: arr(ref("skipped_input")),
|
|
1477
|
+
scope: obj({ checked: names(), not_checked: names() }),
|
|
1478
|
+
parser: ref("parser")
|
|
1479
|
+
}),
|
|
1480
|
+
search_result: obj(
|
|
1481
|
+
{
|
|
1482
|
+
kind: words2("component", "recipe"),
|
|
1483
|
+
name: str(),
|
|
1484
|
+
purpose: str(),
|
|
1485
|
+
score: { type: "number" },
|
|
1486
|
+
why: str(),
|
|
1487
|
+
next: str()
|
|
1488
|
+
},
|
|
1489
|
+
["why"]
|
|
1490
|
+
),
|
|
1491
|
+
search_data: obj({
|
|
1492
|
+
query: str(),
|
|
1493
|
+
limit: { type: "integer", minimum: 1 },
|
|
1494
|
+
matchCount: int(),
|
|
1495
|
+
results: arr(ref("search_result"))
|
|
1496
|
+
}),
|
|
1497
|
+
index_entry: obj({ name: str(), purpose: str(), parts: names() }),
|
|
1498
|
+
component_list_data: obj({ components: arr(ref("index_entry")) }),
|
|
1499
|
+
detail_prop: obj(
|
|
1500
|
+
{
|
|
1501
|
+
name: str(),
|
|
1502
|
+
type: str(),
|
|
1503
|
+
values: names(),
|
|
1504
|
+
types: names(),
|
|
1505
|
+
default: scalar,
|
|
1506
|
+
required: bool(),
|
|
1507
|
+
description: str(),
|
|
1508
|
+
notes: { type: "object", additionalProperties: str() }
|
|
1509
|
+
},
|
|
1510
|
+
["values", "types", "default", "required", "description", "notes"],
|
|
1511
|
+
// The contract may describe a prop with more than these (an array's item type, for one).
|
|
1512
|
+
true
|
|
1513
|
+
),
|
|
1514
|
+
component_detail: obj(
|
|
1515
|
+
{
|
|
1516
|
+
name: str(),
|
|
1517
|
+
description: str(),
|
|
1518
|
+
use_for: names(),
|
|
1519
|
+
not_for: arr(obj({ need: str(), use: str() })),
|
|
1520
|
+
part_of: str(),
|
|
1521
|
+
parts: names(),
|
|
1522
|
+
props: arr(ref("detail_prop")),
|
|
1523
|
+
slots: {
|
|
1524
|
+
type: "object",
|
|
1525
|
+
additionalProperties: obj({ type: str(), required: bool() }, ["required"])
|
|
1526
|
+
},
|
|
1527
|
+
composable_with: names(),
|
|
1528
|
+
never_inside: names(),
|
|
1529
|
+
allowed_children: names(),
|
|
1530
|
+
example: str(),
|
|
1531
|
+
dense: bool()
|
|
1532
|
+
},
|
|
1533
|
+
["description", "part_of", "slots", "allowed_children"]
|
|
1534
|
+
),
|
|
1535
|
+
component_batch_data: obj({ components: arr(ref("component_detail")) }),
|
|
1536
|
+
doctor_check: obj(
|
|
1537
|
+
{ id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
|
|
1538
|
+
["fix"]
|
|
1539
|
+
),
|
|
1540
|
+
doctor_data: obj({
|
|
1541
|
+
consumer_directory: str(),
|
|
1542
|
+
operations: obj({
|
|
1543
|
+
lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
|
|
1544
|
+
check_code: obj(
|
|
1545
|
+
{
|
|
1546
|
+
status: words2("ready", "blocked"),
|
|
1547
|
+
message: str(),
|
|
1548
|
+
reason: words2("parser_missing", "parser_unusable"),
|
|
1549
|
+
fix: str(),
|
|
1550
|
+
parser: ref("parser")
|
|
1551
|
+
},
|
|
1552
|
+
["reason", "fix", "parser"]
|
|
1553
|
+
)
|
|
1554
|
+
}),
|
|
1555
|
+
checks: arr(ref("doctor_check")),
|
|
1556
|
+
summary: obj({ ok: int(), warn: int(), fail: int() })
|
|
1557
|
+
}),
|
|
1558
|
+
init_data: obj({
|
|
1559
|
+
version: str(),
|
|
1560
|
+
dryRun: bool(),
|
|
1561
|
+
files: arr(
|
|
1562
|
+
obj({
|
|
1563
|
+
path: str(),
|
|
1564
|
+
action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
|
|
1565
|
+
})
|
|
1566
|
+
)
|
|
1567
|
+
}),
|
|
1568
|
+
install_file: obj({
|
|
1569
|
+
path: str(),
|
|
1570
|
+
destination: str(),
|
|
1571
|
+
action: words2("create", "overwrite", "skip")
|
|
1572
|
+
}),
|
|
1573
|
+
add_data: obj({
|
|
1574
|
+
outputDirectory: str(),
|
|
1575
|
+
entries: names(),
|
|
1576
|
+
dependencies: names(),
|
|
1577
|
+
devDependencies: names(),
|
|
1578
|
+
ranges: { type: "object", additionalProperties: str() },
|
|
1579
|
+
files: arr(ref("install_file")),
|
|
1580
|
+
dryRun: bool(),
|
|
1581
|
+
setup: ref("setup")
|
|
1582
|
+
}),
|
|
1583
|
+
setup: obj({
|
|
1584
|
+
tailwind: str(),
|
|
1585
|
+
packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
|
|
1586
|
+
stylesheet: arr(obj({ line: str(), why: str() })),
|
|
1587
|
+
root_import: obj({ line: str(), note: str() }),
|
|
1588
|
+
not_needed: arr(obj({ name: str(), why: str() }))
|
|
1589
|
+
}),
|
|
1590
|
+
list_data: obj({
|
|
1591
|
+
version: str(),
|
|
1592
|
+
entries: arr(
|
|
1593
|
+
obj(
|
|
1594
|
+
{
|
|
1595
|
+
name: str(),
|
|
1596
|
+
slug: str(),
|
|
1597
|
+
kind: words2("component", "recipe"),
|
|
1598
|
+
description: str(),
|
|
1599
|
+
files: names(),
|
|
1600
|
+
dependencies: names(),
|
|
1601
|
+
devDependencies: names()
|
|
1602
|
+
},
|
|
1603
|
+
["devDependencies"]
|
|
1604
|
+
)
|
|
1605
|
+
),
|
|
1606
|
+
ranges: { type: "object", additionalProperties: str() }
|
|
1607
|
+
}),
|
|
1608
|
+
instructions_data: obj({
|
|
1609
|
+
rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
|
|
1610
|
+
components: arr(ref("index_entry")),
|
|
1611
|
+
recipes: arr(obj({ name: str(), description: str(), install: str() }))
|
|
1612
|
+
})
|
|
1613
|
+
};
|
|
1614
|
+
DATA = {
|
|
1615
|
+
check: "check_data",
|
|
1616
|
+
search: "search_data",
|
|
1617
|
+
"component.detail": "component_detail",
|
|
1618
|
+
"component.batch": "component_batch_data",
|
|
1619
|
+
"component.list": "component_list_data",
|
|
1620
|
+
init: "init_data",
|
|
1621
|
+
doctor: "doctor_data",
|
|
1622
|
+
add: "add_data",
|
|
1623
|
+
list: "list_data",
|
|
1624
|
+
instructions: "instructions_data"
|
|
1625
|
+
};
|
|
1626
|
+
success = (type) => obj({
|
|
1627
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1628
|
+
type: { const: type },
|
|
1629
|
+
identity: ref("identity"),
|
|
1630
|
+
data: ref(DATA[type])
|
|
1631
|
+
});
|
|
1632
|
+
failure = () => obj(
|
|
1633
|
+
{
|
|
1634
|
+
apiVersion: { const: RESULT_SCHEMA_VERSION },
|
|
1635
|
+
error: str(),
|
|
1636
|
+
code: str({ pattern: "^ERR_[A-Z_]+$" }),
|
|
1637
|
+
suggestions: names(),
|
|
1638
|
+
identity: ref("error_identity")
|
|
1639
|
+
},
|
|
1640
|
+
["suggestions"]
|
|
1641
|
+
);
|
|
1642
|
+
S = { type: "string" };
|
|
1643
|
+
I = { type: "integer" };
|
|
1644
|
+
B = { type: "boolean" };
|
|
1645
|
+
list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
|
|
1646
|
+
shape = (properties, required) => ({
|
|
1647
|
+
type: "object",
|
|
1648
|
+
properties,
|
|
1649
|
+
...required === void 0 ? {} : { required }
|
|
1650
|
+
});
|
|
1651
|
+
strings = list(S);
|
|
1652
|
+
SUMMARIES = {
|
|
1653
|
+
check: shape(
|
|
1654
|
+
{
|
|
1655
|
+
evaluation_status: words2(
|
|
1656
|
+
"evaluated_clean",
|
|
1657
|
+
"evaluated_with_findings",
|
|
1658
|
+
"partially_evaluated",
|
|
1659
|
+
"not_evaluated",
|
|
1660
|
+
"no_files_evaluated"
|
|
1661
|
+
),
|
|
1662
|
+
findings: list(
|
|
1663
|
+
shape({
|
|
1664
|
+
code: S,
|
|
1665
|
+
severity: words2("error", "warning"),
|
|
1666
|
+
file: S,
|
|
1667
|
+
line: I,
|
|
1668
|
+
column: I,
|
|
1669
|
+
message: S
|
|
1670
|
+
})
|
|
1671
|
+
),
|
|
1672
|
+
summary: shape({ errors: I, warnings: I }),
|
|
1673
|
+
coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
|
|
1674
|
+
limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
|
|
1675
|
+
skipped_inputs: list(),
|
|
1676
|
+
scope: { type: "object" },
|
|
1677
|
+
parser: { type: "object" }
|
|
1678
|
+
},
|
|
1679
|
+
["evaluation_status", "findings", "summary", "coverage", "limitations"]
|
|
1680
|
+
),
|
|
1681
|
+
search: shape(
|
|
1682
|
+
{
|
|
1683
|
+
matchCount: I,
|
|
1684
|
+
results: list(
|
|
1685
|
+
shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
|
|
1686
|
+
)
|
|
1687
|
+
},
|
|
1688
|
+
["results"]
|
|
1689
|
+
),
|
|
1690
|
+
"component.detail": shape(
|
|
1691
|
+
{
|
|
1692
|
+
name: S,
|
|
1693
|
+
description: S,
|
|
1694
|
+
use_for: strings,
|
|
1695
|
+
not_for: list(),
|
|
1696
|
+
part_of: S,
|
|
1697
|
+
parts: strings,
|
|
1698
|
+
props: list(
|
|
1699
|
+
shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
|
|
1700
|
+
),
|
|
1701
|
+
slots: { type: "object" },
|
|
1702
|
+
composable_with: strings,
|
|
1703
|
+
never_inside: strings,
|
|
1704
|
+
allowed_children: strings,
|
|
1705
|
+
example: S,
|
|
1706
|
+
dense: B
|
|
1707
|
+
},
|
|
1708
|
+
["name", "props", "example", "dense"]
|
|
1709
|
+
),
|
|
1710
|
+
"component.list": shape({ components: list() }, ["components"]),
|
|
1711
|
+
"component.batch": shape({ components: list() }, ["components"]),
|
|
1712
|
+
instructions: shape(
|
|
1713
|
+
{
|
|
1714
|
+
rules: list(),
|
|
1715
|
+
components: list(),
|
|
1716
|
+
recipes: list()
|
|
1717
|
+
},
|
|
1718
|
+
["rules", "components", "recipes"]
|
|
1719
|
+
),
|
|
1720
|
+
init: shape({ files: list() }),
|
|
1721
|
+
doctor: shape({ operations: { type: "object" }, checks: list() }),
|
|
1722
|
+
add: shape({ files: list(), setup: { type: "object" } }),
|
|
1723
|
+
list: shape({ entries: list() })
|
|
1724
|
+
};
|
|
1725
|
+
RESULT_SCHEMAS = Object.freeze(
|
|
1726
|
+
Object.fromEntries(
|
|
1727
|
+
[...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
|
|
1728
|
+
)
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
// src/contract.ts
|
|
1734
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1735
|
+
import { dirname, join as join2 } from "path";
|
|
1736
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1737
|
+
function loadContract() {
|
|
1738
|
+
const here = dirname(fileURLToPath3(import.meta.url));
|
|
1739
|
+
const candidates = [join2(here, "contract.json"), join2(here, "../generated/contract.json")];
|
|
1740
|
+
const found = candidates.find((candidate) => existsSync2(candidate));
|
|
1741
|
+
if (found === void 0) {
|
|
1742
|
+
throw new Error(
|
|
1743
|
+
`contract.json is missing next to ${here}. Install @oprt/cli again: the contract ships beside the executable, and the executable cannot be copied without it.`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
return JSON.parse(readFileSync2(found, "utf8"));
|
|
1747
|
+
}
|
|
1748
|
+
var init_contract = __esm({
|
|
1749
|
+
"src/contract.ts"() {
|
|
1750
|
+
"use strict";
|
|
1751
|
+
init_esm_shims();
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// src/installer.ts
|
|
1756
|
+
import {
|
|
1757
|
+
constants,
|
|
1758
|
+
closeSync,
|
|
1759
|
+
lstatSync,
|
|
1760
|
+
mkdirSync,
|
|
1761
|
+
openSync,
|
|
1762
|
+
readFileSync as readFileSync3,
|
|
1763
|
+
realpathSync,
|
|
1764
|
+
writeFileSync
|
|
1765
|
+
} from "fs";
|
|
1766
|
+
import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
1767
|
+
function resolveEntries(names2, registry2) {
|
|
1768
|
+
if (!names2.length) throw new CliError("add requires one or more names.", 2);
|
|
1769
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1770
|
+
for (const name of names2) {
|
|
1771
|
+
const normalized = name.toLowerCase();
|
|
1772
|
+
const entry = registry2.entries.find(
|
|
1773
|
+
(candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
|
|
1774
|
+
);
|
|
1775
|
+
if (!entry)
|
|
1776
|
+
throw new CliError(`Unknown component or recipe ${JSON.stringify(name)}. Run oprt list.`, 2);
|
|
1777
|
+
selected.set(entry.name, entry);
|
|
1778
|
+
}
|
|
1779
|
+
return [...selected.values()];
|
|
1780
|
+
}
|
|
1781
|
+
function validateRelative(path2) {
|
|
1782
|
+
if (!path2 || isAbsolute(path2) || /[\\:\0]/.test(path2) || path2.split("/").includes(".."))
|
|
1783
|
+
throw new CliError(
|
|
1784
|
+
`Unsafe path ${JSON.stringify(path2)}: use a nonempty relative path without traversal.`,
|
|
1785
|
+
2
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function stat(path2) {
|
|
1789
|
+
try {
|
|
1790
|
+
return lstatSync(path2);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error.code === "ENOENT") return void 0;
|
|
1793
|
+
throw error;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function inspectPath(root, destination, directory = false) {
|
|
1797
|
+
const path2 = relative2(root, destination);
|
|
1798
|
+
if (path2 === ".." || path2.startsWith(`..${sep2}`) || isAbsolute(path2))
|
|
1799
|
+
throw new CliError("Output path escapes the working directory.");
|
|
1800
|
+
let cursor = root;
|
|
1801
|
+
const segments = path2 ? path2.split(sep2) : [];
|
|
1802
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1803
|
+
cursor = resolve2(cursor, segments[i]);
|
|
1804
|
+
const info = stat(cursor);
|
|
1805
|
+
if (!info) continue;
|
|
1806
|
+
if (info.isSymbolicLink()) throw new CliError(`Refusing symlink in output path: ${cursor}`);
|
|
1807
|
+
const needsDirectory = i < segments.length - 1 || directory;
|
|
1808
|
+
if (needsDirectory && !info.isDirectory())
|
|
1809
|
+
throw new CliError(`Output ancestor is not a directory: ${cursor}`);
|
|
1810
|
+
if (!needsDirectory && !info.isFile())
|
|
1811
|
+
throw new CliError(`Output target is not a regular file: ${cursor}`);
|
|
1812
|
+
}
|
|
1813
|
+
return stat(destination);
|
|
1814
|
+
}
|
|
1815
|
+
function installSources(options, context) {
|
|
1816
|
+
const entries = resolveEntries(options.names, context.registry);
|
|
1817
|
+
const out = options.out ?? DEFAULT_OUT;
|
|
1818
|
+
validateRelative(out);
|
|
1819
|
+
const root = realpathSync(context.cwd);
|
|
1820
|
+
const outputDirectory = resolve2(root, out);
|
|
1821
|
+
inspectPath(root, outputDirectory, true);
|
|
1822
|
+
const paths = [...new Set(entries.flatMap((entry) => entry.files))].sort();
|
|
1823
|
+
const files = paths.map((path2) => {
|
|
1824
|
+
validateRelative(path2);
|
|
1825
|
+
const content = context.registry.files[path2];
|
|
1826
|
+
if (!Object.hasOwn(context.registry.files, path2) || typeof content !== "string")
|
|
1827
|
+
throw new CliError(`Registry is missing source ${path2}.`);
|
|
1828
|
+
const destination = resolve2(outputDirectory, path2);
|
|
1829
|
+
const info = inspectPath(root, destination);
|
|
1830
|
+
if (!info) return { path: path2, destination, action: "create" };
|
|
1831
|
+
if (readFileSync3(destination, "utf8") === content) return { path: path2, destination, action: "skip" };
|
|
1832
|
+
if (!options.overwrite)
|
|
1833
|
+
throw new CliError(
|
|
1834
|
+
`Refusing to overwrite modified file ${destination}. Use --overwrite to replace it explicitly.`
|
|
854
1835
|
);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1836
|
+
return { path: path2, destination, action: "overwrite" };
|
|
1837
|
+
});
|
|
1838
|
+
const dependencies = [...new Set(entries.flatMap((entry) => entry.dependencies))].sort();
|
|
1839
|
+
const runtime = new Set(
|
|
1840
|
+
entries.flatMap(
|
|
1841
|
+
(entry) => entry.dependencies.filter((name) => !(entry.devDependencies ?? []).includes(name))
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
const result = {
|
|
1845
|
+
outputDirectory,
|
|
1846
|
+
entries: entries.map((entry) => entry.name),
|
|
1847
|
+
dependencies,
|
|
1848
|
+
devDependencies: dependencies.filter(
|
|
1849
|
+
(name) => !runtime.has(name) && entries.some((entry) => (entry.devDependencies ?? []).includes(name))
|
|
1850
|
+
),
|
|
1851
|
+
ranges: Object.fromEntries(
|
|
1852
|
+
dependencies.flatMap((name) => {
|
|
1853
|
+
const range = context.registry.ranges?.[name];
|
|
1854
|
+
return range === void 0 ? [] : [[name, range]];
|
|
1855
|
+
})
|
|
1856
|
+
),
|
|
1857
|
+
files,
|
|
1858
|
+
dryRun: options.dryRun ?? false
|
|
867
1859
|
};
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
function findTypeScript(cwd) {
|
|
879
|
-
for (const from of [join2(cwd, "package.json"), fileURLToPath3(import.meta.url)]) {
|
|
1860
|
+
if (result.dryRun) return result;
|
|
1861
|
+
for (const file of files) {
|
|
1862
|
+
if (file.action === "skip") continue;
|
|
1863
|
+
inspectPath(root, file.destination);
|
|
1864
|
+
const parent = dirname2(file.destination);
|
|
1865
|
+
mkdirSync(parent, { recursive: true });
|
|
1866
|
+
inspectPath(root, parent, true);
|
|
1867
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (file.action === "create" ? constants.O_EXCL : constants.O_TRUNC);
|
|
1868
|
+
const descriptor = openSync(file.destination, flags, 420);
|
|
880
1869
|
try {
|
|
881
|
-
|
|
882
|
-
}
|
|
1870
|
+
writeFileSync(descriptor, context.registry.files[file.path]);
|
|
1871
|
+
} finally {
|
|
1872
|
+
closeSync(descriptor);
|
|
883
1873
|
}
|
|
884
1874
|
}
|
|
885
|
-
return
|
|
886
|
-
}
|
|
887
|
-
function checkPaths(paths, context) {
|
|
888
|
-
const root = resolve2(context.cwd);
|
|
889
|
-
const files = [];
|
|
890
|
-
for (const path2 of paths) {
|
|
891
|
-
const absolute = resolve2(root, path2);
|
|
892
|
-
if (!existsSync2(absolute))
|
|
893
|
-
throw new CliError(`No such file or folder: ${path2}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
894
|
-
files.push(...sources(absolute));
|
|
895
|
-
}
|
|
896
|
-
const ts = context.typescript === void 0 ? findTypeScript(root) : context.typescript;
|
|
897
|
-
if (ts === null)
|
|
898
|
-
throw new CliError(
|
|
899
|
-
"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.",
|
|
900
|
-
1,
|
|
901
|
-
{ code: "ERR_TYPESCRIPT_MISSING" }
|
|
902
|
-
);
|
|
903
|
-
const findings = [...new Set(files)].flatMap(
|
|
904
|
-
(file) => checkSource(
|
|
905
|
-
ts,
|
|
906
|
-
context.contract,
|
|
907
|
-
relative2(root, file).split(sep2).join("/"),
|
|
908
|
-
readFileSync3(file, "utf8")
|
|
909
|
-
)
|
|
910
|
-
);
|
|
911
|
-
return {
|
|
912
|
-
files: new Set(files).size,
|
|
913
|
-
findings,
|
|
914
|
-
summary: {
|
|
915
|
-
errors: findings.filter(({ severity }) => severity === "error").length,
|
|
916
|
-
warnings: findings.filter(({ severity }) => severity === "warning").length
|
|
917
|
-
}
|
|
918
|
-
};
|
|
1875
|
+
return result;
|
|
919
1876
|
}
|
|
920
|
-
var
|
|
921
|
-
|
|
922
|
-
"src/check.ts"() {
|
|
1877
|
+
var init_installer = __esm({
|
|
1878
|
+
"src/installer.ts"() {
|
|
923
1879
|
"use strict";
|
|
924
1880
|
init_esm_shims();
|
|
925
|
-
|
|
1881
|
+
init_args();
|
|
926
1882
|
init_types();
|
|
927
|
-
PACKAGE = "@oprt/react";
|
|
928
|
-
NATIVE = {
|
|
929
|
-
button: "Button",
|
|
930
|
-
input: "Input, or the control for its type (Checkbox, RadioGroup, NumberInput, Slider, DatePicker, FileUpload)",
|
|
931
|
-
select: "Select",
|
|
932
|
-
textarea: "Textarea",
|
|
933
|
-
table: "DataTable or DataGrid",
|
|
934
|
-
dialog: "Dialog",
|
|
935
|
-
progress: "Progress",
|
|
936
|
-
hr: "Divider"
|
|
937
|
-
};
|
|
938
|
-
NATIVE_ATTRIBUTES = new Set(
|
|
939
|
-
`key ref children className style id title role tabIndex hidden lang dir slot draggable spellCheck
|
|
940
|
-
translate inert accessKey autoCapitalize contentEditable enterKeyHint inputMode
|
|
941
|
-
dangerouslySetInnerHTML suppressHydrationWarning suppressContentEditableWarning htmlFor type name
|
|
942
|
-
value defaultValue checked defaultChecked disabled readOnly required placeholder autoComplete
|
|
943
|
-
autoFocus min max step minLength maxLength pattern multiple accept form formAction href target rel
|
|
944
|
-
download src alt width height loading rows cols wrap open colSpan rowSpan scope headers label list
|
|
945
|
-
size method action encType noValidate popover popoverTarget popoverTargetAction asChild`.split(
|
|
946
|
-
/\s+/
|
|
947
|
-
)
|
|
948
|
-
);
|
|
949
|
-
nativeAttribute = (name) => NATIVE_ATTRIBUTES.has(name) || /^(aria-|data-)/.test(name) || /^on[A-Z]/.test(name);
|
|
950
|
-
RAW_COLOR_CLASS = /\[(?:#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\()/;
|
|
951
|
-
RAW_COLOR_VALUE = /^#[0-9a-fA-F]{3,8}$|^(?:rgb|hsl)a?\(/;
|
|
952
|
-
ARBITRARY_SIZE = /(?:^|[\s:])-?[a-z][a-z0-9-]*-\[-?\d*\.?\d+px\]/;
|
|
953
|
-
ROUNDED_CORNER = /(?:^|[\s:])rounded(?!-none(?:\s|$))(?:-[^\s]+)?(?=\s|$)/;
|
|
954
|
-
SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
955
|
-
READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
956
|
-
NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
957
1883
|
}
|
|
958
1884
|
});
|
|
959
1885
|
|
|
@@ -1048,26 +1974,30 @@ function installedVersion(cwd, name) {
|
|
|
1048
1974
|
if (dirname3(folder) === folder) return void 0;
|
|
1049
1975
|
}
|
|
1050
1976
|
}
|
|
1051
|
-
function doctor({
|
|
1977
|
+
function doctor({
|
|
1978
|
+
cwd,
|
|
1979
|
+
contract: contract2,
|
|
1980
|
+
parser = probeParser()
|
|
1981
|
+
}) {
|
|
1052
1982
|
const checks = [];
|
|
1053
1983
|
for (const name of PACKAGES) {
|
|
1054
|
-
const
|
|
1984
|
+
const version2 = installedVersion(cwd, name);
|
|
1055
1985
|
const id = `package:${name}`;
|
|
1056
|
-
if (
|
|
1986
|
+
if (version2 === void 0)
|
|
1057
1987
|
checks.push({
|
|
1058
1988
|
id,
|
|
1059
1989
|
status: "warn",
|
|
1060
1990
|
message: `${name} is not installed.`,
|
|
1061
1991
|
fix: `Install ${name}, or copy the sources you need with oprt add.`
|
|
1062
1992
|
});
|
|
1063
|
-
else if (minor(
|
|
1993
|
+
else if (minor(version2) !== minor(contract2.version))
|
|
1064
1994
|
checks.push({
|
|
1065
1995
|
id,
|
|
1066
1996
|
status: "warn",
|
|
1067
|
-
message: `${name} is ${
|
|
1997
|
+
message: `${name} is ${version2}; this installer describes ${contract2.version}.`,
|
|
1068
1998
|
fix: "Install matching versions of the OPERATOR packages and @oprt/cli."
|
|
1069
1999
|
});
|
|
1070
|
-
else checks.push({ id, status: "ok", message: `${name} ${
|
|
2000
|
+
else checks.push({ id, status: "ok", message: `${name} ${version2} is installed.` });
|
|
1071
2001
|
}
|
|
1072
2002
|
const written = INSTRUCTION_FILES.map((path2) => {
|
|
1073
2003
|
const file = join3(cwd, path2);
|
|
@@ -1075,8 +2005,8 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1075
2005
|
path: path2,
|
|
1076
2006
|
version: existsSync3(file) ? blockVersion(readFileSync4(file, "utf8")) : void 0
|
|
1077
2007
|
};
|
|
1078
|
-
}).filter(({ version }) =>
|
|
1079
|
-
const stale = written.find(({ version }) =>
|
|
2008
|
+
}).filter(({ version: version2 }) => version2 !== void 0);
|
|
2009
|
+
const stale = written.find(({ version: version2 }) => version2 !== contract2.version);
|
|
1080
2010
|
if (written.length === 0)
|
|
1081
2011
|
checks.push({
|
|
1082
2012
|
id: "agent-instructions",
|
|
@@ -1097,8 +2027,48 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1097
2027
|
status: "ok",
|
|
1098
2028
|
message: `${written.map(({ path: path2 }) => path2).join(", ")} carries the OPERATOR block (${contract2.version}).`
|
|
1099
2029
|
});
|
|
2030
|
+
let checkCode;
|
|
2031
|
+
if (parser.ready) {
|
|
2032
|
+
const { name, version: version2 } = parser.parser;
|
|
2033
|
+
checkCode = {
|
|
2034
|
+
status: "ready",
|
|
2035
|
+
message: `${name} ${version2}, bundled with oprt: nothing to install.`,
|
|
2036
|
+
parser: parser.parser
|
|
2037
|
+
};
|
|
2038
|
+
checks.push({
|
|
2039
|
+
id: "parser",
|
|
2040
|
+
status: "ok",
|
|
2041
|
+
message: `Code checking reads code with ${name} ${version2}, which ships inside oprt.`
|
|
2042
|
+
});
|
|
2043
|
+
} else {
|
|
2044
|
+
const fix = "Install @oprt/cli again, then run oprt doctor again. Lookups work without it.";
|
|
2045
|
+
checkCode = {
|
|
2046
|
+
status: "blocked",
|
|
2047
|
+
message: `${parser.reason}: ${parser.detail}`,
|
|
2048
|
+
reason: parser.reason,
|
|
2049
|
+
fix
|
|
2050
|
+
};
|
|
2051
|
+
checks.push({
|
|
2052
|
+
id: "parser",
|
|
2053
|
+
status: "fail",
|
|
2054
|
+
message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
|
|
2055
|
+
fix
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1100
2058
|
const count = (status) => checks.filter((check) => check.status === status).length;
|
|
1101
|
-
return {
|
|
2059
|
+
return {
|
|
2060
|
+
consumer_directory: cwd,
|
|
2061
|
+
operations: {
|
|
2062
|
+
lookup: {
|
|
2063
|
+
status: "ready",
|
|
2064
|
+
message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
|
|
2065
|
+
contract_version: contract2.version
|
|
2066
|
+
},
|
|
2067
|
+
check_code: checkCode
|
|
2068
|
+
},
|
|
2069
|
+
checks,
|
|
2070
|
+
summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
|
|
2071
|
+
};
|
|
1102
2072
|
}
|
|
1103
2073
|
var PACKAGES, INSTRUCTION_FILES, minor;
|
|
1104
2074
|
var init_doctor = __esm({
|
|
@@ -1106,6 +2076,7 @@ var init_doctor = __esm({
|
|
|
1106
2076
|
"use strict";
|
|
1107
2077
|
init_esm_shims();
|
|
1108
2078
|
init_agent_block();
|
|
2079
|
+
init_parser();
|
|
1109
2080
|
PACKAGES = ["@oprt/react", "@oprt/tokens", "@oprt/tailwind"];
|
|
1110
2081
|
INSTRUCTION_FILES = [
|
|
1111
2082
|
"AGENTS.md",
|
|
@@ -1113,7 +2084,7 @@ var init_doctor = __esm({
|
|
|
1113
2084
|
".claude/CLAUDE.md",
|
|
1114
2085
|
".cursor/rules/operator.mdc"
|
|
1115
2086
|
];
|
|
1116
|
-
minor = (
|
|
2087
|
+
minor = (version2) => version2.split(".").slice(0, 2).join(".");
|
|
1117
2088
|
}
|
|
1118
2089
|
});
|
|
1119
2090
|
|
|
@@ -1182,7 +2153,72 @@ var init_init = __esm({
|
|
|
1182
2153
|
}
|
|
1183
2154
|
});
|
|
1184
2155
|
|
|
2156
|
+
// src/setup.ts
|
|
2157
|
+
function setupFor(dependencies, contract2) {
|
|
2158
|
+
const { setup } = contract2.guidance;
|
|
2159
|
+
const scansPackage = dependencies.includes(setup.package_source.when);
|
|
2160
|
+
return {
|
|
2161
|
+
tailwind: setup.tailwind,
|
|
2162
|
+
packages: setup.packages.map(({ name, range, dev, why }) => ({
|
|
2163
|
+
name,
|
|
2164
|
+
range: range ?? "latest",
|
|
2165
|
+
dev,
|
|
2166
|
+
why
|
|
2167
|
+
})),
|
|
2168
|
+
stylesheet: [
|
|
2169
|
+
...setup.stylesheet,
|
|
2170
|
+
...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
|
|
2171
|
+
],
|
|
2172
|
+
root_import: { line: setup.root_import, note: setup.root_import_note },
|
|
2173
|
+
not_needed: setup.not_needed
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
function renderSetup(setup, copiedPackageImports) {
|
|
2177
|
+
const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
|
|
2178
|
+
const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
|
|
2179
|
+
const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
|
|
2180
|
+
return [
|
|
2181
|
+
`Setup, once per project: ${setup.tailwind}`,
|
|
2182
|
+
` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
|
|
2183
|
+
" In the stylesheet that imports tailwindcss, after the import:",
|
|
2184
|
+
...setup.stylesheet.map(({ line }) => ` ${line}`),
|
|
2185
|
+
...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
|
|
2186
|
+
" (source copied into the project is scanned by Tailwind 4 without an @source line)"
|
|
2187
|
+
],
|
|
2188
|
+
` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
|
|
2189
|
+
...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
|
|
2190
|
+
];
|
|
2191
|
+
}
|
|
2192
|
+
var init_setup = __esm({
|
|
2193
|
+
"src/setup.ts"() {
|
|
2194
|
+
"use strict";
|
|
2195
|
+
init_esm_shims();
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
|
|
1185
2199
|
// src/agent-commands.ts
|
|
2200
|
+
function renderInstall(result, setup) {
|
|
2201
|
+
const lines = [
|
|
2202
|
+
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
|
|
2203
|
+
`Output: ${result.outputDirectory}`,
|
|
2204
|
+
...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
|
|
2205
|
+
];
|
|
2206
|
+
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
2207
|
+
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
2208
|
+
lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
|
|
2209
|
+
if (result.devDependencies.length > 0)
|
|
2210
|
+
lines.push(
|
|
2211
|
+
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
|
|
2212
|
+
);
|
|
2213
|
+
lines.push(
|
|
2214
|
+
"",
|
|
2215
|
+
...renderSetup(setup, setup.stylesheet.length > 1),
|
|
2216
|
+
"",
|
|
2217
|
+
"Install what is missing yourself. No package configuration or stylesheet was changed."
|
|
2218
|
+
);
|
|
2219
|
+
return `${lines.join("\n")}
|
|
2220
|
+
`;
|
|
2221
|
+
}
|
|
1186
2222
|
function runAgentCommand(command, context) {
|
|
1187
2223
|
const { contract: contract2, registry: registry2, cwd } = context;
|
|
1188
2224
|
switch (command.command) {
|
|
@@ -1194,14 +2230,15 @@ function runAgentCommand(command, context) {
|
|
|
1194
2230
|
({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
|
|
1195
2231
|
`
|
|
1196
2232
|
).join(""),
|
|
1197
|
-
json: envelope("component.list", { components }),
|
|
2233
|
+
json: envelope("component.list", { components }, contract2),
|
|
1198
2234
|
exitCode: 0
|
|
1199
2235
|
};
|
|
1200
2236
|
}
|
|
1201
2237
|
const details = command.names.map((name) => componentDetail(contract2, name));
|
|
2238
|
+
const data = details.map((detail) => componentData(detail, command.dense));
|
|
1202
2239
|
return {
|
|
1203
2240
|
text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
|
|
1204
|
-
json:
|
|
2241
|
+
json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
|
|
1205
2242
|
exitCode: 0
|
|
1206
2243
|
};
|
|
1207
2244
|
}
|
|
@@ -1214,7 +2251,11 @@ function runAgentCommand(command, context) {
|
|
|
1214
2251
|
${next}
|
|
1215
2252
|
`
|
|
1216
2253
|
).join(""),
|
|
1217
|
-
json: envelope(
|
|
2254
|
+
json: envelope(
|
|
2255
|
+
"search",
|
|
2256
|
+
{ query: command.query, limit: command.limit, matchCount: results.length, results },
|
|
2257
|
+
contract2
|
|
2258
|
+
),
|
|
1218
2259
|
exitCode: 0
|
|
1219
2260
|
};
|
|
1220
2261
|
}
|
|
@@ -1225,55 +2266,195 @@ function runAgentCommand(command, context) {
|
|
|
1225
2266
|
...result.files.map(({ path: path2, action }) => ` ${action.padEnd(9)} ${path2}`),
|
|
1226
2267
|
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."
|
|
1227
2268
|
];
|
|
1228
|
-
return {
|
|
1229
|
-
|
|
2269
|
+
return {
|
|
2270
|
+
text: `${lines.join("\n")}
|
|
2271
|
+
`,
|
|
2272
|
+
json: envelope("init", result, contract2),
|
|
2273
|
+
exitCode: 0
|
|
2274
|
+
};
|
|
1230
2275
|
}
|
|
1231
2276
|
case "check": {
|
|
1232
2277
|
const report = checkPaths(command.paths, { cwd, contract: contract2 });
|
|
1233
|
-
const { errors, warnings } = report.summary;
|
|
1234
|
-
const lines = report.findings.map(
|
|
1235
|
-
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
1236
|
-
);
|
|
1237
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1238
|
-
lines.push(
|
|
1239
|
-
`${count(errors, "error")}, ${count(warnings, "warning")} in ${count(report.files, "file")}.`
|
|
1240
|
-
);
|
|
1241
2278
|
return {
|
|
1242
|
-
text:
|
|
1243
|
-
|
|
1244
|
-
json: envelope("check", report),
|
|
2279
|
+
text: renderCheckText(report),
|
|
2280
|
+
json: envelope("check", report, contract2),
|
|
1245
2281
|
// Warnings fail the run only when asked to: they are house rules, or may be native attributes.
|
|
1246
|
-
exitCode:
|
|
2282
|
+
exitCode: checkExitCode(report, { strict: command.strict })
|
|
1247
2283
|
};
|
|
1248
2284
|
}
|
|
1249
2285
|
case "doctor": {
|
|
1250
|
-
const report = doctor({
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
...
|
|
1254
|
-
|
|
2286
|
+
const report = doctor({
|
|
2287
|
+
cwd,
|
|
2288
|
+
contract: contract2,
|
|
2289
|
+
...context.parser ? { parser: context.parser } : {}
|
|
2290
|
+
});
|
|
2291
|
+
const { lookup, check_code: checking } = report.operations;
|
|
2292
|
+
const lines = [
|
|
2293
|
+
"Readiness",
|
|
2294
|
+
` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
|
|
2295
|
+
` code checking ${checking.status.padEnd(8)} ${checking.message}`,
|
|
2296
|
+
...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
|
|
2297
|
+
"",
|
|
2298
|
+
"Setup"
|
|
2299
|
+
];
|
|
2300
|
+
lines.push(
|
|
2301
|
+
...report.checks.flatMap(({ status, message, fix }) => [
|
|
2302
|
+
`[${status}] ${message}`,
|
|
2303
|
+
...fix === void 0 ? [] : [` fix: ${fix}`]
|
|
2304
|
+
])
|
|
2305
|
+
);
|
|
1255
2306
|
lines.push(
|
|
1256
2307
|
`${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
|
|
1257
2308
|
);
|
|
1258
2309
|
return {
|
|
1259
2310
|
text: `${lines.join("\n")}
|
|
1260
2311
|
`,
|
|
1261
|
-
json: envelope("doctor", report),
|
|
2312
|
+
json: envelope("doctor", report, contract2),
|
|
1262
2313
|
exitCode: report.summary.fail > 0 ? 1 : 0
|
|
1263
2314
|
};
|
|
1264
2315
|
}
|
|
2316
|
+
case "list": {
|
|
2317
|
+
const lines = [`OPERATOR ${VERSION}`, ""];
|
|
2318
|
+
for (const kind of ["component", "recipe"]) {
|
|
2319
|
+
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
2320
|
+
lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
|
|
2321
|
+
for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
|
|
2322
|
+
lines.push("");
|
|
2323
|
+
}
|
|
2324
|
+
return {
|
|
2325
|
+
text: `${lines.join("\n")}
|
|
2326
|
+
`,
|
|
2327
|
+
json: envelope(
|
|
2328
|
+
"list",
|
|
2329
|
+
{ version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
2330
|
+
contract2
|
|
2331
|
+
),
|
|
2332
|
+
exitCode: 0
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
case "add": {
|
|
2336
|
+
const result = installSources(command, { cwd, registry: registry2 });
|
|
2337
|
+
const setup = setupFor(result.dependencies, contract2);
|
|
2338
|
+
return {
|
|
2339
|
+
text: renderInstall(result, setup),
|
|
2340
|
+
json: envelope("add", { ...result, setup }, contract2),
|
|
2341
|
+
exitCode: 0
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
1265
2344
|
}
|
|
1266
2345
|
}
|
|
1267
|
-
var envelope;
|
|
1268
2346
|
var init_agent_commands = __esm({
|
|
1269
2347
|
"src/agent-commands.ts"() {
|
|
1270
2348
|
"use strict";
|
|
1271
2349
|
init_esm_shims();
|
|
1272
2350
|
init_check();
|
|
1273
2351
|
init_doctor();
|
|
2352
|
+
init_identity();
|
|
1274
2353
|
init_init();
|
|
2354
|
+
init_installer();
|
|
1275
2355
|
init_lookup();
|
|
1276
|
-
|
|
2356
|
+
init_results();
|
|
2357
|
+
init_setup();
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
// src/validate.ts
|
|
2362
|
+
function describeValue(value) {
|
|
2363
|
+
if (typeof value === "string")
|
|
2364
|
+
return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
|
|
2365
|
+
if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
|
|
2366
|
+
if (value === null || typeof value === "boolean" || value === void 0) return String(value);
|
|
2367
|
+
return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
|
|
2368
|
+
}
|
|
2369
|
+
function expectation(schema) {
|
|
2370
|
+
switch (schema.type) {
|
|
2371
|
+
case "boolean":
|
|
2372
|
+
return "true or false";
|
|
2373
|
+
case "integer":
|
|
2374
|
+
case "number": {
|
|
2375
|
+
const noun = schema.type === "integer" ? "a whole number" : "a number";
|
|
2376
|
+
if (schema.minimum !== void 0 && schema.maximum !== void 0)
|
|
2377
|
+
return `${noun} from ${schema.minimum} to ${schema.maximum}`;
|
|
2378
|
+
if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
|
|
2379
|
+
if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
|
|
2380
|
+
return noun;
|
|
2381
|
+
}
|
|
2382
|
+
case "string": {
|
|
2383
|
+
if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
|
|
2384
|
+
const { minLength, maxLength } = schema;
|
|
2385
|
+
if (minLength !== void 0 && maxLength !== void 0)
|
|
2386
|
+
return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
|
|
2387
|
+
if (maxLength !== void 0)
|
|
2388
|
+
return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
|
|
2389
|
+
return "a string";
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
function accepts(schema, value) {
|
|
2394
|
+
switch (schema.type) {
|
|
2395
|
+
case "boolean":
|
|
2396
|
+
return typeof value === "boolean";
|
|
2397
|
+
case "string":
|
|
2398
|
+
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));
|
|
2399
|
+
case "integer":
|
|
2400
|
+
case "number":
|
|
2401
|
+
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);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
function validateArguments(tool, schema, args) {
|
|
2405
|
+
for (const [field, property] of Object.entries(schema.properties))
|
|
2406
|
+
for (const key of Object.keys(property))
|
|
2407
|
+
if (!KNOWN.has(key))
|
|
2408
|
+
throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
|
|
2409
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
2410
|
+
return `${tool}: arguments must be an object.`;
|
|
2411
|
+
const given = args;
|
|
2412
|
+
const required = schema.required ?? [];
|
|
2413
|
+
const blank = (value) => typeof value === "string" && value.trim() === "";
|
|
2414
|
+
const missing = [];
|
|
2415
|
+
const wrong = [];
|
|
2416
|
+
const unknown = [];
|
|
2417
|
+
for (const field of required)
|
|
2418
|
+
if (given[field] === void 0 || blank(given[field])) missing.push(field);
|
|
2419
|
+
for (const [field, value] of Object.entries(given)) {
|
|
2420
|
+
const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
|
|
2421
|
+
if (property === void 0) unknown.push(field);
|
|
2422
|
+
else if (value !== void 0 && !(required.includes(field) && blank(value))) {
|
|
2423
|
+
if (!accepts(property, value))
|
|
2424
|
+
wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
const reject = schema.additionalProperties === false ? unknown : [];
|
|
2428
|
+
const names2 = Object.keys(schema.properties);
|
|
2429
|
+
if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
|
|
2430
|
+
return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
|
|
2431
|
+
const fragments = [
|
|
2432
|
+
...missing.map(
|
|
2433
|
+
(field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
|
|
2434
|
+
),
|
|
2435
|
+
...wrong,
|
|
2436
|
+
...reject.map(
|
|
2437
|
+
(field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
|
|
2438
|
+
)
|
|
2439
|
+
];
|
|
2440
|
+
return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
|
|
2441
|
+
}
|
|
2442
|
+
var KNOWN, typeName;
|
|
2443
|
+
var init_validate = __esm({
|
|
2444
|
+
"src/validate.ts"() {
|
|
2445
|
+
"use strict";
|
|
2446
|
+
init_esm_shims();
|
|
2447
|
+
KNOWN = /* @__PURE__ */ new Set([
|
|
2448
|
+
"type",
|
|
2449
|
+
"description",
|
|
2450
|
+
"default",
|
|
2451
|
+
"enum",
|
|
2452
|
+
"minimum",
|
|
2453
|
+
"maximum",
|
|
2454
|
+
"minLength",
|
|
2455
|
+
"maxLength"
|
|
2456
|
+
]);
|
|
2457
|
+
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;
|
|
1277
2458
|
}
|
|
1278
2459
|
});
|
|
1279
2460
|
|
|
@@ -1281,75 +2462,82 @@ var init_agent_commands = __esm({
|
|
|
1281
2462
|
import { createInterface } from "readline";
|
|
1282
2463
|
function runTool(name, args, context) {
|
|
1283
2464
|
const { contract: contract2, registry: registry2 } = context;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
return value;
|
|
1289
|
-
};
|
|
2465
|
+
const definition = TOOLS.find((tool) => tool.name === name);
|
|
2466
|
+
if (definition === void 0) throw new UnknownTool(name);
|
|
2467
|
+
const problem = validateArguments(name, definition.inputSchema, args);
|
|
2468
|
+
if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
|
|
1290
2469
|
switch (name) {
|
|
1291
2470
|
case "search": {
|
|
1292
|
-
const query =
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
query
|
|
1297
|
-
|
|
1298
|
-
);
|
|
1299
|
-
return results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
1300
|
-
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
2471
|
+
const query = args["query"];
|
|
2472
|
+
const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
|
|
2473
|
+
const results = search(contract2, registry2, query, limit);
|
|
2474
|
+
return {
|
|
2475
|
+
text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
2476
|
+
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
1301
2477
|
${next}`
|
|
1302
|
-
|
|
2478
|
+
).join("\n"),
|
|
2479
|
+
structured: envelope(
|
|
2480
|
+
"search",
|
|
2481
|
+
{ query, limit, matchCount: results.length, results },
|
|
2482
|
+
contract2
|
|
2483
|
+
)
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
case "get_component": {
|
|
2487
|
+
const dense = args["dense"] ?? true;
|
|
2488
|
+
const detail = componentDetail(contract2, args["name"]);
|
|
2489
|
+
return {
|
|
2490
|
+
text: renderComponent(detail, { dense }),
|
|
2491
|
+
structured: envelope("component.detail", componentData(detail, dense), contract2)
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
case "list_components": {
|
|
2495
|
+
const components = componentIndex(contract2);
|
|
2496
|
+
return {
|
|
2497
|
+
text: components.map(
|
|
2498
|
+
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
2499
|
+
).join("\n"),
|
|
2500
|
+
structured: envelope("component.list", { components }, contract2)
|
|
2501
|
+
};
|
|
1303
2502
|
}
|
|
1304
|
-
case "get_component":
|
|
1305
|
-
return renderComponent(componentDetail(contract2, required("name")), {
|
|
1306
|
-
dense: args.dense !== false
|
|
1307
|
-
});
|
|
1308
|
-
case "list_components":
|
|
1309
|
-
return componentIndex(contract2).map(
|
|
1310
|
-
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
1311
|
-
).join("\n");
|
|
1312
2503
|
case "get_instructions":
|
|
1313
|
-
return
|
|
2504
|
+
return {
|
|
2505
|
+
text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
|
|
2506
|
+
structured: envelope("instructions", instructionsData(contract2), contract2)
|
|
2507
|
+
};
|
|
1314
2508
|
case "check_code": {
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
const findings = checkSource(ts, contract2, "snippet.tsx", code);
|
|
1320
|
-
if (findings.length === 0) return "No findings.";
|
|
1321
|
-
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
1322
|
-
const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1323
|
-
return [
|
|
1324
|
-
...findings.map(
|
|
1325
|
-
({ line, column, severity, code: finding, message }) => `${line}:${column} ${severity} ${finding} ${message}`
|
|
1326
|
-
),
|
|
1327
|
-
`${count(errors, "error")}, ${count(findings.length - errors, "warning")}.`
|
|
1328
|
-
].join("\n");
|
|
2509
|
+
const report = buildCheckReport([
|
|
2510
|
+
checkSource(contract2, "snippet.tsx", args["code"])
|
|
2511
|
+
]);
|
|
2512
|
+
return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
|
|
1329
2513
|
}
|
|
1330
2514
|
default:
|
|
1331
2515
|
throw new UnknownTool(name);
|
|
1332
2516
|
}
|
|
1333
2517
|
}
|
|
1334
2518
|
function handleMessage(raw, context) {
|
|
1335
|
-
const
|
|
2519
|
+
const failure2 = (id2, code, message) => ({
|
|
1336
2520
|
jsonrpc: "2.0",
|
|
1337
2521
|
id: id2 ?? null,
|
|
1338
2522
|
error: { code, message }
|
|
1339
2523
|
});
|
|
1340
|
-
if (typeof raw !== "object" || raw === null) return
|
|
2524
|
+
if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
|
|
1341
2525
|
const { id, method, params } = raw;
|
|
1342
|
-
if (typeof method !== "string") return
|
|
2526
|
+
if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
|
|
1343
2527
|
if (id === void 0) return null;
|
|
1344
2528
|
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
1345
2529
|
switch (method) {
|
|
1346
2530
|
case "initialize": {
|
|
1347
2531
|
const asked = params?.protocolVersion;
|
|
2532
|
+
const identity = identityOf(context.contract);
|
|
1348
2533
|
return result({
|
|
1349
2534
|
protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
|
|
1350
2535
|
capabilities: { tools: {} },
|
|
1351
|
-
|
|
1352
|
-
|
|
2536
|
+
// The program's version, not the contract's: the contract has its own, in `_meta` and in
|
|
2537
|
+
// the identity of every result.
|
|
2538
|
+
serverInfo: { name: "oprt", version: VERSION },
|
|
2539
|
+
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.",
|
|
2540
|
+
_meta: { "dev.oprt/identity": identity }
|
|
1353
2541
|
});
|
|
1354
2542
|
}
|
|
1355
2543
|
case "ping":
|
|
@@ -1359,17 +2547,23 @@ function handleMessage(raw, context) {
|
|
|
1359
2547
|
case "tools/call": {
|
|
1360
2548
|
const { name, arguments: args } = params ?? {};
|
|
1361
2549
|
try {
|
|
1362
|
-
|
|
2550
|
+
const output = runTool(String(name), args ?? {}, context);
|
|
2551
|
+
return result({
|
|
2552
|
+
content: [text(output.text)],
|
|
2553
|
+
structuredContent: output.structured
|
|
2554
|
+
});
|
|
1363
2555
|
} catch (error) {
|
|
1364
|
-
if (error instanceof UnknownTool) return
|
|
2556
|
+
if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
|
|
2557
|
+
const failed = errorEnvelope(error, context.contract);
|
|
1365
2558
|
return result({
|
|
1366
|
-
content: [text(
|
|
2559
|
+
content: [text(failed.error)],
|
|
2560
|
+
structuredContent: failed,
|
|
1367
2561
|
isError: true
|
|
1368
2562
|
});
|
|
1369
2563
|
}
|
|
1370
2564
|
}
|
|
1371
2565
|
default:
|
|
1372
|
-
return
|
|
2566
|
+
return failure2(id, -32601, `Method not found: ${method}`);
|
|
1373
2567
|
}
|
|
1374
2568
|
}
|
|
1375
2569
|
function serve(input, output, context) {
|
|
@@ -1388,64 +2582,108 @@ function serve(input, output, context) {
|
|
|
1388
2582
|
});
|
|
1389
2583
|
return new Promise((done) => lines.on("close", done));
|
|
1390
2584
|
}
|
|
1391
|
-
var VERSIONS, text,
|
|
2585
|
+
var VERSIONS, text, LIMITS, ANSWERS, readOnly, define, TOOLS, UnknownTool;
|
|
1392
2586
|
var init_mcp = __esm({
|
|
1393
2587
|
"src/mcp.ts"() {
|
|
1394
2588
|
"use strict";
|
|
1395
2589
|
init_esm_shims();
|
|
1396
2590
|
init_agent_block();
|
|
1397
2591
|
init_check();
|
|
2592
|
+
init_identity();
|
|
1398
2593
|
init_lookup();
|
|
2594
|
+
init_results();
|
|
2595
|
+
init_types();
|
|
2596
|
+
init_validate();
|
|
1399
2597
|
VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1400
2598
|
text = (value) => ({ type: "text", text: value });
|
|
1401
|
-
|
|
2599
|
+
LIMITS = { query: 500, name: 100, code: 2e5 };
|
|
2600
|
+
ANSWERS = {
|
|
2601
|
+
search: "search",
|
|
2602
|
+
get_component: "component.detail",
|
|
2603
|
+
list_components: "component.list",
|
|
2604
|
+
get_instructions: "instructions",
|
|
2605
|
+
check_code: "check"
|
|
2606
|
+
};
|
|
2607
|
+
readOnly = { readOnlyHint: true };
|
|
2608
|
+
define = (tool) => ({
|
|
2609
|
+
...tool,
|
|
2610
|
+
outputSchema: outputSchemaFor(ANSWERS[tool.name]),
|
|
2611
|
+
annotations: readOnly
|
|
2612
|
+
});
|
|
1402
2613
|
TOOLS = [
|
|
1403
|
-
{
|
|
2614
|
+
define({
|
|
1404
2615
|
name: "search",
|
|
1405
|
-
description: "Find the OPERATOR component or recipe
|
|
2616
|
+
description: "Find the OPERATOR component or recipe for a need, such as 'show a status' or 'table with selection'. Call before writing interface code.",
|
|
1406
2617
|
inputSchema: {
|
|
1407
2618
|
type: "object",
|
|
1408
2619
|
properties: {
|
|
1409
|
-
query:
|
|
1410
|
-
|
|
2620
|
+
query: {
|
|
2621
|
+
type: "string",
|
|
2622
|
+
minLength: 1,
|
|
2623
|
+
maxLength: LIMITS.query,
|
|
2624
|
+
description: "The need, in plain words."
|
|
2625
|
+
},
|
|
2626
|
+
limit: {
|
|
2627
|
+
type: "integer",
|
|
2628
|
+
minimum: 1,
|
|
2629
|
+
maximum: SEARCH_LIMIT_MAX,
|
|
2630
|
+
default: SEARCH_LIMIT_DEFAULT,
|
|
2631
|
+
description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
|
|
2632
|
+
}
|
|
1411
2633
|
},
|
|
1412
|
-
required: ["query"]
|
|
2634
|
+
required: ["query"],
|
|
2635
|
+
additionalProperties: false
|
|
1413
2636
|
}
|
|
1414
|
-
},
|
|
1415
|
-
{
|
|
2637
|
+
}),
|
|
2638
|
+
define({
|
|
1416
2639
|
name: "get_component",
|
|
1417
|
-
description: "One
|
|
2640
|
+
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.",
|
|
1418
2641
|
inputSchema: {
|
|
1419
2642
|
type: "object",
|
|
1420
2643
|
properties: {
|
|
1421
|
-
name:
|
|
2644
|
+
name: {
|
|
2645
|
+
type: "string",
|
|
2646
|
+
minLength: 1,
|
|
2647
|
+
maxLength: LIMITS.name,
|
|
2648
|
+
description: "Component name, such as Button."
|
|
2649
|
+
},
|
|
1422
2650
|
dense: {
|
|
1423
2651
|
type: "boolean",
|
|
1424
|
-
|
|
2652
|
+
default: true,
|
|
2653
|
+
description: "Compact form (every constraint, one sentence per prop). False: full prose."
|
|
1425
2654
|
}
|
|
1426
2655
|
},
|
|
1427
|
-
required: ["name"]
|
|
2656
|
+
required: ["name"],
|
|
2657
|
+
additionalProperties: false
|
|
1428
2658
|
}
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
2659
|
+
}),
|
|
2660
|
+
define({
|
|
1431
2661
|
name: "list_components",
|
|
1432
2662
|
description: "Every OPERATOR component in one line each, with its parts.",
|
|
1433
|
-
inputSchema: { type: "object", properties: {} }
|
|
1434
|
-
},
|
|
1435
|
-
{
|
|
2663
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2664
|
+
}),
|
|
2665
|
+
define({
|
|
1436
2666
|
name: "get_instructions",
|
|
1437
|
-
description: "OPERATOR's house rules,
|
|
1438
|
-
inputSchema: { type: "object", properties: {} }
|
|
1439
|
-
},
|
|
1440
|
-
{
|
|
2667
|
+
description: "OPERATOR's house rules, component index and recipes: read once before building.",
|
|
2668
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2669
|
+
}),
|
|
2670
|
+
define({
|
|
1441
2671
|
name: "check_code",
|
|
1442
|
-
description: "Check TSX
|
|
2672
|
+
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.",
|
|
1443
2673
|
inputSchema: {
|
|
1444
2674
|
type: "object",
|
|
1445
|
-
properties: {
|
|
1446
|
-
|
|
2675
|
+
properties: {
|
|
2676
|
+
code: {
|
|
2677
|
+
type: "string",
|
|
2678
|
+
minLength: 1,
|
|
2679
|
+
maxLength: LIMITS.code,
|
|
2680
|
+
description: "The TSX source of one file."
|
|
2681
|
+
}
|
|
2682
|
+
},
|
|
2683
|
+
required: ["code"],
|
|
2684
|
+
additionalProperties: false
|
|
1447
2685
|
}
|
|
1448
|
-
}
|
|
2686
|
+
})
|
|
1449
2687
|
];
|
|
1450
2688
|
UnknownTool = class extends Error {
|
|
1451
2689
|
constructor(name) {
|
|
@@ -1462,26 +2700,38 @@ var index_exports = {};
|
|
|
1462
2700
|
__export(index_exports, {
|
|
1463
2701
|
CliError: () => CliError,
|
|
1464
2702
|
DEFAULT_OUT: () => DEFAULT_OUT,
|
|
2703
|
+
RESULT_SCHEMAS: () => RESULT_SCHEMAS,
|
|
2704
|
+
RESULT_SCHEMA_VERSION: () => RESULT_SCHEMA_VERSION,
|
|
2705
|
+
RESULT_TYPES: () => RESULT_TYPES,
|
|
1465
2706
|
TOOLS: () => TOOLS,
|
|
1466
2707
|
VERSION: () => VERSION,
|
|
1467
2708
|
applyBlock: () => applyBlock,
|
|
1468
2709
|
blockVersion: () => blockVersion,
|
|
2710
|
+
buildCheckReport: () => buildCheckReport,
|
|
2711
|
+
checkExitCode: () => checkExitCode,
|
|
1469
2712
|
checkPaths: () => checkPaths,
|
|
1470
2713
|
checkSource: () => checkSource,
|
|
1471
2714
|
componentDetail: () => componentDetail,
|
|
1472
2715
|
componentIndex: () => componentIndex,
|
|
1473
2716
|
contract: () => contract,
|
|
2717
|
+
contractDigest: () => contractDigest,
|
|
1474
2718
|
doctor: () => doctor,
|
|
2719
|
+
envelope: () => envelope,
|
|
2720
|
+
errorEnvelope: () => errorEnvelope,
|
|
1475
2721
|
handleMessage: () => handleMessage,
|
|
2722
|
+
identityOf: () => identityOf,
|
|
1476
2723
|
init: () => init,
|
|
1477
2724
|
install: () => install,
|
|
2725
|
+
outputSchemaFor: () => outputSchemaFor,
|
|
1478
2726
|
parseArgs: () => parseArgs,
|
|
1479
2727
|
registry: () => registry,
|
|
1480
2728
|
removeBlock: () => removeBlock,
|
|
1481
2729
|
renderAgentBlock: () => renderAgentBlock,
|
|
2730
|
+
renderCheckText: () => renderCheckText,
|
|
1482
2731
|
renderComponent: () => renderComponent,
|
|
1483
2732
|
resolveEntries: () => resolveEntries,
|
|
1484
2733
|
runAgentCommand: () => runAgentCommand,
|
|
2734
|
+
schemaFor: () => schemaFor,
|
|
1485
2735
|
search: () => search,
|
|
1486
2736
|
serve: () => serve
|
|
1487
2737
|
});
|
|
@@ -1505,15 +2755,17 @@ function install(options, context = {}) {
|
|
|
1505
2755
|
registry: context.registry ?? registry
|
|
1506
2756
|
});
|
|
1507
2757
|
}
|
|
1508
|
-
var
|
|
2758
|
+
var registry, loaded2, contract;
|
|
1509
2759
|
var init_index = __esm({
|
|
1510
2760
|
"src/index.ts"() {
|
|
1511
2761
|
"use strict";
|
|
1512
2762
|
init_esm_shims();
|
|
1513
|
-
init_package();
|
|
1514
2763
|
init_contract();
|
|
1515
2764
|
init_installer();
|
|
2765
|
+
init_identity();
|
|
1516
2766
|
init_agent_commands();
|
|
2767
|
+
init_identity();
|
|
2768
|
+
init_results();
|
|
1517
2769
|
init_agent_block();
|
|
1518
2770
|
init_check();
|
|
1519
2771
|
init_doctor();
|
|
@@ -1523,17 +2775,17 @@ var init_index = __esm({
|
|
|
1523
2775
|
init_args();
|
|
1524
2776
|
init_installer();
|
|
1525
2777
|
init_types();
|
|
1526
|
-
VERSION = package_default.version;
|
|
1527
2778
|
registry = loadRegistry();
|
|
1528
|
-
contract = () =>
|
|
2779
|
+
contract = () => loaded2 ??= loadContract();
|
|
1529
2780
|
}
|
|
1530
2781
|
});
|
|
1531
2782
|
|
|
1532
2783
|
// src/bin.ts
|
|
1533
2784
|
init_esm_shims();
|
|
1534
2785
|
init_args();
|
|
2786
|
+
init_results();
|
|
1535
2787
|
init_types();
|
|
1536
|
-
var helpText = (
|
|
2788
|
+
var helpText = (version2) => `oprt ${version2} \u2014 OPERATOR source installer
|
|
1537
2789
|
|
|
1538
2790
|
Usage:
|
|
1539
2791
|
oprt list [--json]
|
|
@@ -1551,11 +2803,19 @@ For coding agents (each takes --json):
|
|
|
1551
2803
|
oprt component --list Every component, one line each.
|
|
1552
2804
|
oprt check <paths...> [--strict]
|
|
1553
2805
|
Check written code against the contract: option values,
|
|
1554
|
-
nesting, required props, house rules.
|
|
2806
|
+
nesting, required props, house rules. Says how much it
|
|
2807
|
+
read (evaluation_status); a file that does not parse is
|
|
2808
|
+
not checked, and is never clean.
|
|
1555
2809
|
oprt doctor Check this project's setup.
|
|
1556
2810
|
oprt mcp Serve the lookups and the check to an agent over the
|
|
1557
2811
|
Model Context Protocol (stdin and stdout).
|
|
1558
2812
|
|
|
2813
|
+
Exit codes: 0 success. check: 0 when every file read has no errors (no warnings either
|
|
2814
|
+
under --strict); limits it could not read are shown and do not fail the run. 1 when check
|
|
2815
|
+
finds an error, a file does not parse, or no .tsx or .jsx file was read; also for other
|
|
2816
|
+
failures (file conflicts, a parser that cannot be loaded). 2 for a usage error, an unknown name or a
|
|
2817
|
+
path that does not exist.
|
|
2818
|
+
|
|
1559
2819
|
Names accept PascalCase or kebab-case. Default output: src/components/operator.
|
|
1560
2820
|
--dry-run Preview all files and dependencies without creating anything.
|
|
1561
2821
|
--overwrite Replace different existing files; identical files are always skipped.
|
|
@@ -1571,83 +2831,30 @@ try {
|
|
|
1571
2831
|
const major = Number(process.versions.node.split(".")[0]);
|
|
1572
2832
|
if (major < 22) throw new CliError("oprt requires Node.js 22 or newer.");
|
|
1573
2833
|
const command = parseArgs(process.argv.slice(2));
|
|
1574
|
-
const { VERSION: VERSION2, contract: contract2,
|
|
2834
|
+
const { VERSION: VERSION2, contract: contract2, registry: registry2, runAgentCommand: runAgentCommand2, serve: serve2 } = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
1575
2835
|
if (command.command === "mcp")
|
|
1576
2836
|
await serve2(process.stdin, process.stdout, { contract: contract2(), registry: registry2 });
|
|
1577
|
-
else if (command.command === "
|
|
2837
|
+
else if (command.command === "help") process.stdout.write(helpText(VERSION2));
|
|
2838
|
+
else if (command.command === "version") process.stdout.write(`${VERSION2}
|
|
2839
|
+
`);
|
|
2840
|
+
else {
|
|
1578
2841
|
const answer = runAgentCommand2(command, { cwd: process.cwd(), contract: contract2(), registry: registry2 });
|
|
1579
2842
|
process.stdout.write(command.json ? `${JSON.stringify(answer.json, null, 2)}
|
|
1580
2843
|
` : answer.text);
|
|
1581
2844
|
process.exitCode = answer.exitCode;
|
|
1582
|
-
} else if (command.command === "help") process.stdout.write(helpText(VERSION2));
|
|
1583
|
-
else if (command.command === "version") process.stdout.write(`${VERSION2}
|
|
1584
|
-
`);
|
|
1585
|
-
else if (command.command === "list") {
|
|
1586
|
-
if (command.json)
|
|
1587
|
-
process.stdout.write(
|
|
1588
|
-
JSON.stringify(
|
|
1589
|
-
{ version: VERSION2, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
1590
|
-
null,
|
|
1591
|
-
2
|
|
1592
|
-
) + "\n"
|
|
1593
|
-
);
|
|
1594
|
-
else {
|
|
1595
|
-
process.stdout.write(`OPERATOR ${VERSION2}
|
|
1596
|
-
|
|
1597
|
-
`);
|
|
1598
|
-
for (const kind of ["component", "recipe"]) {
|
|
1599
|
-
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
1600
|
-
process.stdout.write(
|
|
1601
|
-
`${kind === "component" ? "Components" : "Recipes"} (${entries.length})
|
|
1602
|
-
`
|
|
1603
|
-
);
|
|
1604
|
-
for (const entry of entries)
|
|
1605
|
-
process.stdout.write(` ${entry.slug.padEnd(22)} ${entry.description}
|
|
1606
|
-
`);
|
|
1607
|
-
process.stdout.write("\n");
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
} else {
|
|
1611
|
-
const result = install2(command);
|
|
1612
|
-
process.stdout.write(
|
|
1613
|
-
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}
|
|
1614
|
-
Output: ${result.outputDirectory}
|
|
1615
|
-
`
|
|
1616
|
-
);
|
|
1617
|
-
for (const file of result.files)
|
|
1618
|
-
process.stdout.write(` ${file.action.padEnd(9)} ${file.path}
|
|
1619
|
-
`);
|
|
1620
|
-
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
1621
|
-
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
1622
|
-
process.stdout.write(`
|
|
1623
|
-
Dependencies: ${runtime.map(versioned).join(", ")}
|
|
1624
|
-
`);
|
|
1625
|
-
if (result.devDependencies.length > 0)
|
|
1626
|
-
process.stdout.write(
|
|
1627
|
-
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}
|
|
1628
|
-
`
|
|
1629
|
-
);
|
|
1630
|
-
process.stdout.write(
|
|
1631
|
-
"Peer and style setup: react-dom, @oprt/tokens, @oprt/tailwind, tailwindcss.\nScan the output directory with Tailwind and load the OPERATOR token CSS.\nInstall any missing dependencies yourself; no package configuration was changed.\n"
|
|
1632
|
-
);
|
|
1633
2845
|
}
|
|
1634
2846
|
} catch (error) {
|
|
1635
2847
|
const message = error instanceof Error ? error.message : String(error);
|
|
1636
|
-
if (process.argv.includes("--json"))
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
2
|
|
1647
|
-
)}
|
|
1648
|
-
`
|
|
1649
|
-
);
|
|
1650
|
-
else process.stderr.write(`oprt: ${message}
|
|
2848
|
+
if (process.argv.includes("--json")) {
|
|
2849
|
+
let loaded3;
|
|
2850
|
+
try {
|
|
2851
|
+
loaded3 = (await Promise.resolve().then(() => (init_index(), index_exports))).contract();
|
|
2852
|
+
} catch {
|
|
2853
|
+
loaded3 = void 0;
|
|
2854
|
+
}
|
|
2855
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope(error, loaded3), null, 2)}
|
|
2856
|
+
`);
|
|
2857
|
+
} else process.stderr.write(`oprt: ${message}
|
|
1651
2858
|
`);
|
|
1652
2859
|
process.exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
1653
2860
|
}
|