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