@oprt/cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -23
- package/dist/bin.js +1750 -673
- package/dist/contract.json +1 -1
- package/dist/index.cjs +1493 -408
- package/dist/index.d.cts +259 -90
- package/dist/index.d.ts +259 -90
- package/dist/index.js +1470 -397
- package/dist/{read-8a9euzJx.d.cts → read-C7rE5UlY.d.cts} +48 -21
- package/dist/{read-8a9euzJx.d.ts → read-C7rE5UlY.d.ts} +48 -21
- package/dist/read.cjs +2 -1
- package/dist/read.d.cts +1 -1
- package/dist/read.d.ts +1 -1
- package/dist/read.js +2 -1
- package/dist/registry.json +1 -1
- package/package.json +2 -1
package/dist/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,448 +396,133 @@ var init_args = __esm({
|
|
|
193
396
|
}
|
|
194
397
|
});
|
|
195
398
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"package.json"() {
|
|
200
|
-
package_default = {
|
|
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
|
-
};
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// src/contract.ts
|
|
271
|
-
import { existsSync, readFileSync } from "fs";
|
|
272
|
-
import { dirname, join } from "path";
|
|
399
|
+
// src/parser.ts
|
|
400
|
+
import { createRequire } from "module";
|
|
401
|
+
import { join } from "path";
|
|
273
402
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (found === void 0) {
|
|
279
|
-
throw new Error(
|
|
280
|
-
`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.`
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
return JSON.parse(readFileSync(found, "utf8"));
|
|
284
|
-
}
|
|
285
|
-
var init_contract = __esm({
|
|
286
|
-
"src/contract.ts"() {
|
|
287
|
-
"use strict";
|
|
288
|
-
init_esm_shims();
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// src/installer.ts
|
|
293
|
-
import {
|
|
294
|
-
constants,
|
|
295
|
-
closeSync,
|
|
296
|
-
lstatSync,
|
|
297
|
-
mkdirSync,
|
|
298
|
-
openSync,
|
|
299
|
-
readFileSync as readFileSync2,
|
|
300
|
-
realpathSync,
|
|
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);
|
|
315
|
-
}
|
|
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
|
-
}
|
|
325
|
-
function stat(path2) {
|
|
403
|
+
function usable(typescript) {
|
|
404
|
+
const ts = typescript;
|
|
405
|
+
if (typeof ts.createSourceFile !== "function" || ts.ScriptKind === void 0)
|
|
406
|
+
return `TypeScript ${ts.version ?? "(unknown version)"} has no JavaScript syntax API (createSourceFile), which oprt reads code with; ${SUPPORTED_TYPESCRIPT} has it`;
|
|
326
407
|
try {
|
|
327
|
-
|
|
408
|
+
const probe = ts.createSourceFile(
|
|
409
|
+
"probe.tsx",
|
|
410
|
+
"<a>",
|
|
411
|
+
ts.ScriptTarget?.Latest ?? 99,
|
|
412
|
+
true,
|
|
413
|
+
ts.ScriptKind.TSX
|
|
414
|
+
);
|
|
415
|
+
const diagnostics = probe.parseDiagnostics;
|
|
416
|
+
if (!Array.isArray(diagnostics)) return "it does not report parse errors on a source file";
|
|
417
|
+
if (diagnostics.length === 0) return "it did not report a syntax error in code that has one";
|
|
328
418
|
} catch (error) {
|
|
329
|
-
|
|
330
|
-
throw error;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function inspectPath(root, destination, directory = false) {
|
|
334
|
-
const path2 = relative(root, destination);
|
|
335
|
-
if (path2 === ".." || path2.startsWith(`..${sep}`) || isAbsolute(path2))
|
|
336
|
-
throw new CliError("Output path escapes the working directory.");
|
|
337
|
-
let cursor = root;
|
|
338
|
-
const segments = path2 ? path2.split(sep) : [];
|
|
339
|
-
for (let i = 0; i < segments.length; i++) {
|
|
340
|
-
cursor = resolve(cursor, segments[i]);
|
|
341
|
-
const info = stat(cursor);
|
|
342
|
-
if (!info) continue;
|
|
343
|
-
if (info.isSymbolicLink()) throw new CliError(`Refusing symlink in output path: ${cursor}`);
|
|
344
|
-
const needsDirectory = i < segments.length - 1 || directory;
|
|
345
|
-
if (needsDirectory && !info.isDirectory())
|
|
346
|
-
throw new CliError(`Output ancestor is not a directory: ${cursor}`);
|
|
347
|
-
if (!needsDirectory && !info.isFile())
|
|
348
|
-
throw new CliError(`Output target is not a regular file: ${cursor}`);
|
|
419
|
+
return `it failed on a probe parse: ${error instanceof Error ? error.message : String(error)}`;
|
|
349
420
|
}
|
|
350
|
-
return
|
|
421
|
+
return null;
|
|
351
422
|
}
|
|
352
|
-
function
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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" };
|
|
374
|
-
});
|
|
375
|
-
const dependencies = [...new Set(entries.flatMap((entry) => entry.dependencies))].sort();
|
|
376
|
-
const runtime = new Set(
|
|
377
|
-
entries.flatMap(
|
|
378
|
-
(entry) => entry.dependencies.filter((name) => !(entry.devDependencies ?? []).includes(name))
|
|
379
|
-
)
|
|
380
|
-
);
|
|
381
|
-
const result = {
|
|
382
|
-
outputDirectory,
|
|
383
|
-
entries: entries.map((entry) => entry.name),
|
|
384
|
-
dependencies,
|
|
385
|
-
devDependencies: dependencies.filter(
|
|
386
|
-
(name) => !runtime.has(name) && entries.some((entry) => (entry.devDependencies ?? []).includes(name))
|
|
387
|
-
),
|
|
388
|
-
ranges: Object.fromEntries(
|
|
389
|
-
dependencies.flatMap((name) => {
|
|
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);
|
|
423
|
+
function probeTypeScript(cwd, options = {}) {
|
|
424
|
+
const places = [
|
|
425
|
+
{ from: join(cwd, "package.json"), source: "project" },
|
|
426
|
+
...options.installer === false ? [] : [{ from: fileURLToPath2(import.meta.url), source: "installer" }]
|
|
427
|
+
];
|
|
428
|
+
let unusable;
|
|
429
|
+
for (const { from, source } of places) {
|
|
430
|
+
let file;
|
|
431
|
+
let loaded2;
|
|
406
432
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
433
|
+
const require2 = createRequire(from);
|
|
434
|
+
file = require2.resolve("typescript");
|
|
435
|
+
loaded2 = require2("typescript");
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error.code !== "MODULE_NOT_FOUND")
|
|
438
|
+
unusable ??= `typescript was found but failed to load: ${error instanceof Error ? error.message : String(error)}`;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const typescript = loaded2.default ?? loaded2;
|
|
442
|
+
const problem = usable(typescript);
|
|
443
|
+
if (problem !== null) {
|
|
444
|
+
unusable ??= `${problem} (at ${packageFolder(file)}).`;
|
|
445
|
+
continue;
|
|
410
446
|
}
|
|
447
|
+
return {
|
|
448
|
+
ready: true,
|
|
449
|
+
typescript,
|
|
450
|
+
resolution: {
|
|
451
|
+
name: "typescript",
|
|
452
|
+
version: typescript.version,
|
|
453
|
+
source,
|
|
454
|
+
resolved_from: packageFolder(file)
|
|
455
|
+
}
|
|
456
|
+
};
|
|
411
457
|
}
|
|
412
|
-
|
|
458
|
+
if (unusable !== void 0)
|
|
459
|
+
return { ready: false, reason: "typescript_unusable", detail: unusable };
|
|
460
|
+
return {
|
|
461
|
+
ready: false,
|
|
462
|
+
reason: "typescript_missing",
|
|
463
|
+
detail: `typescript could not be resolved from ${cwd}${options.installer === false ? "" : " or from beside the installer"}.`
|
|
464
|
+
};
|
|
413
465
|
}
|
|
414
|
-
var
|
|
415
|
-
|
|
466
|
+
var SUPPORTED_TYPESCRIPT, INSTALL_TYPESCRIPT, packageFolder;
|
|
467
|
+
var init_parser = __esm({
|
|
468
|
+
"src/parser.ts"() {
|
|
416
469
|
"use strict";
|
|
417
470
|
init_esm_shims();
|
|
418
|
-
|
|
419
|
-
|
|
471
|
+
SUPPORTED_TYPESCRIPT = "TypeScript 5 or 6";
|
|
472
|
+
INSTALL_TYPESCRIPT = "npm install -D typescript@6";
|
|
473
|
+
packageFolder = (file) => /^(.*[\\/]node_modules[\\/]typescript)[\\/]/.exec(file)?.[1] ?? file;
|
|
420
474
|
}
|
|
421
475
|
});
|
|
422
476
|
|
|
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) {
|
|
477
|
+
// src/check.ts
|
|
478
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
479
|
+
import { basename, extname, join as join2, relative, resolve, sep } from "path";
|
|
480
|
+
function allowedChildren2(rules, name) {
|
|
458
481
|
for (const [parent, rule] of Object.entries(rules ?? {})) {
|
|
459
482
|
if (parent === name) return rule.allowed_children;
|
|
460
|
-
const nested =
|
|
483
|
+
const nested = allowedChildren2(rule.children, name);
|
|
461
484
|
if (nested !== void 0) return nested;
|
|
462
485
|
}
|
|
463
486
|
return void 0;
|
|
464
487
|
}
|
|
465
|
-
function
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const guide = contract2.guidance.components[name] ?? {};
|
|
476
|
-
const props = Object.entries(component.props).map(([prop, definition]) => {
|
|
477
|
-
const notes = guide.values?.[prop];
|
|
478
|
-
return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
|
|
479
|
-
});
|
|
480
|
-
const allowed = allowedChildren(contract2.composition_rules, name);
|
|
481
|
-
return {
|
|
482
|
-
name,
|
|
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
|
|
494
|
-
};
|
|
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("; ")}`
|
|
488
|
+
function checkSource(typescript, contract2, file, text2) {
|
|
489
|
+
const ts = typescript;
|
|
490
|
+
const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
|
|
491
|
+
const source = ts.createSourceFile(file, text2, ts.ScriptTarget.Latest, true, kind);
|
|
492
|
+
const parseDiagnostics = source.parseDiagnostics;
|
|
493
|
+
if (!Array.isArray(parseDiagnostics))
|
|
494
|
+
throw new CliError(
|
|
495
|
+
`This TypeScript does not report syntax errors on a parsed file, so a file that does not parse could be called clean. Install TypeScript 5 or 6 in this project (${INSTALL_TYPESCRIPT}).`,
|
|
496
|
+
1,
|
|
497
|
+
{ code: "ERR_TYPESCRIPT_UNUSABLE" }
|
|
512
498
|
);
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
499
|
+
if (parseDiagnostics.length > 0) {
|
|
500
|
+
const ordered = [...parseDiagnostics].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
501
|
+
const shown = ordered.slice(0, MAX_SYNTAX_ERRORS);
|
|
502
|
+
return {
|
|
503
|
+
file,
|
|
504
|
+
status: "not_evaluated",
|
|
505
|
+
limitations: [],
|
|
506
|
+
findings: shown.map((diagnostic, index) => {
|
|
507
|
+
const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
508
|
+
const more = ordered.length - shown.length;
|
|
509
|
+
return {
|
|
510
|
+
code: "SYNTAX_ERROR",
|
|
511
|
+
severity: "error",
|
|
512
|
+
file,
|
|
513
|
+
line: line + 1,
|
|
514
|
+
column: character + 1,
|
|
515
|
+
message: `${ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")} (TS${diagnostic.code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
|
|
516
|
+
};
|
|
517
|
+
})
|
|
518
|
+
};
|
|
517
519
|
}
|
|
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
520
|
const findings = [];
|
|
521
|
+
const limitations = [];
|
|
522
|
+
const limit = (node, limitation) => {
|
|
523
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
524
|
+
limitations.push({ ...limitation, file, line: line + 1, column: character + 1 });
|
|
525
|
+
};
|
|
638
526
|
const report = (node, finding) => {
|
|
639
527
|
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
640
528
|
findings.push({
|
|
@@ -705,6 +593,31 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
705
593
|
return [];
|
|
706
594
|
};
|
|
707
595
|
const valueOf = (attribute) => attribute.initializer === void 0 ? void 0 : ts.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
|
|
596
|
+
const optionValue = (expression) => {
|
|
597
|
+
const read = [];
|
|
598
|
+
const unread = [];
|
|
599
|
+
const walk = (node) => {
|
|
600
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
601
|
+
read.push(node.text);
|
|
602
|
+
else if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(node))
|
|
603
|
+
walk(node.expression);
|
|
604
|
+
else if (ts.isConditionalExpression(node)) {
|
|
605
|
+
walk(node.whenTrue);
|
|
606
|
+
walk(node.whenFalse);
|
|
607
|
+
} else if (ts.isBinaryExpression(node)) {
|
|
608
|
+
const operator = node.operatorToken.kind;
|
|
609
|
+
if (operator === ts.SyntaxKind.QuestionQuestionToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
610
|
+
walk(node.left);
|
|
611
|
+
walk(node.right);
|
|
612
|
+
} else if (operator === ts.SyntaxKind.AmpersandAmpersandToken) walk(node.right);
|
|
613
|
+
else unread.push(node);
|
|
614
|
+
} else if (!// No value: the component's default applies.
|
|
615
|
+
(ts.isIdentifier(node) && node.text === "undefined" || node.kind === ts.SyntaxKind.NullKeyword))
|
|
616
|
+
unread.push(node);
|
|
617
|
+
};
|
|
618
|
+
if (expression) walk(expression);
|
|
619
|
+
return { literals: read, unread };
|
|
620
|
+
};
|
|
708
621
|
const classText = (expression) => {
|
|
709
622
|
if (!expression) return "";
|
|
710
623
|
if (ts.isTemplateExpression(expression))
|
|
@@ -777,6 +690,13 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
777
690
|
if (component === null) return null;
|
|
778
691
|
const definition = contract2.components[component];
|
|
779
692
|
const spread = attributes.some((attribute) => ts.isJsxSpreadAttribute(attribute));
|
|
693
|
+
for (const attribute of attributes)
|
|
694
|
+
if (ts.isJsxSpreadAttribute(attribute))
|
|
695
|
+
limit(attribute, {
|
|
696
|
+
code: "UNRESOLVED_SPREAD",
|
|
697
|
+
component,
|
|
698
|
+
message: `Props spread into <${component}> (${quote(attribute.getText(source))}) were not read: option values and required props that arrive through them were not checked.`
|
|
699
|
+
});
|
|
780
700
|
const given = /* @__PURE__ */ new Set();
|
|
781
701
|
for (const attribute of attributes) {
|
|
782
702
|
if (!ts.isJsxAttribute(attribute)) continue;
|
|
@@ -785,18 +705,26 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
785
705
|
const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
|
|
786
706
|
if (known === void 0) {
|
|
787
707
|
if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
|
|
788
|
-
const
|
|
708
|
+
const names2 = Object.keys(definition.props);
|
|
789
709
|
report(attribute, {
|
|
790
710
|
code: "UNKNOWN_PROP",
|
|
791
711
|
severity: "warning",
|
|
792
712
|
component,
|
|
793
713
|
prop,
|
|
794
|
-
message: `${component} has no prop ${prop}.` + (
|
|
714
|
+
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
715
|
});
|
|
796
716
|
continue;
|
|
797
717
|
}
|
|
798
718
|
if (known.type !== "enum" || known.values === void 0) continue;
|
|
799
|
-
|
|
719
|
+
const { literals: options, unread } = optionValue(valueOf(attribute));
|
|
720
|
+
for (const expression of unread)
|
|
721
|
+
limit(attribute, {
|
|
722
|
+
code: "UNRESOLVED_VALUE",
|
|
723
|
+
component,
|
|
724
|
+
prop,
|
|
725
|
+
message: `${component} ${prop} is set from an expression (${quote(expression.getText(source))}) whose value is not in the source, so it was not compared with the options.`
|
|
726
|
+
});
|
|
727
|
+
for (const literal of options) {
|
|
800
728
|
if (known.values.includes(literal)) continue;
|
|
801
729
|
const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
|
|
802
730
|
report(attribute, {
|
|
@@ -866,94 +794,949 @@ function checkSource(typescript, contract2, file, text2) {
|
|
|
866
794
|
ts.forEachChild(node, (child) => visit(child, ancestors, parent));
|
|
867
795
|
};
|
|
868
796
|
visit(source, [], null);
|
|
869
|
-
|
|
797
|
+
const byPlace = (a, b) => a.line - b.line || a.column - b.column;
|
|
798
|
+
return {
|
|
799
|
+
file,
|
|
800
|
+
status: limitations.length > 0 ? "partial" : "evaluated",
|
|
801
|
+
findings: findings.sort(byPlace),
|
|
802
|
+
limitations: limitations.sort(byPlace)
|
|
803
|
+
};
|
|
870
804
|
}
|
|
871
805
|
function* sources(path2) {
|
|
872
|
-
|
|
873
|
-
if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
|
|
806
|
+
if (excluded(basename(path2))) return;
|
|
874
807
|
if (statSync(path2).isDirectory()) {
|
|
875
808
|
for (const entry of readdirSync(path2).sort()) yield* sources(join2(path2, entry));
|
|
876
|
-
} else if (READ.has(extname(path2)) && !NOT_INTERFACE.test(
|
|
809
|
+
} else if (READ.has(extname(path2)) && !NOT_INTERFACE.test(basename(path2))) yield path2;
|
|
877
810
|
}
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
811
|
+
function whyNotRead(path2) {
|
|
812
|
+
const name = basename(path2);
|
|
813
|
+
if (excluded(name)) return "excluded_folder";
|
|
814
|
+
if (statSync(path2).isDirectory()) return null;
|
|
815
|
+
if (!READ.has(extname(path2))) return "unsupported_extension";
|
|
816
|
+
return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
|
|
817
|
+
}
|
|
818
|
+
function buildCheckReport(evaluations, context) {
|
|
819
|
+
const findings = evaluations.flatMap(({ findings: found }) => found);
|
|
820
|
+
const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
|
|
821
|
+
const partial = count("partial");
|
|
822
|
+
const notEvaluated = count("not_evaluated");
|
|
823
|
+
const evaluated = evaluations.length - notEvaluated;
|
|
824
|
+
const errors = findings.filter(({ severity }) => severity === "error").length;
|
|
825
|
+
const warnings = findings.length - errors;
|
|
826
|
+
let status;
|
|
827
|
+
if (evaluations.length === 0) status = "no_files_evaluated";
|
|
828
|
+
else if (evaluated === 0) status = "not_evaluated";
|
|
829
|
+
else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
|
|
830
|
+
else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
|
|
831
|
+
return {
|
|
832
|
+
evaluation_status: status,
|
|
833
|
+
files: evaluations.length,
|
|
834
|
+
findings,
|
|
835
|
+
summary: { errors, warnings },
|
|
836
|
+
coverage: {
|
|
837
|
+
files_found: evaluations.length,
|
|
838
|
+
files_evaluated: evaluated,
|
|
839
|
+
files_partial: partial,
|
|
840
|
+
files_not_evaluated: notEvaluated
|
|
841
|
+
},
|
|
842
|
+
limitations: evaluations.flatMap(({ limitations }) => limitations),
|
|
843
|
+
skipped_inputs: context.skipped ?? [],
|
|
844
|
+
scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
|
|
845
|
+
parser: context.parser
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function checkExitCode(report, { strict }) {
|
|
849
|
+
if (report.evaluation_status === "no_files_evaluated") return 1;
|
|
850
|
+
if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
|
|
851
|
+
return strict && report.summary.warnings > 0 ? 1 : 0;
|
|
852
|
+
}
|
|
853
|
+
function resolveParser(cwd, provided, who) {
|
|
854
|
+
if (provided !== void 0 && provided !== null)
|
|
855
|
+
return { typescript: provided, parser: describeProvided(provided) };
|
|
856
|
+
const probe = provided === null ? null : probeTypeScript(cwd);
|
|
857
|
+
if (probe?.ready) {
|
|
858
|
+
const { name, version, source } = probe.resolution;
|
|
859
|
+
return { typescript: probe.typescript, parser: { name, version, source } };
|
|
884
860
|
}
|
|
885
|
-
|
|
861
|
+
if (probe !== null && probe.reason === "typescript_unusable")
|
|
862
|
+
throw new CliError(`${who} cannot use the TypeScript it found: ${probe.detail}`, 1, {
|
|
863
|
+
code: "ERR_TYPESCRIPT_UNUSABLE"
|
|
864
|
+
});
|
|
865
|
+
throw new CliError(
|
|
866
|
+
`${who} reads code with the project's TypeScript, and found none. Install typescript in this project (${INSTALL_TYPESCRIPT}) and run it again. It looked from ${cwd}.`,
|
|
867
|
+
1,
|
|
868
|
+
{ code: "ERR_TYPESCRIPT_MISSING" }
|
|
869
|
+
);
|
|
886
870
|
}
|
|
887
871
|
function checkPaths(paths, context) {
|
|
888
|
-
const root =
|
|
872
|
+
const root = resolve(context.cwd);
|
|
889
873
|
const files = [];
|
|
874
|
+
const skipped = [];
|
|
890
875
|
for (const path2 of paths) {
|
|
891
|
-
const absolute =
|
|
892
|
-
if (!
|
|
876
|
+
const absolute = resolve(root, path2);
|
|
877
|
+
if (!existsSync(absolute))
|
|
893
878
|
throw new CliError(`No such file or folder: ${path2}`, 2, { code: "ERR_PATH_NOT_FOUND" });
|
|
879
|
+
const reason = whyNotRead(absolute);
|
|
880
|
+
if (reason !== null) skipped.push({ path: path2, reason });
|
|
894
881
|
files.push(...sources(absolute));
|
|
895
882
|
}
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
883
|
+
const { typescript, parser } = resolveParser(root, context.typescript, "oprt check");
|
|
884
|
+
const evaluations = [...new Set(files)].map(
|
|
885
|
+
(file) => checkSource(
|
|
886
|
+
typescript,
|
|
887
|
+
context.contract,
|
|
888
|
+
relative(root, file).split(sep).join("/"),
|
|
889
|
+
readFileSync(file, "utf8")
|
|
890
|
+
)
|
|
891
|
+
);
|
|
892
|
+
return buildCheckReport(evaluations, { parser, skipped });
|
|
893
|
+
}
|
|
894
|
+
function renderCheckText(report) {
|
|
895
|
+
const lines = report.findings.map(
|
|
896
|
+
({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
|
|
897
|
+
);
|
|
898
|
+
const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
|
|
899
|
+
for (const { file, line, column, code, message } of shown)
|
|
900
|
+
lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
|
|
901
|
+
if (report.limitations.length > shown.length)
|
|
902
|
+
lines.push(
|
|
903
|
+
` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
|
|
904
|
+
);
|
|
905
|
+
const { coverage, evaluation_status: status } = report;
|
|
906
|
+
if (status === "no_files_evaluated") {
|
|
907
|
+
lines.push("No .tsx or .jsx file was read, so nothing was checked.");
|
|
908
|
+
const labels = {
|
|
909
|
+
unsupported_extension: "not a .tsx or .jsx file",
|
|
910
|
+
test_or_story_file: "a test, spec or story file, which is skipped",
|
|
911
|
+
excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
|
|
912
|
+
};
|
|
913
|
+
for (const { path: path2, reason } of report.skipped_inputs)
|
|
914
|
+
lines.push(` ${path2}: ${labels[reason]}`);
|
|
915
|
+
} else if (coverage.files_not_evaluated > 0)
|
|
916
|
+
lines.push(
|
|
917
|
+
`${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
|
|
918
|
+
);
|
|
919
|
+
else if (status === "evaluated_clean")
|
|
920
|
+
lines.push(
|
|
921
|
+
"Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
|
|
922
|
+
);
|
|
923
|
+
lines.push(
|
|
924
|
+
`${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
|
|
925
|
+
);
|
|
926
|
+
return `${lines.join("\n")}
|
|
927
|
+
`;
|
|
928
|
+
}
|
|
929
|
+
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, describeProvided, plural, MAX_LIMITS_SHOWN;
|
|
930
|
+
var init_check = __esm({
|
|
931
|
+
"src/check.ts"() {
|
|
932
|
+
"use strict";
|
|
933
|
+
init_esm_shims();
|
|
934
|
+
init_lookup();
|
|
935
|
+
init_parser();
|
|
936
|
+
init_types();
|
|
937
|
+
init_parser();
|
|
938
|
+
FINDING_CODES = [
|
|
939
|
+
"INVALID_PROP_VALUE",
|
|
940
|
+
"UNKNOWN_EXPORT",
|
|
941
|
+
"DOT_NOTATION",
|
|
942
|
+
"INVALID_NESTING",
|
|
943
|
+
"INVALID_CHILD",
|
|
944
|
+
"MISSING_REQUIRED_PROP",
|
|
945
|
+
"UNKNOWN_PROP",
|
|
946
|
+
"RAW_COLOR",
|
|
947
|
+
"ARBITRARY_SIZE",
|
|
948
|
+
"ROUNDED_CORNER",
|
|
949
|
+
"NATIVE_CONTROL",
|
|
950
|
+
/** The file does not parse. It was not checked against the contract. */
|
|
951
|
+
"SYNTAX_ERROR"
|
|
952
|
+
];
|
|
953
|
+
LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
|
|
954
|
+
SCOPE = {
|
|
955
|
+
checked: ["syntax", "contract", "house_rules"],
|
|
956
|
+
not_checked: ["types", "render"]
|
|
957
|
+
};
|
|
958
|
+
PACKAGE = "@oprt/react";
|
|
959
|
+
NATIVE = {
|
|
960
|
+
button: "Button",
|
|
961
|
+
input: "Input, or the control for its type (Checkbox, RadioGroup, NumberInput, Slider, DatePicker, FileUpload)",
|
|
962
|
+
select: "Select",
|
|
963
|
+
textarea: "Textarea",
|
|
964
|
+
table: "DataTable or DataGrid",
|
|
965
|
+
dialog: "Dialog",
|
|
966
|
+
progress: "Progress",
|
|
967
|
+
hr: "Divider"
|
|
968
|
+
};
|
|
969
|
+
NATIVE_ATTRIBUTES = new Set(
|
|
970
|
+
`key ref children className style id title role tabIndex hidden lang dir slot draggable spellCheck
|
|
971
|
+
translate inert accessKey autoCapitalize contentEditable enterKeyHint inputMode
|
|
972
|
+
dangerouslySetInnerHTML suppressHydrationWarning suppressContentEditableWarning htmlFor type name
|
|
973
|
+
value defaultValue checked defaultChecked disabled readOnly required placeholder autoComplete
|
|
974
|
+
autoFocus min max step minLength maxLength pattern multiple accept form formAction href target rel
|
|
975
|
+
download src alt width height loading rows cols wrap open colSpan rowSpan scope headers label list
|
|
976
|
+
size method action encType noValidate popover popoverTarget popoverTargetAction asChild`.split(
|
|
977
|
+
/\s+/
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
nativeAttribute = (name) => NATIVE_ATTRIBUTES.has(name) || /^(aria-|data-)/.test(name) || /^on[A-Z]/.test(name);
|
|
981
|
+
RAW_COLOR_CLASS = /\[(?:#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\()/;
|
|
982
|
+
RAW_COLOR_VALUE = /^#[0-9a-fA-F]{3,8}$|^(?:rgb|hsl)a?\(/;
|
|
983
|
+
ARBITRARY_SIZE = /(?:^|[\s:])-?[a-z][a-z0-9-]*-\[-?\d*\.?\d+px\]/;
|
|
984
|
+
ROUNDED_CORNER = /(?:^|[\s:])rounded(?!-none(?:\s|$))(?:-[^\s]+)?(?=\s|$)/;
|
|
985
|
+
quote = (text2) => {
|
|
986
|
+
const flat = text2.replace(/\s+/g, " ").trim();
|
|
987
|
+
return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
|
|
988
|
+
};
|
|
989
|
+
MAX_SYNTAX_ERRORS = 10;
|
|
990
|
+
SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
991
|
+
READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
992
|
+
NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
|
|
993
|
+
excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
|
|
994
|
+
describeProvided = (typescript) => ({
|
|
995
|
+
name: "typescript",
|
|
996
|
+
version: typescript.version ?? "unknown",
|
|
997
|
+
source: "provided"
|
|
998
|
+
});
|
|
999
|
+
plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
|
|
1000
|
+
MAX_LIMITS_SHOWN = 20;
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// package.json
|
|
1005
|
+
var package_default;
|
|
1006
|
+
var init_package = __esm({
|
|
1007
|
+
"package.json"() {
|
|
1008
|
+
package_default = {
|
|
1009
|
+
name: "@oprt/cli",
|
|
1010
|
+
version: "0.3.0",
|
|
1011
|
+
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.",
|
|
1012
|
+
keywords: [
|
|
1013
|
+
"design-system",
|
|
1014
|
+
"cli",
|
|
1015
|
+
"coding-agents",
|
|
1016
|
+
"mcp",
|
|
1017
|
+
"model-context-protocol",
|
|
1018
|
+
"code-check",
|
|
1019
|
+
"components"
|
|
1020
|
+
],
|
|
1021
|
+
homepage: "https://oprt.dev/docs/agents/cli",
|
|
1022
|
+
type: "module",
|
|
1023
|
+
main: "./dist/index.cjs",
|
|
1024
|
+
module: "./dist/index.js",
|
|
1025
|
+
types: "./dist/index.d.ts",
|
|
1026
|
+
bin: {
|
|
1027
|
+
oprt: "./dist/bin.js"
|
|
1028
|
+
},
|
|
1029
|
+
engines: {
|
|
1030
|
+
node: ">=22"
|
|
1031
|
+
},
|
|
1032
|
+
exports: {
|
|
1033
|
+
".": {
|
|
1034
|
+
import: {
|
|
1035
|
+
types: "./dist/index.d.ts",
|
|
1036
|
+
default: "./dist/index.js"
|
|
1037
|
+
},
|
|
1038
|
+
require: {
|
|
1039
|
+
types: "./dist/index.d.cts",
|
|
1040
|
+
default: "./dist/index.cjs"
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
"./read": {
|
|
1044
|
+
import: {
|
|
1045
|
+
types: "./dist/read.d.ts",
|
|
1046
|
+
default: "./dist/read.js"
|
|
1047
|
+
},
|
|
1048
|
+
require: {
|
|
1049
|
+
types: "./dist/read.d.cts",
|
|
1050
|
+
default: "./dist/read.cjs"
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
files: [
|
|
1055
|
+
"dist"
|
|
1056
|
+
],
|
|
1057
|
+
scripts: {
|
|
1058
|
+
clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
|
|
1059
|
+
build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
|
|
1060
|
+
dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
|
|
1061
|
+
test: "vitest run",
|
|
1062
|
+
typecheck: "tsc --noEmit",
|
|
1063
|
+
lint: "eslint src scripts"
|
|
1064
|
+
},
|
|
1065
|
+
devDependencies: {
|
|
1066
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
1067
|
+
tsup: "^8.3.0",
|
|
1068
|
+
typescript: "^5.6.3",
|
|
1069
|
+
vitest: "^2.1.3"
|
|
1070
|
+
},
|
|
1071
|
+
license: "MIT",
|
|
1072
|
+
publishConfig: {
|
|
1073
|
+
access: "public"
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// src/identity.ts
|
|
1080
|
+
import { createHash } from "crypto";
|
|
1081
|
+
function contractDigest(contract2) {
|
|
1082
|
+
let digest = digests.get(contract2);
|
|
1083
|
+
if (digest === void 0) {
|
|
1084
|
+
digest = `sha256:${createHash("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
|
|
1085
|
+
digests.set(contract2, digest);
|
|
1086
|
+
}
|
|
1087
|
+
return digest;
|
|
1088
|
+
}
|
|
1089
|
+
function identityOf(contract2) {
|
|
1090
|
+
return {
|
|
1091
|
+
cli_version: VERSION,
|
|
1092
|
+
contract_version: contract2.version,
|
|
1093
|
+
contract_digest: contractDigest(contract2),
|
|
1094
|
+
schema_version: RESULT_SCHEMA_VERSION
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
var VERSION, RESULT_SCHEMA_VERSION, digests;
|
|
1098
|
+
var init_identity = __esm({
|
|
1099
|
+
"src/identity.ts"() {
|
|
1100
|
+
"use strict";
|
|
1101
|
+
init_esm_shims();
|
|
1102
|
+
init_package();
|
|
1103
|
+
VERSION = package_default.version;
|
|
1104
|
+
RESULT_SCHEMA_VERSION = 1;
|
|
1105
|
+
digests = /* @__PURE__ */ new WeakMap();
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// src/results.ts
|
|
1110
|
+
function errorEnvelope(error, contract2) {
|
|
1111
|
+
const known = error instanceof CliError ? error : void 0;
|
|
1112
|
+
const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
|
|
1113
|
+
cli_version,
|
|
1114
|
+
schema_version,
|
|
1115
|
+
contract_version,
|
|
1116
|
+
contract_digest
|
|
1117
|
+
}))(identityOf(contract2));
|
|
1118
|
+
return {
|
|
1119
|
+
apiVersion: 1,
|
|
1120
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1121
|
+
code: known?.code ?? "ERR_FAILED",
|
|
1122
|
+
...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
|
|
1123
|
+
identity
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function componentData(detail, dense) {
|
|
1127
|
+
if (!dense) return { ...detail, dense };
|
|
1128
|
+
const { description: _description, ...rest } = detail;
|
|
1129
|
+
return {
|
|
1130
|
+
...rest,
|
|
1131
|
+
props: detail.props.map(
|
|
1132
|
+
(prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
|
|
1133
|
+
),
|
|
1134
|
+
dense
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function instructionsData(contract2) {
|
|
1138
|
+
return {
|
|
1139
|
+
rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
|
|
1140
|
+
id,
|
|
1141
|
+
rule,
|
|
1142
|
+
...wrong === void 0 ? {} : { wrong },
|
|
1143
|
+
...right === void 0 ? {} : { right }
|
|
1144
|
+
})),
|
|
1145
|
+
components: componentIndex(contract2),
|
|
1146
|
+
recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
|
|
1147
|
+
name,
|
|
1148
|
+
description,
|
|
1149
|
+
install: install2
|
|
1150
|
+
}))
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function bundle(root) {
|
|
1154
|
+
const used = {};
|
|
1155
|
+
const visit = (node) => {
|
|
1156
|
+
if (Array.isArray(node)) node.forEach(visit);
|
|
1157
|
+
else if (typeof node === "object" && node !== null) {
|
|
1158
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1159
|
+
if (key === "$ref" && typeof value === "string") {
|
|
1160
|
+
const name = value.replace("#/$defs/", "");
|
|
1161
|
+
if (used[name] === void 0) {
|
|
1162
|
+
used[name] = DEFS[name];
|
|
1163
|
+
visit(DEFS[name]);
|
|
1164
|
+
}
|
|
1165
|
+
} else visit(value);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
visit(root);
|
|
1170
|
+
return { ...root, $defs: used };
|
|
1171
|
+
}
|
|
1172
|
+
function schemaFor(type) {
|
|
1173
|
+
return bundle(type === "error" ? failure() : success(type));
|
|
1174
|
+
}
|
|
1175
|
+
function shareRepeats(schema) {
|
|
1176
|
+
const count = /* @__PURE__ */ new Map();
|
|
1177
|
+
const tally = (node) => {
|
|
1178
|
+
if (Array.isArray(node)) node.forEach(tally);
|
|
1179
|
+
else if (typeof node === "object" && node !== null) {
|
|
1180
|
+
const text2 = JSON.stringify(node);
|
|
1181
|
+
if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
|
|
1182
|
+
Object.values(node).forEach(tally);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
tally(schema);
|
|
1186
|
+
const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
|
|
1187
|
+
if (repeated.length === 0) return schema;
|
|
1188
|
+
const names2 = new Map(
|
|
1189
|
+
repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
|
|
1190
|
+
);
|
|
1191
|
+
const defs = {};
|
|
1192
|
+
const rewrite = (node) => {
|
|
1193
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
1194
|
+
if (typeof node !== "object" || node === null) return node;
|
|
1195
|
+
const name = names2.get(JSON.stringify(node));
|
|
1196
|
+
if (name !== void 0) {
|
|
1197
|
+
defs[name] = node;
|
|
1198
|
+
return { $ref: `#/$defs/${name}` };
|
|
1199
|
+
}
|
|
1200
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
|
|
1201
|
+
};
|
|
1202
|
+
const rewritten = rewrite(schema);
|
|
1203
|
+
return { ...rewritten, $defs: defs };
|
|
1204
|
+
}
|
|
1205
|
+
function outputSchemaFor(type) {
|
|
1206
|
+
return shareRepeats({
|
|
1207
|
+
type: "object",
|
|
1208
|
+
properties: {
|
|
1209
|
+
type: { const: type },
|
|
1210
|
+
// The four versions and the digest; the docs name them, and so does every answer.
|
|
1211
|
+
identity: { type: "object" },
|
|
1212
|
+
data: SUMMARIES[type]
|
|
1213
|
+
},
|
|
1214
|
+
required: ["identity"],
|
|
1215
|
+
anyOf: [{ required: ["data"] }, { required: ["error"] }]
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
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;
|
|
1219
|
+
var init_results = __esm({
|
|
1220
|
+
"src/results.ts"() {
|
|
1221
|
+
"use strict";
|
|
1222
|
+
init_esm_shims();
|
|
1223
|
+
init_check();
|
|
1224
|
+
init_identity();
|
|
1225
|
+
init_lookup();
|
|
1226
|
+
init_types();
|
|
1227
|
+
RESULT_TYPES = [
|
|
1228
|
+
"check",
|
|
1229
|
+
"search",
|
|
1230
|
+
"component.detail",
|
|
1231
|
+
"component.batch",
|
|
1232
|
+
"component.list",
|
|
1233
|
+
"init",
|
|
1234
|
+
"doctor",
|
|
1235
|
+
"add",
|
|
1236
|
+
"list",
|
|
1237
|
+
"instructions"
|
|
1238
|
+
];
|
|
1239
|
+
envelope = (type, data, contract2) => ({
|
|
1240
|
+
apiVersion: 1,
|
|
1241
|
+
type,
|
|
1242
|
+
identity: identityOf(contract2),
|
|
1243
|
+
data
|
|
1244
|
+
});
|
|
1245
|
+
str = (extra = {}) => ({ type: "string", ...extra });
|
|
1246
|
+
int = () => ({ type: "integer", minimum: 0 });
|
|
1247
|
+
bool = () => ({ type: "boolean" });
|
|
1248
|
+
arr = (items) => ({ type: "array", items });
|
|
1249
|
+
names = () => arr(str());
|
|
1250
|
+
words2 = (...values) => ({ type: "string", enum: [...values] });
|
|
1251
|
+
ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
1252
|
+
obj = (properties, optional = [], open = false) => ({
|
|
1253
|
+
type: "object",
|
|
1254
|
+
properties,
|
|
1255
|
+
required: Object.keys(properties).filter((key) => !optional.includes(key)),
|
|
1256
|
+
additionalProperties: open
|
|
1257
|
+
});
|
|
1258
|
+
scalar = { type: ["string", "number", "boolean"] };
|
|
1259
|
+
DEFS = {
|
|
1260
|
+
identity: obj({
|
|
1261
|
+
cli_version: str(),
|
|
1262
|
+
contract_version: str(),
|
|
1263
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
|
|
1264
|
+
schema_version: { type: "integer", minimum: 1 }
|
|
1265
|
+
}),
|
|
1266
|
+
error_identity: obj(
|
|
1267
|
+
{
|
|
1268
|
+
cli_version: str(),
|
|
1269
|
+
schema_version: { type: "integer", minimum: 1 },
|
|
1270
|
+
contract_version: str(),
|
|
1271
|
+
contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
|
|
1272
|
+
},
|
|
1273
|
+
["contract_version", "contract_digest"]
|
|
1274
|
+
),
|
|
1275
|
+
finding: obj(
|
|
1276
|
+
{
|
|
1277
|
+
code: words2(...FINDING_CODES),
|
|
1278
|
+
severity: words2("error", "warning"),
|
|
1279
|
+
file: str(),
|
|
1280
|
+
line: { type: "integer", minimum: 1 },
|
|
1281
|
+
column: { type: "integer", minimum: 1 },
|
|
1282
|
+
component: str(),
|
|
1283
|
+
prop: str(),
|
|
1284
|
+
message: str(),
|
|
1285
|
+
valid_values: names(),
|
|
1286
|
+
suggestions: names()
|
|
1287
|
+
},
|
|
1288
|
+
["component", "prop", "valid_values", "suggestions"]
|
|
1289
|
+
),
|
|
1290
|
+
limitation: obj(
|
|
1291
|
+
{
|
|
1292
|
+
code: words2(...LIMITATION_CODES),
|
|
1293
|
+
file: str(),
|
|
1294
|
+
line: { type: "integer", minimum: 1 },
|
|
1295
|
+
column: { type: "integer", minimum: 1 },
|
|
1296
|
+
component: str(),
|
|
1297
|
+
prop: str(),
|
|
1298
|
+
message: str()
|
|
1299
|
+
},
|
|
1300
|
+
["prop"]
|
|
1301
|
+
),
|
|
1302
|
+
skipped_input: obj({
|
|
1303
|
+
path: str(),
|
|
1304
|
+
reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
|
|
1305
|
+
}),
|
|
1306
|
+
parser: obj({
|
|
1307
|
+
name: words2("typescript"),
|
|
1308
|
+
version: str(),
|
|
1309
|
+
source: words2("project", "installer", "provided")
|
|
1310
|
+
}),
|
|
1311
|
+
check_data: obj({
|
|
1312
|
+
evaluation_status: words2(
|
|
1313
|
+
"evaluated_clean",
|
|
1314
|
+
"evaluated_with_findings",
|
|
1315
|
+
"partially_evaluated",
|
|
1316
|
+
"not_evaluated",
|
|
1317
|
+
"no_files_evaluated"
|
|
1318
|
+
),
|
|
1319
|
+
files: int(),
|
|
1320
|
+
findings: arr(ref("finding")),
|
|
1321
|
+
summary: obj({ errors: int(), warnings: int() }),
|
|
1322
|
+
coverage: obj({
|
|
1323
|
+
files_found: int(),
|
|
1324
|
+
files_evaluated: int(),
|
|
1325
|
+
files_partial: int(),
|
|
1326
|
+
files_not_evaluated: int()
|
|
1327
|
+
}),
|
|
1328
|
+
limitations: arr(ref("limitation")),
|
|
1329
|
+
skipped_inputs: arr(ref("skipped_input")),
|
|
1330
|
+
scope: obj({ checked: names(), not_checked: names() }),
|
|
1331
|
+
parser: ref("parser")
|
|
1332
|
+
}),
|
|
1333
|
+
search_result: obj(
|
|
1334
|
+
{
|
|
1335
|
+
kind: words2("component", "recipe"),
|
|
1336
|
+
name: str(),
|
|
1337
|
+
purpose: str(),
|
|
1338
|
+
score: { type: "number" },
|
|
1339
|
+
why: str(),
|
|
1340
|
+
next: str()
|
|
1341
|
+
},
|
|
1342
|
+
["why"]
|
|
1343
|
+
),
|
|
1344
|
+
search_data: obj({
|
|
1345
|
+
query: str(),
|
|
1346
|
+
limit: { type: "integer", minimum: 1 },
|
|
1347
|
+
matchCount: int(),
|
|
1348
|
+
results: arr(ref("search_result"))
|
|
1349
|
+
}),
|
|
1350
|
+
index_entry: obj({ name: str(), purpose: str(), parts: names() }),
|
|
1351
|
+
component_list_data: obj({ components: arr(ref("index_entry")) }),
|
|
1352
|
+
detail_prop: obj(
|
|
1353
|
+
{
|
|
1354
|
+
name: str(),
|
|
1355
|
+
type: str(),
|
|
1356
|
+
values: names(),
|
|
1357
|
+
types: names(),
|
|
1358
|
+
default: scalar,
|
|
1359
|
+
required: bool(),
|
|
1360
|
+
description: str(),
|
|
1361
|
+
notes: { type: "object", additionalProperties: str() }
|
|
1362
|
+
},
|
|
1363
|
+
["values", "types", "default", "required", "description", "notes"],
|
|
1364
|
+
// The contract may describe a prop with more than these (an array's item type, for one).
|
|
1365
|
+
true
|
|
1366
|
+
),
|
|
1367
|
+
component_detail: obj(
|
|
1368
|
+
{
|
|
1369
|
+
name: str(),
|
|
1370
|
+
description: str(),
|
|
1371
|
+
use_for: names(),
|
|
1372
|
+
not_for: arr(obj({ need: str(), use: str() })),
|
|
1373
|
+
part_of: str(),
|
|
1374
|
+
parts: names(),
|
|
1375
|
+
props: arr(ref("detail_prop")),
|
|
1376
|
+
slots: {
|
|
1377
|
+
type: "object",
|
|
1378
|
+
additionalProperties: obj({ type: str(), required: bool() }, ["required"])
|
|
1379
|
+
},
|
|
1380
|
+
composable_with: names(),
|
|
1381
|
+
never_inside: names(),
|
|
1382
|
+
allowed_children: names(),
|
|
1383
|
+
example: str(),
|
|
1384
|
+
dense: bool()
|
|
1385
|
+
},
|
|
1386
|
+
["description", "part_of", "slots", "allowed_children"]
|
|
1387
|
+
),
|
|
1388
|
+
component_batch_data: obj({ components: arr(ref("component_detail")) }),
|
|
1389
|
+
doctor_check: obj(
|
|
1390
|
+
{ id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
|
|
1391
|
+
["fix"]
|
|
1392
|
+
),
|
|
1393
|
+
doctor_data: obj({
|
|
1394
|
+
consumer_directory: str(),
|
|
1395
|
+
operations: obj({
|
|
1396
|
+
lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
|
|
1397
|
+
check_code: obj(
|
|
1398
|
+
{
|
|
1399
|
+
status: words2("ready", "blocked"),
|
|
1400
|
+
message: str(),
|
|
1401
|
+
reason: words2("typescript_missing", "typescript_unusable"),
|
|
1402
|
+
fix: str(),
|
|
1403
|
+
typescript: obj({
|
|
1404
|
+
version: str(),
|
|
1405
|
+
resolved_from: str(),
|
|
1406
|
+
source: words2("project", "installer", "provided")
|
|
1407
|
+
})
|
|
1408
|
+
},
|
|
1409
|
+
["reason", "fix", "typescript"]
|
|
1410
|
+
)
|
|
1411
|
+
}),
|
|
1412
|
+
checks: arr(ref("doctor_check")),
|
|
1413
|
+
summary: obj({ ok: int(), warn: int(), fail: int() })
|
|
1414
|
+
}),
|
|
1415
|
+
init_data: obj({
|
|
1416
|
+
version: str(),
|
|
1417
|
+
dryRun: bool(),
|
|
1418
|
+
files: arr(
|
|
1419
|
+
obj({
|
|
1420
|
+
path: str(),
|
|
1421
|
+
action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
|
|
1422
|
+
})
|
|
1423
|
+
)
|
|
1424
|
+
}),
|
|
1425
|
+
install_file: obj({
|
|
1426
|
+
path: str(),
|
|
1427
|
+
destination: str(),
|
|
1428
|
+
action: words2("create", "overwrite", "skip")
|
|
1429
|
+
}),
|
|
1430
|
+
add_data: obj({
|
|
1431
|
+
outputDirectory: str(),
|
|
1432
|
+
entries: names(),
|
|
1433
|
+
dependencies: names(),
|
|
1434
|
+
devDependencies: names(),
|
|
1435
|
+
ranges: { type: "object", additionalProperties: str() },
|
|
1436
|
+
files: arr(ref("install_file")),
|
|
1437
|
+
dryRun: bool(),
|
|
1438
|
+
setup: ref("setup")
|
|
1439
|
+
}),
|
|
1440
|
+
setup: obj({
|
|
1441
|
+
tailwind: str(),
|
|
1442
|
+
packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
|
|
1443
|
+
stylesheet: arr(obj({ line: str(), why: str() })),
|
|
1444
|
+
root_import: obj({ line: str(), note: str() }),
|
|
1445
|
+
not_needed: arr(obj({ name: str(), why: str() }))
|
|
1446
|
+
}),
|
|
1447
|
+
list_data: obj({
|
|
1448
|
+
version: str(),
|
|
1449
|
+
entries: arr(
|
|
1450
|
+
obj(
|
|
1451
|
+
{
|
|
1452
|
+
name: str(),
|
|
1453
|
+
slug: str(),
|
|
1454
|
+
kind: words2("component", "recipe"),
|
|
1455
|
+
description: str(),
|
|
1456
|
+
files: names(),
|
|
1457
|
+
dependencies: names(),
|
|
1458
|
+
devDependencies: names()
|
|
1459
|
+
},
|
|
1460
|
+
["devDependencies"]
|
|
1461
|
+
)
|
|
1462
|
+
),
|
|
1463
|
+
ranges: { type: "object", additionalProperties: str() }
|
|
1464
|
+
}),
|
|
1465
|
+
instructions_data: obj({
|
|
1466
|
+
rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
|
|
1467
|
+
components: arr(ref("index_entry")),
|
|
1468
|
+
recipes: arr(obj({ name: str(), description: str(), install: str() }))
|
|
1469
|
+
})
|
|
1470
|
+
};
|
|
1471
|
+
DATA = {
|
|
1472
|
+
check: "check_data",
|
|
1473
|
+
search: "search_data",
|
|
1474
|
+
"component.detail": "component_detail",
|
|
1475
|
+
"component.batch": "component_batch_data",
|
|
1476
|
+
"component.list": "component_list_data",
|
|
1477
|
+
init: "init_data",
|
|
1478
|
+
doctor: "doctor_data",
|
|
1479
|
+
add: "add_data",
|
|
1480
|
+
list: "list_data",
|
|
1481
|
+
instructions: "instructions_data"
|
|
1482
|
+
};
|
|
1483
|
+
success = (type) => obj({
|
|
1484
|
+
apiVersion: { const: 1 },
|
|
1485
|
+
type: { const: type },
|
|
1486
|
+
identity: ref("identity"),
|
|
1487
|
+
data: ref(DATA[type])
|
|
1488
|
+
});
|
|
1489
|
+
failure = () => obj(
|
|
1490
|
+
{
|
|
1491
|
+
apiVersion: { const: 1 },
|
|
1492
|
+
error: str(),
|
|
1493
|
+
code: str({ pattern: "^ERR_[A-Z_]+$" }),
|
|
1494
|
+
suggestions: names(),
|
|
1495
|
+
identity: ref("error_identity")
|
|
1496
|
+
},
|
|
1497
|
+
["suggestions"]
|
|
1498
|
+
);
|
|
1499
|
+
S = { type: "string" };
|
|
1500
|
+
I = { type: "integer" };
|
|
1501
|
+
B = { type: "boolean" };
|
|
1502
|
+
list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
|
|
1503
|
+
shape = (properties, required) => ({
|
|
1504
|
+
type: "object",
|
|
1505
|
+
properties,
|
|
1506
|
+
...required === void 0 ? {} : { required }
|
|
1507
|
+
});
|
|
1508
|
+
strings = list(S);
|
|
1509
|
+
SUMMARIES = {
|
|
1510
|
+
check: shape(
|
|
1511
|
+
{
|
|
1512
|
+
evaluation_status: words2(
|
|
1513
|
+
"evaluated_clean",
|
|
1514
|
+
"evaluated_with_findings",
|
|
1515
|
+
"partially_evaluated",
|
|
1516
|
+
"not_evaluated",
|
|
1517
|
+
"no_files_evaluated"
|
|
1518
|
+
),
|
|
1519
|
+
findings: list(
|
|
1520
|
+
shape({
|
|
1521
|
+
code: S,
|
|
1522
|
+
severity: words2("error", "warning"),
|
|
1523
|
+
file: S,
|
|
1524
|
+
line: I,
|
|
1525
|
+
column: I,
|
|
1526
|
+
message: S
|
|
1527
|
+
})
|
|
1528
|
+
),
|
|
1529
|
+
summary: shape({ errors: I, warnings: I }),
|
|
1530
|
+
coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
|
|
1531
|
+
limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
|
|
1532
|
+
skipped_inputs: list(),
|
|
1533
|
+
scope: { type: "object" },
|
|
1534
|
+
parser: { type: "object" }
|
|
1535
|
+
},
|
|
1536
|
+
["evaluation_status", "findings", "summary", "coverage", "limitations"]
|
|
1537
|
+
),
|
|
1538
|
+
search: shape(
|
|
1539
|
+
{
|
|
1540
|
+
matchCount: I,
|
|
1541
|
+
results: list(
|
|
1542
|
+
shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
|
|
1543
|
+
)
|
|
1544
|
+
},
|
|
1545
|
+
["results"]
|
|
1546
|
+
),
|
|
1547
|
+
"component.detail": shape(
|
|
1548
|
+
{
|
|
1549
|
+
name: S,
|
|
1550
|
+
description: S,
|
|
1551
|
+
use_for: strings,
|
|
1552
|
+
not_for: list(),
|
|
1553
|
+
part_of: S,
|
|
1554
|
+
parts: strings,
|
|
1555
|
+
props: list(
|
|
1556
|
+
shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
|
|
1557
|
+
),
|
|
1558
|
+
slots: { type: "object" },
|
|
1559
|
+
composable_with: strings,
|
|
1560
|
+
never_inside: strings,
|
|
1561
|
+
allowed_children: strings,
|
|
1562
|
+
example: S,
|
|
1563
|
+
dense: B
|
|
1564
|
+
},
|
|
1565
|
+
["name", "props", "example", "dense"]
|
|
1566
|
+
),
|
|
1567
|
+
"component.list": shape({ components: list() }, ["components"]),
|
|
1568
|
+
"component.batch": shape({ components: list() }, ["components"]),
|
|
1569
|
+
instructions: shape(
|
|
1570
|
+
{
|
|
1571
|
+
rules: list(),
|
|
1572
|
+
components: list(),
|
|
1573
|
+
recipes: list()
|
|
1574
|
+
},
|
|
1575
|
+
["rules", "components", "recipes"]
|
|
1576
|
+
),
|
|
1577
|
+
init: shape({ files: list() }),
|
|
1578
|
+
doctor: shape({ operations: { type: "object" }, checks: list() }),
|
|
1579
|
+
add: shape({ files: list(), setup: { type: "object" } }),
|
|
1580
|
+
list: shape({ entries: list() })
|
|
1581
|
+
};
|
|
1582
|
+
RESULT_SCHEMAS = Object.freeze(
|
|
1583
|
+
Object.fromEntries(
|
|
1584
|
+
[...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
|
|
1585
|
+
)
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
// src/contract.ts
|
|
1591
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1592
|
+
import { dirname, join as join3 } from "path";
|
|
1593
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1594
|
+
function loadContract() {
|
|
1595
|
+
const here = dirname(fileURLToPath3(import.meta.url));
|
|
1596
|
+
const candidates = [join3(here, "contract.json"), join3(here, "../generated/contract.json")];
|
|
1597
|
+
const found = candidates.find((candidate) => existsSync2(candidate));
|
|
1598
|
+
if (found === void 0) {
|
|
1599
|
+
throw new Error(
|
|
1600
|
+
`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.`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
return JSON.parse(readFileSync2(found, "utf8"));
|
|
1604
|
+
}
|
|
1605
|
+
var init_contract = __esm({
|
|
1606
|
+
"src/contract.ts"() {
|
|
1607
|
+
"use strict";
|
|
1608
|
+
init_esm_shims();
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// src/installer.ts
|
|
1613
|
+
import {
|
|
1614
|
+
constants,
|
|
1615
|
+
closeSync,
|
|
1616
|
+
lstatSync,
|
|
1617
|
+
mkdirSync,
|
|
1618
|
+
openSync,
|
|
1619
|
+
readFileSync as readFileSync3,
|
|
1620
|
+
realpathSync,
|
|
1621
|
+
writeFileSync
|
|
1622
|
+
} from "fs";
|
|
1623
|
+
import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
1624
|
+
function resolveEntries(names2, registry2) {
|
|
1625
|
+
if (!names2.length) throw new CliError("add requires one or more names.", 2);
|
|
1626
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (const name of names2) {
|
|
1628
|
+
const normalized = name.toLowerCase();
|
|
1629
|
+
const entry = registry2.entries.find(
|
|
1630
|
+
(candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
|
|
1631
|
+
);
|
|
1632
|
+
if (!entry)
|
|
1633
|
+
throw new CliError(`Unknown component or recipe ${JSON.stringify(name)}. Run oprt list.`, 2);
|
|
1634
|
+
selected.set(entry.name, entry);
|
|
1635
|
+
}
|
|
1636
|
+
return [...selected.values()];
|
|
1637
|
+
}
|
|
1638
|
+
function validateRelative(path2) {
|
|
1639
|
+
if (!path2 || isAbsolute(path2) || /[\\:\0]/.test(path2) || path2.split("/").includes(".."))
|
|
1640
|
+
throw new CliError(
|
|
1641
|
+
`Unsafe path ${JSON.stringify(path2)}: use a nonempty relative path without traversal.`,
|
|
1642
|
+
2
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
function stat(path2) {
|
|
1646
|
+
try {
|
|
1647
|
+
return lstatSync(path2);
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
if (error.code === "ENOENT") return void 0;
|
|
1650
|
+
throw error;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function inspectPath(root, destination, directory = false) {
|
|
1654
|
+
const path2 = relative2(root, destination);
|
|
1655
|
+
if (path2 === ".." || path2.startsWith(`..${sep2}`) || isAbsolute(path2))
|
|
1656
|
+
throw new CliError("Output path escapes the working directory.");
|
|
1657
|
+
let cursor = root;
|
|
1658
|
+
const segments = path2 ? path2.split(sep2) : [];
|
|
1659
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1660
|
+
cursor = resolve2(cursor, segments[i]);
|
|
1661
|
+
const info = stat(cursor);
|
|
1662
|
+
if (!info) continue;
|
|
1663
|
+
if (info.isSymbolicLink()) throw new CliError(`Refusing symlink in output path: ${cursor}`);
|
|
1664
|
+
const needsDirectory = i < segments.length - 1 || directory;
|
|
1665
|
+
if (needsDirectory && !info.isDirectory())
|
|
1666
|
+
throw new CliError(`Output ancestor is not a directory: ${cursor}`);
|
|
1667
|
+
if (!needsDirectory && !info.isFile())
|
|
1668
|
+
throw new CliError(`Output target is not a regular file: ${cursor}`);
|
|
1669
|
+
}
|
|
1670
|
+
return stat(destination);
|
|
1671
|
+
}
|
|
1672
|
+
function installSources(options, context) {
|
|
1673
|
+
const entries = resolveEntries(options.names, context.registry);
|
|
1674
|
+
const out = options.out ?? DEFAULT_OUT;
|
|
1675
|
+
validateRelative(out);
|
|
1676
|
+
const root = realpathSync(context.cwd);
|
|
1677
|
+
const outputDirectory = resolve2(root, out);
|
|
1678
|
+
inspectPath(root, outputDirectory, true);
|
|
1679
|
+
const paths = [...new Set(entries.flatMap((entry) => entry.files))].sort();
|
|
1680
|
+
const files = paths.map((path2) => {
|
|
1681
|
+
validateRelative(path2);
|
|
1682
|
+
const content = context.registry.files[path2];
|
|
1683
|
+
if (!Object.hasOwn(context.registry.files, path2) || typeof content !== "string")
|
|
1684
|
+
throw new CliError(`Registry is missing source ${path2}.`);
|
|
1685
|
+
const destination = resolve2(outputDirectory, path2);
|
|
1686
|
+
const info = inspectPath(root, destination);
|
|
1687
|
+
if (!info) return { path: path2, destination, action: "create" };
|
|
1688
|
+
if (readFileSync3(destination, "utf8") === content) return { path: path2, destination, action: "skip" };
|
|
1689
|
+
if (!options.overwrite)
|
|
1690
|
+
throw new CliError(
|
|
1691
|
+
`Refusing to overwrite modified file ${destination}. Use --overwrite to replace it explicitly.`
|
|
1692
|
+
);
|
|
1693
|
+
return { path: path2, destination, action: "overwrite" };
|
|
1694
|
+
});
|
|
1695
|
+
const dependencies = [...new Set(entries.flatMap((entry) => entry.dependencies))].sort();
|
|
1696
|
+
const runtime = new Set(
|
|
1697
|
+
entries.flatMap(
|
|
1698
|
+
(entry) => entry.dependencies.filter((name) => !(entry.devDependencies ?? []).includes(name))
|
|
909
1699
|
)
|
|
910
1700
|
);
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1701
|
+
const result = {
|
|
1702
|
+
outputDirectory,
|
|
1703
|
+
entries: entries.map((entry) => entry.name),
|
|
1704
|
+
dependencies,
|
|
1705
|
+
devDependencies: dependencies.filter(
|
|
1706
|
+
(name) => !runtime.has(name) && entries.some((entry) => (entry.devDependencies ?? []).includes(name))
|
|
1707
|
+
),
|
|
1708
|
+
ranges: Object.fromEntries(
|
|
1709
|
+
dependencies.flatMap((name) => {
|
|
1710
|
+
const range = context.registry.ranges?.[name];
|
|
1711
|
+
return range === void 0 ? [] : [[name, range]];
|
|
1712
|
+
})
|
|
1713
|
+
),
|
|
1714
|
+
files,
|
|
1715
|
+
dryRun: options.dryRun ?? false
|
|
918
1716
|
};
|
|
1717
|
+
if (result.dryRun) return result;
|
|
1718
|
+
for (const file of files) {
|
|
1719
|
+
if (file.action === "skip") continue;
|
|
1720
|
+
inspectPath(root, file.destination);
|
|
1721
|
+
const parent = dirname2(file.destination);
|
|
1722
|
+
mkdirSync(parent, { recursive: true });
|
|
1723
|
+
inspectPath(root, parent, true);
|
|
1724
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (file.action === "create" ? constants.O_EXCL : constants.O_TRUNC);
|
|
1725
|
+
const descriptor = openSync(file.destination, flags, 420);
|
|
1726
|
+
try {
|
|
1727
|
+
writeFileSync(descriptor, context.registry.files[file.path]);
|
|
1728
|
+
} finally {
|
|
1729
|
+
closeSync(descriptor);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return result;
|
|
919
1733
|
}
|
|
920
|
-
var
|
|
921
|
-
|
|
922
|
-
"src/check.ts"() {
|
|
1734
|
+
var init_installer = __esm({
|
|
1735
|
+
"src/installer.ts"() {
|
|
923
1736
|
"use strict";
|
|
924
1737
|
init_esm_shims();
|
|
925
|
-
|
|
1738
|
+
init_args();
|
|
926
1739
|
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
1740
|
}
|
|
958
1741
|
});
|
|
959
1742
|
|
|
@@ -1039,16 +1822,20 @@ var init_agent_block = __esm({
|
|
|
1039
1822
|
|
|
1040
1823
|
// src/doctor.ts
|
|
1041
1824
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
1042
|
-
import { dirname as dirname3, join as
|
|
1825
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1043
1826
|
function installedVersion(cwd, name) {
|
|
1044
1827
|
for (let folder = cwd; ; folder = dirname3(folder)) {
|
|
1045
|
-
const manifest =
|
|
1828
|
+
const manifest = join4(folder, "node_modules", name, "package.json");
|
|
1046
1829
|
if (existsSync3(manifest))
|
|
1047
1830
|
return JSON.parse(readFileSync4(manifest, "utf8")).version;
|
|
1048
1831
|
if (dirname3(folder) === folder) return void 0;
|
|
1049
1832
|
}
|
|
1050
1833
|
}
|
|
1051
|
-
function doctor({
|
|
1834
|
+
function doctor({
|
|
1835
|
+
cwd,
|
|
1836
|
+
contract: contract2,
|
|
1837
|
+
parser = probeTypeScript(cwd)
|
|
1838
|
+
}) {
|
|
1052
1839
|
const checks = [];
|
|
1053
1840
|
for (const name of PACKAGES) {
|
|
1054
1841
|
const version = installedVersion(cwd, name);
|
|
@@ -1070,7 +1857,7 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1070
1857
|
else checks.push({ id, status: "ok", message: `${name} ${version} is installed.` });
|
|
1071
1858
|
}
|
|
1072
1859
|
const written = INSTRUCTION_FILES.map((path2) => {
|
|
1073
|
-
const file =
|
|
1860
|
+
const file = join4(cwd, path2);
|
|
1074
1861
|
return {
|
|
1075
1862
|
path: path2,
|
|
1076
1863
|
version: existsSync3(file) ? blockVersion(readFileSync4(file, "utf8")) : void 0
|
|
@@ -1097,8 +1884,55 @@ function doctor({ cwd, contract: contract2 }) {
|
|
|
1097
1884
|
status: "ok",
|
|
1098
1885
|
message: `${written.map(({ path: path2 }) => path2).join(", ")} carries the OPERATOR block (${contract2.version}).`
|
|
1099
1886
|
});
|
|
1887
|
+
const install2 = `${INSTALL_TYPESCRIPT} (or your package manager's equivalent), then run oprt doctor again. Lookups work without it.`;
|
|
1888
|
+
let checkCode;
|
|
1889
|
+
if (parser.ready) {
|
|
1890
|
+
const { version, resolved_from, source } = parser.resolution;
|
|
1891
|
+
checkCode = {
|
|
1892
|
+
status: "ready",
|
|
1893
|
+
message: `TypeScript ${version}, resolved from ${resolved_from}${source === "project" ? "" : " (beside oprt, not this project)"}.`,
|
|
1894
|
+
typescript: { version, resolved_from, source }
|
|
1895
|
+
};
|
|
1896
|
+
checks.push(
|
|
1897
|
+
source === "project" ? {
|
|
1898
|
+
id: "typescript",
|
|
1899
|
+
status: "ok",
|
|
1900
|
+
message: `TypeScript ${version} is installed in this project (${resolved_from}).`
|
|
1901
|
+
} : {
|
|
1902
|
+
id: "typescript",
|
|
1903
|
+
status: "warn",
|
|
1904
|
+
message: `TypeScript ${version} is not in this project; the one beside oprt is used (${resolved_from}).`,
|
|
1905
|
+
fix: `${INSTALL_TYPESCRIPT}, so checking reads code with a version installed in the project.`
|
|
1906
|
+
}
|
|
1907
|
+
);
|
|
1908
|
+
} else {
|
|
1909
|
+
checkCode = {
|
|
1910
|
+
status: "blocked",
|
|
1911
|
+
message: `${parser.reason}: ${parser.detail}`,
|
|
1912
|
+
reason: parser.reason,
|
|
1913
|
+
fix: install2
|
|
1914
|
+
};
|
|
1915
|
+
checks.push({
|
|
1916
|
+
id: "typescript",
|
|
1917
|
+
status: "fail",
|
|
1918
|
+
message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
|
|
1919
|
+
fix: install2
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1100
1922
|
const count = (status) => checks.filter((check) => check.status === status).length;
|
|
1101
|
-
return {
|
|
1923
|
+
return {
|
|
1924
|
+
consumer_directory: cwd,
|
|
1925
|
+
operations: {
|
|
1926
|
+
lookup: {
|
|
1927
|
+
status: "ready",
|
|
1928
|
+
message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
|
|
1929
|
+
contract_version: contract2.version
|
|
1930
|
+
},
|
|
1931
|
+
check_code: checkCode
|
|
1932
|
+
},
|
|
1933
|
+
checks,
|
|
1934
|
+
summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
|
|
1935
|
+
};
|
|
1102
1936
|
}
|
|
1103
1937
|
var PACKAGES, INSTRUCTION_FILES, minor;
|
|
1104
1938
|
var init_doctor = __esm({
|
|
@@ -1106,6 +1940,7 @@ var init_doctor = __esm({
|
|
|
1106
1940
|
"use strict";
|
|
1107
1941
|
init_esm_shims();
|
|
1108
1942
|
init_agent_block();
|
|
1943
|
+
init_parser();
|
|
1109
1944
|
PACKAGES = ["@oprt/react", "@oprt/tokens", "@oprt/tailwind"];
|
|
1110
1945
|
INSTRUCTION_FILES = [
|
|
1111
1946
|
"AGENTS.md",
|
|
@@ -1182,7 +2017,72 @@ var init_init = __esm({
|
|
|
1182
2017
|
}
|
|
1183
2018
|
});
|
|
1184
2019
|
|
|
2020
|
+
// src/setup.ts
|
|
2021
|
+
function setupFor(dependencies, contract2) {
|
|
2022
|
+
const { setup } = contract2.guidance;
|
|
2023
|
+
const scansPackage = dependencies.includes(setup.package_source.when);
|
|
2024
|
+
return {
|
|
2025
|
+
tailwind: setup.tailwind,
|
|
2026
|
+
packages: setup.packages.map(({ name, range, dev, why }) => ({
|
|
2027
|
+
name,
|
|
2028
|
+
range: range ?? "latest",
|
|
2029
|
+
dev,
|
|
2030
|
+
why
|
|
2031
|
+
})),
|
|
2032
|
+
stylesheet: [
|
|
2033
|
+
...setup.stylesheet,
|
|
2034
|
+
...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
|
|
2035
|
+
],
|
|
2036
|
+
root_import: { line: setup.root_import, note: setup.root_import_note },
|
|
2037
|
+
not_needed: setup.not_needed
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function renderSetup(setup, copiedPackageImports) {
|
|
2041
|
+
const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
|
|
2042
|
+
const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
|
|
2043
|
+
const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
|
|
2044
|
+
return [
|
|
2045
|
+
`Setup, once per project: ${setup.tailwind}`,
|
|
2046
|
+
` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
|
|
2047
|
+
" In the stylesheet that imports tailwindcss, after the import:",
|
|
2048
|
+
...setup.stylesheet.map(({ line }) => ` ${line}`),
|
|
2049
|
+
...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
|
|
2050
|
+
" (source copied into the project is scanned by Tailwind 4 without an @source line)"
|
|
2051
|
+
],
|
|
2052
|
+
` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
|
|
2053
|
+
...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
|
|
2054
|
+
];
|
|
2055
|
+
}
|
|
2056
|
+
var init_setup = __esm({
|
|
2057
|
+
"src/setup.ts"() {
|
|
2058
|
+
"use strict";
|
|
2059
|
+
init_esm_shims();
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
|
|
1185
2063
|
// src/agent-commands.ts
|
|
2064
|
+
function renderInstall(result, setup) {
|
|
2065
|
+
const lines = [
|
|
2066
|
+
`${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
|
|
2067
|
+
`Output: ${result.outputDirectory}`,
|
|
2068
|
+
...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
|
|
2069
|
+
];
|
|
2070
|
+
const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
|
|
2071
|
+
const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
|
|
2072
|
+
lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
|
|
2073
|
+
if (result.devDependencies.length > 0)
|
|
2074
|
+
lines.push(
|
|
2075
|
+
`Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
|
|
2076
|
+
);
|
|
2077
|
+
lines.push(
|
|
2078
|
+
"",
|
|
2079
|
+
...renderSetup(setup, setup.stylesheet.length > 1),
|
|
2080
|
+
"",
|
|
2081
|
+
"Install what is missing yourself. No package configuration or stylesheet was changed."
|
|
2082
|
+
);
|
|
2083
|
+
return `${lines.join("\n")}
|
|
2084
|
+
`;
|
|
2085
|
+
}
|
|
1186
2086
|
function runAgentCommand(command, context) {
|
|
1187
2087
|
const { contract: contract2, registry: registry2, cwd } = context;
|
|
1188
2088
|
switch (command.command) {
|
|
@@ -1194,14 +2094,15 @@ function runAgentCommand(command, context) {
|
|
|
1194
2094
|
({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
|
|
1195
2095
|
`
|
|
1196
2096
|
).join(""),
|
|
1197
|
-
json: envelope("component.list", { components }),
|
|
2097
|
+
json: envelope("component.list", { components }, contract2),
|
|
1198
2098
|
exitCode: 0
|
|
1199
2099
|
};
|
|
1200
2100
|
}
|
|
1201
2101
|
const details = command.names.map((name) => componentDetail(contract2, name));
|
|
2102
|
+
const data = details.map((detail) => componentData(detail, command.dense));
|
|
1202
2103
|
return {
|
|
1203
2104
|
text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
|
|
1204
|
-
json:
|
|
2105
|
+
json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
|
|
1205
2106
|
exitCode: 0
|
|
1206
2107
|
};
|
|
1207
2108
|
}
|
|
@@ -1214,7 +2115,11 @@ function runAgentCommand(command, context) {
|
|
|
1214
2115
|
${next}
|
|
1215
2116
|
`
|
|
1216
2117
|
).join(""),
|
|
1217
|
-
json: envelope(
|
|
2118
|
+
json: envelope(
|
|
2119
|
+
"search",
|
|
2120
|
+
{ query: command.query, limit: command.limit, matchCount: results.length, results },
|
|
2121
|
+
contract2
|
|
2122
|
+
),
|
|
1218
2123
|
exitCode: 0
|
|
1219
2124
|
};
|
|
1220
2125
|
}
|
|
@@ -1225,55 +2130,195 @@ function runAgentCommand(command, context) {
|
|
|
1225
2130
|
...result.files.map(({ path: path2, action }) => ` ${action.padEnd(9)} ${path2}`),
|
|
1226
2131
|
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
2132
|
];
|
|
1228
|
-
return {
|
|
1229
|
-
|
|
2133
|
+
return {
|
|
2134
|
+
text: `${lines.join("\n")}
|
|
2135
|
+
`,
|
|
2136
|
+
json: envelope("init", result, contract2),
|
|
2137
|
+
exitCode: 0
|
|
2138
|
+
};
|
|
1230
2139
|
}
|
|
1231
2140
|
case "check": {
|
|
1232
2141
|
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
2142
|
return {
|
|
1242
|
-
text:
|
|
1243
|
-
|
|
1244
|
-
json: envelope("check", report),
|
|
2143
|
+
text: renderCheckText(report),
|
|
2144
|
+
json: envelope("check", report, contract2),
|
|
1245
2145
|
// Warnings fail the run only when asked to: they are house rules, or may be native attributes.
|
|
1246
|
-
exitCode:
|
|
2146
|
+
exitCode: checkExitCode(report, { strict: command.strict })
|
|
1247
2147
|
};
|
|
1248
2148
|
}
|
|
1249
2149
|
case "doctor": {
|
|
1250
|
-
const report = doctor({
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
...
|
|
1254
|
-
|
|
2150
|
+
const report = doctor({
|
|
2151
|
+
cwd,
|
|
2152
|
+
contract: contract2,
|
|
2153
|
+
...context.parser ? { parser: context.parser } : {}
|
|
2154
|
+
});
|
|
2155
|
+
const { lookup, check_code: checking } = report.operations;
|
|
2156
|
+
const lines = [
|
|
2157
|
+
"Readiness",
|
|
2158
|
+
` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
|
|
2159
|
+
` code checking ${checking.status.padEnd(8)} ${checking.message}`,
|
|
2160
|
+
...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
|
|
2161
|
+
"",
|
|
2162
|
+
"Setup"
|
|
2163
|
+
];
|
|
2164
|
+
lines.push(
|
|
2165
|
+
...report.checks.flatMap(({ status, message, fix }) => [
|
|
2166
|
+
`[${status}] ${message}`,
|
|
2167
|
+
...fix === void 0 ? [] : [` fix: ${fix}`]
|
|
2168
|
+
])
|
|
2169
|
+
);
|
|
1255
2170
|
lines.push(
|
|
1256
2171
|
`${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
|
|
1257
2172
|
);
|
|
1258
2173
|
return {
|
|
1259
2174
|
text: `${lines.join("\n")}
|
|
1260
2175
|
`,
|
|
1261
|
-
json: envelope("doctor", report),
|
|
2176
|
+
json: envelope("doctor", report, contract2),
|
|
1262
2177
|
exitCode: report.summary.fail > 0 ? 1 : 0
|
|
1263
2178
|
};
|
|
1264
2179
|
}
|
|
2180
|
+
case "list": {
|
|
2181
|
+
const lines = [`OPERATOR ${VERSION}`, ""];
|
|
2182
|
+
for (const kind of ["component", "recipe"]) {
|
|
2183
|
+
const entries = registry2.entries.filter((entry) => entry.kind === kind);
|
|
2184
|
+
lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
|
|
2185
|
+
for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
|
|
2186
|
+
lines.push("");
|
|
2187
|
+
}
|
|
2188
|
+
return {
|
|
2189
|
+
text: `${lines.join("\n")}
|
|
2190
|
+
`,
|
|
2191
|
+
json: envelope(
|
|
2192
|
+
"list",
|
|
2193
|
+
{ version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
|
|
2194
|
+
contract2
|
|
2195
|
+
),
|
|
2196
|
+
exitCode: 0
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
case "add": {
|
|
2200
|
+
const result = installSources(command, { cwd, registry: registry2 });
|
|
2201
|
+
const setup = setupFor(result.dependencies, contract2);
|
|
2202
|
+
return {
|
|
2203
|
+
text: renderInstall(result, setup),
|
|
2204
|
+
json: envelope("add", { ...result, setup }, contract2),
|
|
2205
|
+
exitCode: 0
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
1265
2208
|
}
|
|
1266
2209
|
}
|
|
1267
|
-
var envelope;
|
|
1268
2210
|
var init_agent_commands = __esm({
|
|
1269
2211
|
"src/agent-commands.ts"() {
|
|
1270
2212
|
"use strict";
|
|
1271
2213
|
init_esm_shims();
|
|
1272
2214
|
init_check();
|
|
1273
2215
|
init_doctor();
|
|
2216
|
+
init_identity();
|
|
1274
2217
|
init_init();
|
|
2218
|
+
init_installer();
|
|
1275
2219
|
init_lookup();
|
|
1276
|
-
|
|
2220
|
+
init_results();
|
|
2221
|
+
init_setup();
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
// src/validate.ts
|
|
2226
|
+
function describeValue(value) {
|
|
2227
|
+
if (typeof value === "string")
|
|
2228
|
+
return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
|
|
2229
|
+
if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
|
|
2230
|
+
if (value === null || typeof value === "boolean" || value === void 0) return String(value);
|
|
2231
|
+
return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
|
|
2232
|
+
}
|
|
2233
|
+
function expectation(schema) {
|
|
2234
|
+
switch (schema.type) {
|
|
2235
|
+
case "boolean":
|
|
2236
|
+
return "true or false";
|
|
2237
|
+
case "integer":
|
|
2238
|
+
case "number": {
|
|
2239
|
+
const noun = schema.type === "integer" ? "a whole number" : "a number";
|
|
2240
|
+
if (schema.minimum !== void 0 && schema.maximum !== void 0)
|
|
2241
|
+
return `${noun} from ${schema.minimum} to ${schema.maximum}`;
|
|
2242
|
+
if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
|
|
2243
|
+
if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
|
|
2244
|
+
return noun;
|
|
2245
|
+
}
|
|
2246
|
+
case "string": {
|
|
2247
|
+
if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
|
|
2248
|
+
const { minLength, maxLength } = schema;
|
|
2249
|
+
if (minLength !== void 0 && maxLength !== void 0)
|
|
2250
|
+
return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
|
|
2251
|
+
if (maxLength !== void 0)
|
|
2252
|
+
return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
|
|
2253
|
+
return "a string";
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
function accepts(schema, value) {
|
|
2258
|
+
switch (schema.type) {
|
|
2259
|
+
case "boolean":
|
|
2260
|
+
return typeof value === "boolean";
|
|
2261
|
+
case "string":
|
|
2262
|
+
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));
|
|
2263
|
+
case "integer":
|
|
2264
|
+
case "number":
|
|
2265
|
+
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);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function validateArguments(tool, schema, args) {
|
|
2269
|
+
for (const [field, property] of Object.entries(schema.properties))
|
|
2270
|
+
for (const key of Object.keys(property))
|
|
2271
|
+
if (!KNOWN.has(key))
|
|
2272
|
+
throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
|
|
2273
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
2274
|
+
return `${tool}: arguments must be an object.`;
|
|
2275
|
+
const given = args;
|
|
2276
|
+
const required = schema.required ?? [];
|
|
2277
|
+
const blank = (value) => typeof value === "string" && value.trim() === "";
|
|
2278
|
+
const missing = [];
|
|
2279
|
+
const wrong = [];
|
|
2280
|
+
const unknown = [];
|
|
2281
|
+
for (const field of required)
|
|
2282
|
+
if (given[field] === void 0 || blank(given[field])) missing.push(field);
|
|
2283
|
+
for (const [field, value] of Object.entries(given)) {
|
|
2284
|
+
const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
|
|
2285
|
+
if (property === void 0) unknown.push(field);
|
|
2286
|
+
else if (value !== void 0 && !(required.includes(field) && blank(value))) {
|
|
2287
|
+
if (!accepts(property, value))
|
|
2288
|
+
wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const reject = schema.additionalProperties === false ? unknown : [];
|
|
2292
|
+
const names2 = Object.keys(schema.properties);
|
|
2293
|
+
if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
|
|
2294
|
+
return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
|
|
2295
|
+
const fragments = [
|
|
2296
|
+
...missing.map(
|
|
2297
|
+
(field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
|
|
2298
|
+
),
|
|
2299
|
+
...wrong,
|
|
2300
|
+
...reject.map(
|
|
2301
|
+
(field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
|
|
2302
|
+
)
|
|
2303
|
+
];
|
|
2304
|
+
return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
|
|
2305
|
+
}
|
|
2306
|
+
var KNOWN, typeName;
|
|
2307
|
+
var init_validate = __esm({
|
|
2308
|
+
"src/validate.ts"() {
|
|
2309
|
+
"use strict";
|
|
2310
|
+
init_esm_shims();
|
|
2311
|
+
KNOWN = /* @__PURE__ */ new Set([
|
|
2312
|
+
"type",
|
|
2313
|
+
"description",
|
|
2314
|
+
"default",
|
|
2315
|
+
"enum",
|
|
2316
|
+
"minimum",
|
|
2317
|
+
"maximum",
|
|
2318
|
+
"minLength",
|
|
2319
|
+
"maxLength"
|
|
2320
|
+
]);
|
|
2321
|
+
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
2322
|
}
|
|
1278
2323
|
});
|
|
1279
2324
|
|
|
@@ -1281,75 +2326,88 @@ var init_agent_commands = __esm({
|
|
|
1281
2326
|
import { createInterface } from "readline";
|
|
1282
2327
|
function runTool(name, args, context) {
|
|
1283
2328
|
const { contract: contract2, registry: registry2 } = context;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
return value;
|
|
1289
|
-
};
|
|
2329
|
+
const definition = TOOLS.find((tool) => tool.name === name);
|
|
2330
|
+
if (definition === void 0) throw new UnknownTool(name);
|
|
2331
|
+
const problem = validateArguments(name, definition.inputSchema, args);
|
|
2332
|
+
if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
|
|
1290
2333
|
switch (name) {
|
|
1291
2334
|
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}]`}
|
|
2335
|
+
const query = args["query"];
|
|
2336
|
+
const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
|
|
2337
|
+
const results = search(contract2, registry2, query, limit);
|
|
2338
|
+
return {
|
|
2339
|
+
text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
|
|
2340
|
+
({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
|
|
1301
2341
|
${next}`
|
|
1302
|
-
|
|
2342
|
+
).join("\n"),
|
|
2343
|
+
structured: envelope(
|
|
2344
|
+
"search",
|
|
2345
|
+
{ query, limit, matchCount: results.length, results },
|
|
2346
|
+
contract2
|
|
2347
|
+
)
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
case "get_component": {
|
|
2351
|
+
const dense = args["dense"] ?? true;
|
|
2352
|
+
const detail = componentDetail(contract2, args["name"]);
|
|
2353
|
+
return {
|
|
2354
|
+
text: renderComponent(detail, { dense }),
|
|
2355
|
+
structured: envelope("component.detail", componentData(detail, dense), contract2)
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
case "list_components": {
|
|
2359
|
+
const components = componentIndex(contract2);
|
|
2360
|
+
return {
|
|
2361
|
+
text: components.map(
|
|
2362
|
+
({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
|
|
2363
|
+
).join("\n"),
|
|
2364
|
+
structured: envelope("component.list", { components }, contract2)
|
|
2365
|
+
};
|
|
1303
2366
|
}
|
|
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
2367
|
case "get_instructions":
|
|
1313
|
-
return
|
|
2368
|
+
return {
|
|
2369
|
+
text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
|
|
2370
|
+
structured: envelope("instructions", instructionsData(contract2), contract2)
|
|
2371
|
+
};
|
|
1314
2372
|
case "check_code": {
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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");
|
|
2373
|
+
const { typescript, parser } = resolveParser(
|
|
2374
|
+
context.cwd ?? process.cwd(),
|
|
2375
|
+
context.typescript,
|
|
2376
|
+
"check_code"
|
|
2377
|
+
);
|
|
2378
|
+
const report = buildCheckReport(
|
|
2379
|
+
[checkSource(typescript, contract2, "snippet.tsx", args["code"])],
|
|
2380
|
+
{ parser }
|
|
2381
|
+
);
|
|
2382
|
+
return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
|
|
1329
2383
|
}
|
|
1330
2384
|
default:
|
|
1331
2385
|
throw new UnknownTool(name);
|
|
1332
2386
|
}
|
|
1333
2387
|
}
|
|
1334
2388
|
function handleMessage(raw, context) {
|
|
1335
|
-
const
|
|
2389
|
+
const failure2 = (id2, code, message) => ({
|
|
1336
2390
|
jsonrpc: "2.0",
|
|
1337
2391
|
id: id2 ?? null,
|
|
1338
2392
|
error: { code, message }
|
|
1339
2393
|
});
|
|
1340
|
-
if (typeof raw !== "object" || raw === null) return
|
|
2394
|
+
if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
|
|
1341
2395
|
const { id, method, params } = raw;
|
|
1342
|
-
if (typeof method !== "string") return
|
|
2396
|
+
if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
|
|
1343
2397
|
if (id === void 0) return null;
|
|
1344
2398
|
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
1345
2399
|
switch (method) {
|
|
1346
2400
|
case "initialize": {
|
|
1347
2401
|
const asked = params?.protocolVersion;
|
|
2402
|
+
const identity = identityOf(context.contract);
|
|
1348
2403
|
return result({
|
|
1349
2404
|
protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
|
|
1350
2405
|
capabilities: { tools: {} },
|
|
1351
|
-
|
|
1352
|
-
|
|
2406
|
+
// The program's version, not the contract's: the contract has its own, in `_meta` and in
|
|
2407
|
+
// the identity of every result.
|
|
2408
|
+
serverInfo: { name: "oprt", version: VERSION },
|
|
2409
|
+
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.",
|
|
2410
|
+
_meta: { "dev.oprt/identity": identity }
|
|
1353
2411
|
});
|
|
1354
2412
|
}
|
|
1355
2413
|
case "ping":
|
|
@@ -1359,17 +2417,23 @@ function handleMessage(raw, context) {
|
|
|
1359
2417
|
case "tools/call": {
|
|
1360
2418
|
const { name, arguments: args } = params ?? {};
|
|
1361
2419
|
try {
|
|
1362
|
-
|
|
2420
|
+
const output = runTool(String(name), args ?? {}, context);
|
|
2421
|
+
return result({
|
|
2422
|
+
content: [text(output.text)],
|
|
2423
|
+
structuredContent: output.structured
|
|
2424
|
+
});
|
|
1363
2425
|
} catch (error) {
|
|
1364
|
-
if (error instanceof UnknownTool) return
|
|
2426
|
+
if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
|
|
2427
|
+
const failed = errorEnvelope(error, context.contract);
|
|
1365
2428
|
return result({
|
|
1366
|
-
content: [text(
|
|
2429
|
+
content: [text(failed.error)],
|
|
2430
|
+
structuredContent: failed,
|
|
1367
2431
|
isError: true
|
|
1368
2432
|
});
|
|
1369
2433
|
}
|
|
1370
2434
|
}
|
|
1371
2435
|
default:
|
|
1372
|
-
return
|
|
2436
|
+
return failure2(id, -32601, `Method not found: ${method}`);
|
|
1373
2437
|
}
|
|
1374
2438
|
}
|
|
1375
2439
|
function serve(input, output, context) {
|
|
@@ -1388,64 +2452,108 @@ function serve(input, output, context) {
|
|
|
1388
2452
|
});
|
|
1389
2453
|
return new Promise((done) => lines.on("close", done));
|
|
1390
2454
|
}
|
|
1391
|
-
var VERSIONS, text,
|
|
2455
|
+
var VERSIONS, text, LIMITS, ANSWERS, readOnly, define, TOOLS, UnknownTool;
|
|
1392
2456
|
var init_mcp = __esm({
|
|
1393
2457
|
"src/mcp.ts"() {
|
|
1394
2458
|
"use strict";
|
|
1395
2459
|
init_esm_shims();
|
|
1396
2460
|
init_agent_block();
|
|
1397
2461
|
init_check();
|
|
2462
|
+
init_identity();
|
|
1398
2463
|
init_lookup();
|
|
2464
|
+
init_results();
|
|
2465
|
+
init_types();
|
|
2466
|
+
init_validate();
|
|
1399
2467
|
VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1400
2468
|
text = (value) => ({ type: "text", text: value });
|
|
1401
|
-
|
|
2469
|
+
LIMITS = { query: 500, name: 100, code: 2e5 };
|
|
2470
|
+
ANSWERS = {
|
|
2471
|
+
search: "search",
|
|
2472
|
+
get_component: "component.detail",
|
|
2473
|
+
list_components: "component.list",
|
|
2474
|
+
get_instructions: "instructions",
|
|
2475
|
+
check_code: "check"
|
|
2476
|
+
};
|
|
2477
|
+
readOnly = { readOnlyHint: true };
|
|
2478
|
+
define = (tool) => ({
|
|
2479
|
+
...tool,
|
|
2480
|
+
outputSchema: outputSchemaFor(ANSWERS[tool.name]),
|
|
2481
|
+
annotations: readOnly
|
|
2482
|
+
});
|
|
1402
2483
|
TOOLS = [
|
|
1403
|
-
{
|
|
2484
|
+
define({
|
|
1404
2485
|
name: "search",
|
|
1405
|
-
description: "Find the OPERATOR component or recipe
|
|
2486
|
+
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
2487
|
inputSchema: {
|
|
1407
2488
|
type: "object",
|
|
1408
2489
|
properties: {
|
|
1409
|
-
query:
|
|
1410
|
-
|
|
2490
|
+
query: {
|
|
2491
|
+
type: "string",
|
|
2492
|
+
minLength: 1,
|
|
2493
|
+
maxLength: LIMITS.query,
|
|
2494
|
+
description: "The need, in plain words."
|
|
2495
|
+
},
|
|
2496
|
+
limit: {
|
|
2497
|
+
type: "integer",
|
|
2498
|
+
minimum: 1,
|
|
2499
|
+
maximum: SEARCH_LIMIT_MAX,
|
|
2500
|
+
default: SEARCH_LIMIT_DEFAULT,
|
|
2501
|
+
description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
|
|
2502
|
+
}
|
|
1411
2503
|
},
|
|
1412
|
-
required: ["query"]
|
|
2504
|
+
required: ["query"],
|
|
2505
|
+
additionalProperties: false
|
|
1413
2506
|
}
|
|
1414
|
-
},
|
|
1415
|
-
{
|
|
2507
|
+
}),
|
|
2508
|
+
define({
|
|
1416
2509
|
name: "get_component",
|
|
1417
|
-
description: "One
|
|
2510
|
+
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
2511
|
inputSchema: {
|
|
1419
2512
|
type: "object",
|
|
1420
2513
|
properties: {
|
|
1421
|
-
name:
|
|
2514
|
+
name: {
|
|
2515
|
+
type: "string",
|
|
2516
|
+
minLength: 1,
|
|
2517
|
+
maxLength: LIMITS.name,
|
|
2518
|
+
description: "Component name, such as Button."
|
|
2519
|
+
},
|
|
1422
2520
|
dense: {
|
|
1423
2521
|
type: "boolean",
|
|
1424
|
-
|
|
2522
|
+
default: true,
|
|
2523
|
+
description: "Compact form (every constraint, one sentence per prop). False: full prose."
|
|
1425
2524
|
}
|
|
1426
2525
|
},
|
|
1427
|
-
required: ["name"]
|
|
2526
|
+
required: ["name"],
|
|
2527
|
+
additionalProperties: false
|
|
1428
2528
|
}
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
2529
|
+
}),
|
|
2530
|
+
define({
|
|
1431
2531
|
name: "list_components",
|
|
1432
2532
|
description: "Every OPERATOR component in one line each, with its parts.",
|
|
1433
|
-
inputSchema: { type: "object", properties: {} }
|
|
1434
|
-
},
|
|
1435
|
-
{
|
|
2533
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2534
|
+
}),
|
|
2535
|
+
define({
|
|
1436
2536
|
name: "get_instructions",
|
|
1437
|
-
description: "OPERATOR's house rules,
|
|
1438
|
-
inputSchema: { type: "object", properties: {} }
|
|
1439
|
-
},
|
|
1440
|
-
{
|
|
2537
|
+
description: "OPERATOR's house rules, component index and recipes: read once before building.",
|
|
2538
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
2539
|
+
}),
|
|
2540
|
+
define({
|
|
1441
2541
|
name: "check_code",
|
|
1442
|
-
description: "Check TSX
|
|
2542
|
+
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
2543
|
inputSchema: {
|
|
1444
2544
|
type: "object",
|
|
1445
|
-
properties: {
|
|
1446
|
-
|
|
2545
|
+
properties: {
|
|
2546
|
+
code: {
|
|
2547
|
+
type: "string",
|
|
2548
|
+
minLength: 1,
|
|
2549
|
+
maxLength: LIMITS.code,
|
|
2550
|
+
description: "The TSX source of one file."
|
|
2551
|
+
}
|
|
2552
|
+
},
|
|
2553
|
+
required: ["code"],
|
|
2554
|
+
additionalProperties: false
|
|
1447
2555
|
}
|
|
1448
|
-
}
|
|
2556
|
+
})
|
|
1449
2557
|
];
|
|
1450
2558
|
UnknownTool = class extends Error {
|
|
1451
2559
|
constructor(name) {
|
|
@@ -1462,35 +2570,47 @@ var index_exports = {};
|
|
|
1462
2570
|
__export(index_exports, {
|
|
1463
2571
|
CliError: () => CliError,
|
|
1464
2572
|
DEFAULT_OUT: () => DEFAULT_OUT,
|
|
2573
|
+
RESULT_SCHEMAS: () => RESULT_SCHEMAS,
|
|
2574
|
+
RESULT_SCHEMA_VERSION: () => RESULT_SCHEMA_VERSION,
|
|
2575
|
+
RESULT_TYPES: () => RESULT_TYPES,
|
|
1465
2576
|
TOOLS: () => TOOLS,
|
|
1466
2577
|
VERSION: () => VERSION,
|
|
1467
2578
|
applyBlock: () => applyBlock,
|
|
1468
2579
|
blockVersion: () => blockVersion,
|
|
2580
|
+
buildCheckReport: () => buildCheckReport,
|
|
2581
|
+
checkExitCode: () => checkExitCode,
|
|
1469
2582
|
checkPaths: () => checkPaths,
|
|
1470
2583
|
checkSource: () => checkSource,
|
|
1471
2584
|
componentDetail: () => componentDetail,
|
|
1472
2585
|
componentIndex: () => componentIndex,
|
|
1473
2586
|
contract: () => contract,
|
|
2587
|
+
contractDigest: () => contractDigest,
|
|
1474
2588
|
doctor: () => doctor,
|
|
2589
|
+
envelope: () => envelope,
|
|
2590
|
+
errorEnvelope: () => errorEnvelope,
|
|
1475
2591
|
handleMessage: () => handleMessage,
|
|
2592
|
+
identityOf: () => identityOf,
|
|
1476
2593
|
init: () => init,
|
|
1477
2594
|
install: () => install,
|
|
2595
|
+
outputSchemaFor: () => outputSchemaFor,
|
|
1478
2596
|
parseArgs: () => parseArgs,
|
|
1479
2597
|
registry: () => registry,
|
|
1480
2598
|
removeBlock: () => removeBlock,
|
|
1481
2599
|
renderAgentBlock: () => renderAgentBlock,
|
|
2600
|
+
renderCheckText: () => renderCheckText,
|
|
1482
2601
|
renderComponent: () => renderComponent,
|
|
1483
2602
|
resolveEntries: () => resolveEntries,
|
|
1484
2603
|
runAgentCommand: () => runAgentCommand,
|
|
2604
|
+
schemaFor: () => schemaFor,
|
|
1485
2605
|
search: () => search,
|
|
1486
2606
|
serve: () => serve
|
|
1487
2607
|
});
|
|
1488
2608
|
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
1489
|
-
import { dirname as dirname5, join as
|
|
2609
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
1490
2610
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1491
2611
|
function loadRegistry() {
|
|
1492
2612
|
const here = dirname5(fileURLToPath4(import.meta.url));
|
|
1493
|
-
const candidates = [
|
|
2613
|
+
const candidates = [join5(here, "registry.json"), join5(here, "../generated/registry.json")];
|
|
1494
2614
|
const found = candidates.find((candidate) => existsSync5(candidate));
|
|
1495
2615
|
if (found === void 0) {
|
|
1496
2616
|
throw new Error(
|
|
@@ -1505,15 +2625,17 @@ function install(options, context = {}) {
|
|
|
1505
2625
|
registry: context.registry ?? registry
|
|
1506
2626
|
});
|
|
1507
2627
|
}
|
|
1508
|
-
var
|
|
2628
|
+
var registry, loaded, contract;
|
|
1509
2629
|
var init_index = __esm({
|
|
1510
2630
|
"src/index.ts"() {
|
|
1511
2631
|
"use strict";
|
|
1512
2632
|
init_esm_shims();
|
|
1513
|
-
init_package();
|
|
1514
2633
|
init_contract();
|
|
1515
2634
|
init_installer();
|
|
2635
|
+
init_identity();
|
|
1516
2636
|
init_agent_commands();
|
|
2637
|
+
init_identity();
|
|
2638
|
+
init_results();
|
|
1517
2639
|
init_agent_block();
|
|
1518
2640
|
init_check();
|
|
1519
2641
|
init_doctor();
|
|
@@ -1523,7 +2645,6 @@ var init_index = __esm({
|
|
|
1523
2645
|
init_args();
|
|
1524
2646
|
init_installer();
|
|
1525
2647
|
init_types();
|
|
1526
|
-
VERSION = package_default.version;
|
|
1527
2648
|
registry = loadRegistry();
|
|
1528
2649
|
contract = () => loaded ??= loadContract();
|
|
1529
2650
|
}
|
|
@@ -1532,6 +2653,7 @@ var init_index = __esm({
|
|
|
1532
2653
|
// src/bin.ts
|
|
1533
2654
|
init_esm_shims();
|
|
1534
2655
|
init_args();
|
|
2656
|
+
init_results();
|
|
1535
2657
|
init_types();
|
|
1536
2658
|
var helpText = (version) => `oprt ${version} \u2014 OPERATOR source installer
|
|
1537
2659
|
|
|
@@ -1551,11 +2673,19 @@ For coding agents (each takes --json):
|
|
|
1551
2673
|
oprt component --list Every component, one line each.
|
|
1552
2674
|
oprt check <paths...> [--strict]
|
|
1553
2675
|
Check written code against the contract: option values,
|
|
1554
|
-
nesting, required props, house rules.
|
|
2676
|
+
nesting, required props, house rules. Says how much it
|
|
2677
|
+
read (evaluation_status); a file that does not parse is
|
|
2678
|
+
not checked, and is never clean.
|
|
1555
2679
|
oprt doctor Check this project's setup.
|
|
1556
2680
|
oprt mcp Serve the lookups and the check to an agent over the
|
|
1557
2681
|
Model Context Protocol (stdin and stdout).
|
|
1558
2682
|
|
|
2683
|
+
Exit codes: 0 success. check: 0 when every file read has no errors (no warnings either
|
|
2684
|
+
under --strict); limits it could not read are shown and do not fail the run. 1 when check
|
|
2685
|
+
finds an error, a file does not parse, or no .tsx or .jsx file was read; also for other
|
|
2686
|
+
failures (file conflicts, TypeScript missing). 2 for a usage error, an unknown name or a
|
|
2687
|
+
path that does not exist.
|
|
2688
|
+
|
|
1559
2689
|
Names accept PascalCase or kebab-case. Default output: src/components/operator.
|
|
1560
2690
|
--dry-run Preview all files and dependencies without creating anything.
|
|
1561
2691
|
--overwrite Replace different existing files; identical files are always skipped.
|
|
@@ -1571,83 +2701,30 @@ try {
|
|
|
1571
2701
|
const major = Number(process.versions.node.split(".")[0]);
|
|
1572
2702
|
if (major < 22) throw new CliError("oprt requires Node.js 22 or newer.");
|
|
1573
2703
|
const command = parseArgs(process.argv.slice(2));
|
|
1574
|
-
const { VERSION: VERSION2, contract: contract2,
|
|
2704
|
+
const { VERSION: VERSION2, contract: contract2, registry: registry2, runAgentCommand: runAgentCommand2, serve: serve2 } = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
1575
2705
|
if (command.command === "mcp")
|
|
1576
2706
|
await serve2(process.stdin, process.stdout, { contract: contract2(), registry: registry2 });
|
|
1577
|
-
else if (command.command === "
|
|
2707
|
+
else if (command.command === "help") process.stdout.write(helpText(VERSION2));
|
|
2708
|
+
else if (command.command === "version") process.stdout.write(`${VERSION2}
|
|
2709
|
+
`);
|
|
2710
|
+
else {
|
|
1578
2711
|
const answer = runAgentCommand2(command, { cwd: process.cwd(), contract: contract2(), registry: registry2 });
|
|
1579
2712
|
process.stdout.write(command.json ? `${JSON.stringify(answer.json, null, 2)}
|
|
1580
2713
|
` : answer.text);
|
|
1581
2714
|
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
2715
|
}
|
|
1634
2716
|
} catch (error) {
|
|
1635
2717
|
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}
|
|
2718
|
+
if (process.argv.includes("--json")) {
|
|
2719
|
+
let loaded2;
|
|
2720
|
+
try {
|
|
2721
|
+
loaded2 = (await Promise.resolve().then(() => (init_index(), index_exports))).contract();
|
|
2722
|
+
} catch {
|
|
2723
|
+
loaded2 = void 0;
|
|
2724
|
+
}
|
|
2725
|
+
process.stdout.write(`${JSON.stringify(errorEnvelope(error, loaded2), null, 2)}
|
|
2726
|
+
`);
|
|
2727
|
+
} else process.stderr.write(`oprt: ${message}
|
|
1651
2728
|
`);
|
|
1652
2729
|
process.exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
1653
2730
|
}
|