@oprt/cli 0.1.0 → 0.4.0

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