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