@oprt/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1445 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CliError: () => CliError,
24
+ DEFAULT_OUT: () => DEFAULT_OUT,
25
+ TOOLS: () => TOOLS,
26
+ VERSION: () => VERSION,
27
+ applyBlock: () => applyBlock,
28
+ blockVersion: () => blockVersion,
29
+ checkPaths: () => checkPaths,
30
+ checkSource: () => checkSource,
31
+ componentDetail: () => componentDetail,
32
+ componentIndex: () => componentIndex,
33
+ contract: () => contract,
34
+ doctor: () => doctor,
35
+ handleMessage: () => handleMessage,
36
+ init: () => init,
37
+ install: () => install,
38
+ parseArgs: () => parseArgs,
39
+ registry: () => registry,
40
+ removeBlock: () => removeBlock,
41
+ renderAgentBlock: () => renderAgentBlock,
42
+ renderComponent: () => renderComponent,
43
+ resolveEntries: () => resolveEntries,
44
+ runAgentCommand: () => runAgentCommand,
45
+ search: () => search,
46
+ serve: () => serve
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.30_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
51
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
52
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
53
+
54
+ // src/index.ts
55
+ var import_node_fs6 = require("fs");
56
+ var import_node_path6 = require("path");
57
+ var import_node_url3 = require("url");
58
+
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
+ // src/contract.ts
129
+ var import_node_fs = require("fs");
130
+ var import_node_path = require("path");
131
+ var import_node_url = require("url");
132
+ function loadContract() {
133
+ const here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
134
+ const candidates = [(0, import_node_path.join)(here, "contract.json"), (0, import_node_path.join)(here, "../generated/contract.json")];
135
+ const found = candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
136
+ if (found === void 0) {
137
+ throw new Error(
138
+ `contract.json is missing next to ${here}. Install @oprt/cli again: the contract ships beside the executable, and the executable cannot be copied without it.`
139
+ );
140
+ }
141
+ return JSON.parse((0, import_node_fs.readFileSync)(found, "utf8"));
142
+ }
143
+
144
+ // src/installer.ts
145
+ var import_node_fs2 = require("fs");
146
+ var import_node_path2 = require("path");
147
+
148
+ // src/types.ts
149
+ var CliError = class extends Error {
150
+ constructor(message, exitCode = 1, details = {}) {
151
+ super(message);
152
+ this.exitCode = exitCode;
153
+ this.name = "CliError";
154
+ this.code = details.code ?? (exitCode === 2 ? "ERR_USAGE" : "ERR_FAILED");
155
+ if (details.suggestions !== void 0) this.suggestions = details.suggestions;
156
+ }
157
+ exitCode;
158
+ code;
159
+ suggestions;
160
+ };
161
+
162
+ // src/args.ts
163
+ var DEFAULT_OUT = "src/components/operator";
164
+ function split(command, rest, spec) {
165
+ const flags = /* @__PURE__ */ new Set();
166
+ const values = /* @__PURE__ */ new Map();
167
+ const positional = [];
168
+ for (let i = 0; i < rest.length; i++) {
169
+ const token = rest[i];
170
+ if (!token.startsWith("-")) {
171
+ positional.push(token);
172
+ continue;
173
+ }
174
+ const kind = spec[token];
175
+ if (kind === void 0)
176
+ throw new CliError(
177
+ `Unknown option ${JSON.stringify(token)} for ${command}. Run oprt --help.`,
178
+ 2
179
+ );
180
+ if (flags.has(token) || values.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
181
+ if (kind === "flag") flags.add(token);
182
+ else {
183
+ const value = rest[++i];
184
+ if (!value || value.startsWith("-")) throw new CliError(`${token} requires a value.`, 2);
185
+ values.set(token, value);
186
+ }
187
+ }
188
+ return {
189
+ has: (flag) => flags.has(flag),
190
+ value: (name) => values.get(name),
191
+ positional
192
+ };
193
+ }
194
+ var AGENT_COMMANDS = {
195
+ component(rest) {
196
+ const { has, positional } = split("component", rest, {
197
+ "--list": "flag",
198
+ "--dense": "flag",
199
+ "--json": "flag"
200
+ });
201
+ if (has("--list") ? positional.length > 0 : positional.length === 0)
202
+ throw new CliError("Usage: oprt component <Name...> [--dense], or component --list", 2);
203
+ return {
204
+ command: "component",
205
+ names: positional,
206
+ list: has("--list"),
207
+ dense: has("--dense"),
208
+ json: has("--json")
209
+ };
210
+ },
211
+ search(rest) {
212
+ const { has, value, positional } = split("search", rest, {
213
+ "--limit": "value",
214
+ "--json": "flag"
215
+ });
216
+ 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);
220
+ return { command: "search", query: positional.join(" "), limit, json: has("--json") };
221
+ },
222
+ init(rest) {
223
+ const { has, value, positional } = split("init", rest, {
224
+ "--agent": "value",
225
+ "--path": "value",
226
+ "--remove": "flag",
227
+ "--dry-run": "flag",
228
+ "--json": "flag"
229
+ });
230
+ const agent = value("--agent");
231
+ const path = value("--path");
232
+ if (positional.length > 0 || agent !== void 0 && path !== void 0)
233
+ throw new CliError(
234
+ "Usage: oprt init [--agent <name> | --path <file>] [--remove] [--dry-run]",
235
+ 2
236
+ );
237
+ return {
238
+ command: "init",
239
+ ...agent === void 0 ? {} : { agent },
240
+ ...path === void 0 ? {} : { path },
241
+ remove: has("--remove"),
242
+ dryRun: has("--dry-run"),
243
+ json: has("--json")
244
+ };
245
+ },
246
+ check(rest) {
247
+ const { has, positional } = split("check", rest, { "--strict": "flag", "--json": "flag" });
248
+ if (positional.length === 0)
249
+ throw new CliError("Usage: oprt check <file or folder...> [--strict]", 2);
250
+ return { command: "check", paths: positional, strict: has("--strict"), json: has("--json") };
251
+ },
252
+ mcp(rest) {
253
+ if (rest.length > 0) throw new CliError("Usage: oprt mcp", 2);
254
+ return { command: "mcp" };
255
+ },
256
+ doctor(rest) {
257
+ const { has, positional } = split("doctor", rest, { "--json": "flag" });
258
+ if (positional.length > 0) throw new CliError("Usage: oprt doctor", 2);
259
+ return { command: "doctor", json: has("--json") };
260
+ }
261
+ };
262
+ function parseArgs(args) {
263
+ if (!args.length || args.length === 1 && ["--help", "-h", "help"].includes(args[0]))
264
+ return { command: "help" };
265
+ if (args.length === 1 && ["--version", "-v"].includes(args[0])) return { command: "version" };
266
+ const [command, ...rest] = args;
267
+ if ([...Object.keys(AGENT_COMMANDS), "list", "add"].includes(command) && rest.length === 1 && ["--help", "-h"].includes(rest[0]))
268
+ return { command: "help" };
269
+ if (Object.hasOwn(AGENT_COMMANDS, command)) return AGENT_COMMANDS[command](rest);
270
+ if (command === "list") {
271
+ if (rest.length === 0) return { command, json: false };
272
+ if (rest.length === 1 && rest[0] === "--json") return { command, json: true };
273
+ throw new CliError("Usage: oprt list [--json]", 2);
274
+ }
275
+ if (command !== "add")
276
+ throw new CliError(`Unknown command ${JSON.stringify(command)}. Run oprt --help.`, 2);
277
+ const parsed = {
278
+ command,
279
+ names: [],
280
+ out: DEFAULT_OUT,
281
+ dryRun: false,
282
+ overwrite: false
283
+ };
284
+ const seen = /* @__PURE__ */ new Set();
285
+ for (let i = 0; i < rest.length; i++) {
286
+ const token = rest[i];
287
+ if (!token.startsWith("-")) {
288
+ parsed.names.push(token);
289
+ continue;
290
+ }
291
+ if (!["--out", "--dry-run", "--overwrite"].includes(token))
292
+ throw new CliError(`Unknown option ${JSON.stringify(token)}. Run oprt --help.`, 2);
293
+ if (seen.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
294
+ seen.add(token);
295
+ if (token === "--out") {
296
+ const value = rest[++i];
297
+ if (!value || value.startsWith("-"))
298
+ throw new CliError("--out requires a relative directory.", 2);
299
+ parsed.out = value;
300
+ } else if (token === "--dry-run") parsed.dryRun = true;
301
+ else parsed.overwrite = true;
302
+ }
303
+ if (!parsed.names.length)
304
+ throw new CliError("add requires one or more component or recipe names. Run oprt list.", 2);
305
+ return parsed;
306
+ }
307
+
308
+ // src/installer.ts
309
+ function resolveEntries(names, registry2) {
310
+ if (!names.length) throw new CliError("add requires one or more names.", 2);
311
+ const selected = /* @__PURE__ */ new Map();
312
+ for (const name of names) {
313
+ const normalized = name.toLowerCase();
314
+ const entry = registry2.entries.find(
315
+ (candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
316
+ );
317
+ if (!entry)
318
+ throw new CliError(`Unknown component or recipe ${JSON.stringify(name)}. Run oprt list.`, 2);
319
+ selected.set(entry.name, entry);
320
+ }
321
+ return [...selected.values()];
322
+ }
323
+ function validateRelative(path) {
324
+ if (!path || (0, import_node_path2.isAbsolute)(path) || /[\\:\0]/.test(path) || path.split("/").includes(".."))
325
+ throw new CliError(
326
+ `Unsafe path ${JSON.stringify(path)}: use a nonempty relative path without traversal.`,
327
+ 2
328
+ );
329
+ }
330
+ function stat(path) {
331
+ try {
332
+ return (0, import_node_fs2.lstatSync)(path);
333
+ } catch (error) {
334
+ if (error.code === "ENOENT") return void 0;
335
+ throw error;
336
+ }
337
+ }
338
+ function inspectPath(root, destination, directory = false) {
339
+ const path = (0, import_node_path2.relative)(root, destination);
340
+ if (path === ".." || path.startsWith(`..${import_node_path2.sep}`) || (0, import_node_path2.isAbsolute)(path))
341
+ throw new CliError("Output path escapes the working directory.");
342
+ let cursor = root;
343
+ const segments = path ? path.split(import_node_path2.sep) : [];
344
+ for (let i = 0; i < segments.length; i++) {
345
+ cursor = (0, import_node_path2.resolve)(cursor, segments[i]);
346
+ const info = stat(cursor);
347
+ if (!info) continue;
348
+ if (info.isSymbolicLink()) throw new CliError(`Refusing symlink in output path: ${cursor}`);
349
+ const needsDirectory = i < segments.length - 1 || directory;
350
+ if (needsDirectory && !info.isDirectory())
351
+ throw new CliError(`Output ancestor is not a directory: ${cursor}`);
352
+ if (!needsDirectory && !info.isFile())
353
+ throw new CliError(`Output target is not a regular file: ${cursor}`);
354
+ }
355
+ return stat(destination);
356
+ }
357
+ function installSources(options, context) {
358
+ const entries = resolveEntries(options.names, context.registry);
359
+ const out = options.out ?? DEFAULT_OUT;
360
+ validateRelative(out);
361
+ const root = (0, import_node_fs2.realpathSync)(context.cwd);
362
+ const outputDirectory = (0, import_node_path2.resolve)(root, out);
363
+ inspectPath(root, outputDirectory, true);
364
+ const paths = [...new Set(entries.flatMap((entry) => entry.files))].sort();
365
+ const files = paths.map((path) => {
366
+ validateRelative(path);
367
+ const content = context.registry.files[path];
368
+ if (!Object.hasOwn(context.registry.files, path) || typeof content !== "string")
369
+ throw new CliError(`Registry is missing source ${path}.`);
370
+ const destination = (0, import_node_path2.resolve)(outputDirectory, path);
371
+ const info = inspectPath(root, destination);
372
+ if (!info) return { path, destination, action: "create" };
373
+ if ((0, import_node_fs2.readFileSync)(destination, "utf8") === content) return { path, destination, action: "skip" };
374
+ if (!options.overwrite)
375
+ throw new CliError(
376
+ `Refusing to overwrite modified file ${destination}. Use --overwrite to replace it explicitly.`
377
+ );
378
+ return { path, destination, action: "overwrite" };
379
+ });
380
+ const dependencies = [...new Set(entries.flatMap((entry) => entry.dependencies))].sort();
381
+ const runtime = new Set(
382
+ entries.flatMap(
383
+ (entry) => entry.dependencies.filter((name) => !(entry.devDependencies ?? []).includes(name))
384
+ )
385
+ );
386
+ const result = {
387
+ outputDirectory,
388
+ entries: entries.map((entry) => entry.name),
389
+ dependencies,
390
+ devDependencies: dependencies.filter(
391
+ (name) => !runtime.has(name) && entries.some((entry) => (entry.devDependencies ?? []).includes(name))
392
+ ),
393
+ ranges: Object.fromEntries(
394
+ dependencies.flatMap((name) => {
395
+ const range = context.registry.ranges?.[name];
396
+ return range === void 0 ? [] : [[name, range]];
397
+ })
398
+ ),
399
+ files,
400
+ dryRun: options.dryRun ?? false
401
+ };
402
+ if (result.dryRun) return result;
403
+ for (const file of files) {
404
+ if (file.action === "skip") continue;
405
+ inspectPath(root, file.destination);
406
+ const parent = (0, import_node_path2.dirname)(file.destination);
407
+ (0, import_node_fs2.mkdirSync)(parent, { recursive: true });
408
+ inspectPath(root, parent, true);
409
+ const flags = import_node_fs2.constants.O_WRONLY | import_node_fs2.constants.O_CREAT | (import_node_fs2.constants.O_NOFOLLOW ?? 0) | (file.action === "create" ? import_node_fs2.constants.O_EXCL : import_node_fs2.constants.O_TRUNC);
410
+ const descriptor = (0, import_node_fs2.openSync)(file.destination, flags, 420);
411
+ try {
412
+ (0, import_node_fs2.writeFileSync)(descriptor, context.registry.files[file.path]);
413
+ } finally {
414
+ (0, import_node_fs2.closeSync)(descriptor);
415
+ }
416
+ }
417
+ return result;
418
+ }
419
+
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");
425
+
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;
438
+ }
439
+ }
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;
467
+ }
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
+ }));
479
+ }
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);
489
+ 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
502
+ };
503
+ }
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}` : ""}`
509
+ ];
510
+ for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
511
+ return lines;
512
+ }
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("; ")}`
520
+ );
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
+ }
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
+ `;
542
+ }
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;
552
+ };
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
+ });
564
+ };
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}`);
579
+ }
580
+ const inDescription = matched(wanted, component.description);
581
+ if (inDescription > 0) add(name, 2 * inDescription);
582
+ }
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}`
593
+ });
594
+ }
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
+ });
609
+ }
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);
613
+ }
614
+
615
+ // src/check.ts
616
+ var PACKAGE = "@oprt/react";
617
+ var NATIVE = {
618
+ button: "Button",
619
+ input: "Input, or the control for its type (Checkbox, RadioGroup, NumberInput, Slider, DatePicker, FileUpload)",
620
+ select: "Select",
621
+ textarea: "Textarea",
622
+ table: "DataTable or DataGrid",
623
+ dialog: "Dialog",
624
+ progress: "Progress",
625
+ hr: "Divider"
626
+ };
627
+ var NATIVE_ATTRIBUTES = new Set(
628
+ `key ref children className style id title role tabIndex hidden lang dir slot draggable spellCheck
629
+ translate inert accessKey autoCapitalize contentEditable enterKeyHint inputMode
630
+ dangerouslySetInnerHTML suppressHydrationWarning suppressContentEditableWarning htmlFor type name
631
+ value defaultValue checked defaultChecked disabled readOnly required placeholder autoComplete
632
+ autoFocus min max step minLength maxLength pattern multiple accept form formAction href target rel
633
+ download src alt width height loading rows cols wrap open colSpan rowSpan scope headers label list
634
+ size method action encType noValidate popover popoverTarget popoverTargetAction asChild`.split(
635
+ /\s+/
636
+ )
637
+ );
638
+ var nativeAttribute = (name) => NATIVE_ATTRIBUTES.has(name) || /^(aria-|data-)/.test(name) || /^on[A-Z]/.test(name);
639
+ var RAW_COLOR_CLASS = /\[(?:#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\()/;
640
+ var RAW_COLOR_VALUE = /^#[0-9a-fA-F]{3,8}$|^(?:rgb|hsl)a?\(/;
641
+ var ARBITRARY_SIZE = /(?:^|[\s:])-?[a-z][a-z0-9-]*-\[-?\d*\.?\d+px\]/;
642
+ var ROUNDED_CORNER = /(?:^|[\s:])rounded(?!-none(?:\s|$))(?:-[^\s]+)?(?=\s|$)/;
643
+ function allowedChildren2(rules, name) {
644
+ for (const [parent, rule] of Object.entries(rules ?? {})) {
645
+ if (parent === name) return rule.allowed_children;
646
+ const nested = allowedChildren2(rule.children, name);
647
+ if (nested !== void 0) return nested;
648
+ }
649
+ return void 0;
650
+ }
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);
655
+ const findings = [];
656
+ const report = (node, finding) => {
657
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
658
+ findings.push({
659
+ code: finding.code,
660
+ severity: finding.severity,
661
+ file,
662
+ line: line + 1,
663
+ column: character + 1,
664
+ ...finding.component === void 0 ? {} : { component: finding.component },
665
+ ...finding.prop === void 0 ? {} : { prop: finding.prop },
666
+ message: finding.message,
667
+ ...finding.valid_values === void 0 ? {} : { valid_values: finding.valid_values },
668
+ ...finding.suggestions === void 0 ? {} : { suggestions: finding.suggestions }
669
+ });
670
+ };
671
+ const isComponent = (name) => Object.hasOwn(contract2.components, name);
672
+ const named = /* @__PURE__ */ new Map();
673
+ 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;
680
+ const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0, import_node_path3.basename)(from));
681
+ 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;
689
+ if (from === PACKAGE && !contract2.exports.includes(exported)) {
690
+ let suggestions = nearest(exported, Object.keys(contract2.components));
691
+ if (suggestions.length === 0) suggestions = nearest(exported, contract2.exports);
692
+ let hint = suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "";
693
+ if (suggestions.length === 0) {
694
+ const [byPurpose] = search(
695
+ contract2,
696
+ { version: contract2.version, entries: [], files: {} },
697
+ exported.replace(/([a-z0-9])([A-Z])/g, "$1 $2"),
698
+ 1
699
+ );
700
+ if (byPurpose !== void 0) {
701
+ suggestions = [byPurpose.name];
702
+ hint = ` Closest by purpose: ${byPurpose.name} (${byPurpose.purpose}).`;
703
+ }
704
+ }
705
+ report(element2, {
706
+ code: "UNKNOWN_EXPORT",
707
+ severity: "error",
708
+ message: `${PACKAGE} has no export ${JSON.stringify(exported)}.${hint}`,
709
+ ...suggestions.length > 0 ? { suggestions } : {}
710
+ });
711
+ continue;
712
+ }
713
+ if (isComponent(exported)) named.set(element2.name.text, exported);
714
+ }
715
+ }
716
+ const literals = (expression) => {
717
+ 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)];
723
+ return [];
724
+ };
725
+ const valueOf = (attribute) => attribute.initializer === void 0 ? void 0 : ts.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
726
+ const classText = (expression) => {
727
+ if (!expression) return "";
728
+ if (ts.isTemplateExpression(expression))
729
+ return [
730
+ expression.head.text,
731
+ ...expression.templateSpans.map((span) => span.literal.text)
732
+ ].join(" ");
733
+ return literals(expression).join(" ");
734
+ };
735
+ const houseRules = (attribute, name) => {
736
+ const value = valueOf(attribute);
737
+ if (name === "className") {
738
+ const classes = classText(value);
739
+ if (RAW_COLOR_CLASS.test(classes))
740
+ report(attribute, {
741
+ code: "RAW_COLOR",
742
+ severity: "warning",
743
+ message: "A color written out by hand. Use an OPERATOR color token (bg-oprt-\u2026, text-oprt-\u2026)."
744
+ });
745
+ if (ARBITRARY_SIZE.test(classes))
746
+ report(attribute, {
747
+ code: "ARBITRARY_SIZE",
748
+ severity: "warning",
749
+ message: "A size in pixels written out by hand. Use the spacing scale (p-2u, gap-3u, h-oprt-md)."
750
+ });
751
+ if (ROUNDED_CORNER.test(classes))
752
+ report(attribute, {
753
+ code: "ROUNDED_CORNER",
754
+ severity: "warning",
755
+ message: "A rounded corner. The radius is 0 everywhere in OPERATOR."
756
+ });
757
+ }
758
+ if (name === "style" && value && ts.isObjectLiteralExpression(value)) {
759
+ for (const property of value.properties)
760
+ if (ts.isPropertyAssignment(property) && literals(property.initializer).some((literal) => RAW_COLOR_VALUE.test(literal)))
761
+ report(attribute, {
762
+ code: "RAW_COLOR",
763
+ severity: "warning",
764
+ message: "A color written out by hand. Use an OPERATOR color token (var(--oprt-\u2026))."
765
+ });
766
+ }
767
+ };
768
+ const element = (opening, ancestors, parent, hasChildren, renderedAs) => {
769
+ const tag = opening.tagName;
770
+ 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))
774
+ report(opening, {
775
+ code: "NATIVE_CONTROL",
776
+ severity: "warning",
777
+ message: `A native <${tag.text}>: use ${NATIVE[tag.text]}.`
778
+ });
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;
782
+ else if (named.has(owner)) {
783
+ const joined = `${named.get(owner)}${tag.name.text}`;
784
+ report(opening, {
785
+ code: "DOT_NOTATION",
786
+ 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.")
788
+ });
789
+ }
790
+ }
791
+ const attributes = opening.attributes.properties;
792
+ for (const attribute of attributes)
793
+ if (ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name))
794
+ houseRules(attribute, attribute.name.text);
795
+ if (component === null) return null;
796
+ const definition = contract2.components[component];
797
+ const spread = attributes.some((attribute) => ts.isJsxSpreadAttribute(attribute));
798
+ const given = /* @__PURE__ */ new Set();
799
+ 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);
802
+ given.add(prop);
803
+ const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
804
+ if (known === void 0) {
805
+ if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
806
+ const names = Object.keys(definition.props);
807
+ report(attribute, {
808
+ code: "UNKNOWN_PROP",
809
+ severity: "warning",
810
+ component,
811
+ 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" : ""}.`)
813
+ });
814
+ continue;
815
+ }
816
+ if (known.type !== "enum" || known.values === void 0) continue;
817
+ for (const literal of literals(valueOf(attribute))) {
818
+ if (known.values.includes(literal)) continue;
819
+ const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
820
+ report(attribute, {
821
+ code: "INVALID_PROP_VALUE",
822
+ severity: "error",
823
+ component,
824
+ prop,
825
+ message: `${component} ${prop}=${JSON.stringify(literal)} is not an option. Use one of: ${known.values.join(", ")}.` + (elsewhere.length > 0 ? ` ${JSON.stringify(literal)} is a ${elsewhere.slice(0, 3).join(" or ")} ${prop}.` : ""),
826
+ valid_values: known.values
827
+ });
828
+ }
829
+ }
830
+ if (!spread) {
831
+ const needed = [
832
+ ...Object.entries(definition.props),
833
+ ...Object.entries(definition.slots ?? {})
834
+ ].filter(([, { required }]) => required);
835
+ for (const [prop] of needed)
836
+ if (!given.has(prop) && !(prop === "children" && hasChildren))
837
+ report(opening, {
838
+ code: "MISSING_REQUIRED_PROP",
839
+ severity: "error",
840
+ component,
841
+ prop,
842
+ message: `${component} needs ${prop}.`
843
+ });
844
+ }
845
+ const banned = (definition.never_inside ?? []).find((name) => ancestors.includes(name));
846
+ if (banned !== void 0)
847
+ report(opening, {
848
+ code: "INVALID_NESTING",
849
+ severity: "error",
850
+ component,
851
+ message: `${component} must not be inside ${banned}.`
852
+ });
853
+ const allowed = parent === null ? void 0 : allowedChildren2(contract2.composition_rules, parent);
854
+ if (allowed !== void 0 && !allowed.includes("any") && !allowed.includes(component))
855
+ report(opening, {
856
+ code: "INVALID_CHILD",
857
+ severity: "error",
858
+ component,
859
+ message: `${parent} takes only ${allowed.filter((name) => name !== "text").join(", ")} directly inside it, not ${component}.`
860
+ });
861
+ return component;
862
+ };
863
+ 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)
868
+ );
869
+ 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"
872
+ );
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
+ );
882
+ return;
883
+ }
884
+ ts.forEachChild(node, (child) => visit(child, ancestors, parent));
885
+ };
886
+ visit(source, [], null);
887
+ return findings.sort((a, b) => a.line - b.line || a.column - b.column);
888
+ }
889
+ var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
890
+ var READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
891
+ var NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
892
+ function* sources(path) {
893
+ const name = (0, import_node_path3.basename)(path);
894
+ if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
895
+ if ((0, import_node_fs3.statSync)(path).isDirectory()) {
896
+ 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;
898
+ }
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;
907
+ }
908
+ function checkPaths(paths, context) {
909
+ const root = (0, import_node_path3.resolve)(context.cwd);
910
+ const files = [];
911
+ for (const path of paths) {
912
+ const absolute = (0, import_node_path3.resolve)(root, path);
913
+ if (!(0, import_node_fs3.existsSync)(absolute))
914
+ throw new CliError(`No such file or folder: ${path}`, 2, { code: "ERR_PATH_NOT_FOUND" });
915
+ files.push(...sources(absolute));
916
+ }
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(
925
+ (file) => checkSource(
926
+ ts,
927
+ context.contract,
928
+ (0, import_node_path3.relative)(root, file).split(import_node_path3.sep).join("/"),
929
+ (0, import_node_fs3.readFileSync)(file, "utf8")
930
+ )
931
+ );
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
+ };
940
+ }
941
+
942
+ // src/doctor.ts
943
+ var import_node_fs4 = require("fs");
944
+ var import_node_path4 = require("path");
945
+
946
+ // src/agent-block.ts
947
+ var START = "<!-- OPERATOR:START -->";
948
+ var END = "<!-- OPERATOR:END -->";
949
+ var firstSentence2 = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
950
+ function renderAgentBlock(contract2) {
951
+ const lines = [
952
+ START,
953
+ `<!-- Written by oprt ${contract2.version}. Do not edit between these markers: run \`oprt init\` to refresh. -->`,
954
+ "## OPERATOR design system",
955
+ "",
956
+ "This project builds its interface with OPERATOR (`@oprt/react`). Use its components, and look them up instead of guessing: these commands print the facts for the version installed here. Run them with the project's package runner (`npx oprt \u2026`, `pnpm exec oprt \u2026`).",
957
+ "",
958
+ "- `oprt search <need>`: which component or recipe answers a need",
959
+ "- `oprt component <Name>`: one component's options, nesting rules and example (`--dense` for the short form)",
960
+ "- `oprt add <recipe>`: copy a working screen into the project to adapt",
961
+ "- `oprt check <paths>`: check written code against the contract",
962
+ "",
963
+ "### Rules",
964
+ ""
965
+ ];
966
+ contract2.guidance.rules.forEach(({ rule, wrong, right }, index) => {
967
+ lines.push(`${index + 1}. ${rule}`);
968
+ if (wrong !== void 0) lines.push(` Wrong: \`${wrong}\``);
969
+ if (right !== void 0) lines.push(` Right: \`${right}\``);
970
+ });
971
+ lines.push("", "### Components", "");
972
+ for (const { name, purpose, parts } of componentIndex(contract2))
973
+ lines.push(`- ${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`);
974
+ const recipes = Object.entries(contract2.patterns ?? {});
975
+ if (recipes.length > 0) {
976
+ lines.push("", "### Recipes (whole screens to start from)", "");
977
+ for (const [name, { description, install: install2 }] of recipes)
978
+ lines.push(`- ${name}: ${firstSentence2(description)} \`${install2}\``);
979
+ }
980
+ lines.push(END);
981
+ return `${lines.join("\n")}
982
+ `;
983
+ }
984
+ function blockVersion(text2) {
985
+ const start = text2.indexOf(START);
986
+ if (start === -1) return void 0;
987
+ return /Written by oprt (\S+)\. Do not edit/.exec(text2.slice(start))?.[1];
988
+ }
989
+ function region(text2) {
990
+ const start = text2.indexOf(START);
991
+ if (start === -1) return void 0;
992
+ const end = text2.indexOf(END, start);
993
+ if (end === -1) return void 0;
994
+ const after = end + END.length;
995
+ return [start, text2[after] === "\n" ? after + 1 : after];
996
+ }
997
+ function applyBlock(existing, block) {
998
+ if (existing === null) return { text: block, action: "create" };
999
+ const found = region(existing);
1000
+ if (found === void 0) {
1001
+ const before = existing === "" || existing.endsWith("\n") ? existing : `${existing}
1002
+ `;
1003
+ return { text: `${before}${before === "" ? "" : "\n"}${block}`, action: "append" };
1004
+ }
1005
+ const text2 = existing.slice(0, found[0]) + block + existing.slice(found[1]);
1006
+ return { text: text2, action: text2 === existing ? "unchanged" : "update" };
1007
+ }
1008
+ function removeBlock(existing) {
1009
+ const found = region(existing);
1010
+ if (found === void 0) return { text: existing, found: false };
1011
+ const before = existing.slice(0, found[0]);
1012
+ return {
1013
+ text: (before.endsWith("\n\n") ? before.slice(0, -1) : before) + existing.slice(found[1]),
1014
+ found: true
1015
+ };
1016
+ }
1017
+
1018
+ // src/doctor.ts
1019
+ var PACKAGES = ["@oprt/react", "@oprt/tokens", "@oprt/tailwind"];
1020
+ var INSTRUCTION_FILES = [
1021
+ "AGENTS.md",
1022
+ "CLAUDE.md",
1023
+ ".claude/CLAUDE.md",
1024
+ ".cursor/rules/operator.mdc"
1025
+ ];
1026
+ var minor = (version) => version.split(".").slice(0, 2).join(".");
1027
+ function installedVersion(cwd, name) {
1028
+ for (let folder = cwd; ; folder = (0, import_node_path4.dirname)(folder)) {
1029
+ const manifest = (0, import_node_path4.join)(folder, "node_modules", name, "package.json");
1030
+ if ((0, import_node_fs4.existsSync)(manifest))
1031
+ return JSON.parse((0, import_node_fs4.readFileSync)(manifest, "utf8")).version;
1032
+ if ((0, import_node_path4.dirname)(folder) === folder) return void 0;
1033
+ }
1034
+ }
1035
+ function doctor({ cwd, contract: contract2 }) {
1036
+ const checks = [];
1037
+ for (const name of PACKAGES) {
1038
+ const version = installedVersion(cwd, name);
1039
+ const id = `package:${name}`;
1040
+ if (version === void 0)
1041
+ checks.push({
1042
+ id,
1043
+ status: "warn",
1044
+ message: `${name} is not installed.`,
1045
+ fix: `Install ${name}, or copy the sources you need with oprt add.`
1046
+ });
1047
+ else if (minor(version) !== minor(contract2.version))
1048
+ checks.push({
1049
+ id,
1050
+ status: "warn",
1051
+ message: `${name} is ${version}; this installer describes ${contract2.version}.`,
1052
+ fix: "Install matching versions of the OPERATOR packages and @oprt/cli."
1053
+ });
1054
+ else checks.push({ id, status: "ok", message: `${name} ${version} is installed.` });
1055
+ }
1056
+ const written = INSTRUCTION_FILES.map((path) => {
1057
+ const file = (0, import_node_path4.join)(cwd, path);
1058
+ return {
1059
+ path,
1060
+ version: (0, import_node_fs4.existsSync)(file) ? blockVersion((0, import_node_fs4.readFileSync)(file, "utf8")) : void 0
1061
+ };
1062
+ }).filter(({ version }) => version !== void 0);
1063
+ const stale = written.find(({ version }) => version !== contract2.version);
1064
+ if (written.length === 0)
1065
+ checks.push({
1066
+ id: "agent-instructions",
1067
+ status: "warn",
1068
+ message: "No agent instruction file carries the OPERATOR block.",
1069
+ fix: "oprt init"
1070
+ });
1071
+ else if (stale !== void 0)
1072
+ checks.push({
1073
+ id: "agent-instructions",
1074
+ status: "warn",
1075
+ message: `${stale.path} was written by oprt ${stale.version}; this is ${contract2.version}.`,
1076
+ fix: "oprt init"
1077
+ });
1078
+ else
1079
+ checks.push({
1080
+ id: "agent-instructions",
1081
+ status: "ok",
1082
+ message: `${written.map(({ path }) => path).join(", ")} carries the OPERATOR block (${contract2.version}).`
1083
+ });
1084
+ const count = (status) => checks.filter((check) => check.status === status).length;
1085
+ return { checks, summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") } };
1086
+ }
1087
+
1088
+ // src/init.ts
1089
+ var import_node_fs5 = require("fs");
1090
+ var import_node_path5 = require("path");
1091
+ var AGENT_FILES = {
1092
+ claude: ["CLAUDE.md"],
1093
+ cursor: [".cursor/rules/operator.mdc"],
1094
+ codex: ["AGENTS.md"],
1095
+ all: ["AGENTS.md", "CLAUDE.md", ".cursor/rules/operator.mdc"]
1096
+ };
1097
+ var DEFAULT_FILES = ["AGENTS.md"];
1098
+ var CURSOR_FRONT_MATTER = "---\nalwaysApply: true\n---\n\n";
1099
+ function targets({ agent, path }) {
1100
+ if (path !== void 0) return [path];
1101
+ if (agent === void 0) return DEFAULT_FILES;
1102
+ const files = AGENT_FILES[agent];
1103
+ if (files === void 0)
1104
+ throw new CliError(
1105
+ `Unknown agent ${JSON.stringify(agent)}. Use one of: ${Object.keys(AGENT_FILES).join(", ")}; or --path <file>.`,
1106
+ 2,
1107
+ { code: "ERR_UNKNOWN_AGENT", suggestions: Object.keys(AGENT_FILES) }
1108
+ );
1109
+ return files;
1110
+ }
1111
+ var withoutFrontMatter = (text2) => text2.replace(/^---\n[\s\S]*?\n---\n?/, "");
1112
+ function init(options, context) {
1113
+ const root = (0, import_node_fs5.realpathSync)(context.cwd);
1114
+ const block = renderAgentBlock(context.contract);
1115
+ const dryRun = options.dryRun ?? false;
1116
+ const files = targets(options).map((path) => {
1117
+ validateRelative(path);
1118
+ const destination = (0, import_node_path5.resolve)(root, path);
1119
+ inspectPath(root, destination);
1120
+ const existing = (0, import_node_fs5.existsSync)(destination) ? (0, import_node_fs5.readFileSync)(destination, "utf8") : null;
1121
+ if (options.remove) {
1122
+ if (existing === null) return { path, action: "absent" };
1123
+ const { text: text3, found } = removeBlock(existing);
1124
+ if (!found) return { path, action: "absent" };
1125
+ const empty = withoutFrontMatter(text3).trim() === "";
1126
+ if (!dryRun) {
1127
+ if (empty) (0, import_node_fs5.rmSync)(destination);
1128
+ else (0, import_node_fs5.writeFileSync)(destination, text3);
1129
+ }
1130
+ return { path, action: empty ? "delete" : "remove" };
1131
+ }
1132
+ const fresh = existing === null && path.endsWith(".mdc") ? CURSOR_FRONT_MATTER : existing;
1133
+ const { text: text2, action } = applyBlock(fresh, block);
1134
+ if (action !== "unchanged" && !dryRun) {
1135
+ (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(destination), { recursive: true });
1136
+ (0, import_node_fs5.writeFileSync)(destination, text2);
1137
+ }
1138
+ return { path, action: existing === null ? "create" : action };
1139
+ });
1140
+ return { version: context.contract.version, dryRun, files };
1141
+ }
1142
+
1143
+ // src/agent-commands.ts
1144
+ var envelope = (type, data) => ({ apiVersion: 1, type, data });
1145
+ function runAgentCommand(command, context) {
1146
+ const { contract: contract2, registry: registry2, cwd } = context;
1147
+ switch (command.command) {
1148
+ case "component": {
1149
+ if (command.list) {
1150
+ const components = componentIndex(contract2);
1151
+ return {
1152
+ text: components.map(
1153
+ ({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
1154
+ `
1155
+ ).join(""),
1156
+ json: envelope("component.list", { components }),
1157
+ exitCode: 0
1158
+ };
1159
+ }
1160
+ const details = command.names.map((name) => componentDetail(contract2, name));
1161
+ return {
1162
+ 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 }),
1164
+ exitCode: 0
1165
+ };
1166
+ }
1167
+ case "search": {
1168
+ const results = search(contract2, registry2, command.query, command.limit);
1169
+ return {
1170
+ text: results.length === 0 ? `Nothing matches ${JSON.stringify(command.query)}. Run oprt component --list to see what there is.
1171
+ ` : results.map(
1172
+ ({ kind, name, purpose, why, next }) => `${name} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
1173
+ ${next}
1174
+ `
1175
+ ).join(""),
1176
+ json: envelope("search", { query: command.query, matchCount: results.length, results }),
1177
+ exitCode: 0
1178
+ };
1179
+ }
1180
+ case "init": {
1181
+ const result = init(command, { cwd, contract: contract2 });
1182
+ const lines = [
1183
+ `OPERATOR ${result.version} agent instructions${result.dryRun ? " (dry run)" : ""}`,
1184
+ ...result.files.map(({ path, action }) => ` ${action.padEnd(9)} ${path}`),
1185
+ 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
+ ];
1187
+ return { text: `${lines.join("\n")}
1188
+ `, json: envelope("init", result), exitCode: 0 };
1189
+ }
1190
+ case "check": {
1191
+ 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
+ return {
1201
+ text: `${lines.join("\n")}
1202
+ `,
1203
+ json: envelope("check", report),
1204
+ // 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
1206
+ };
1207
+ }
1208
+ 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
+ ]);
1214
+ lines.push(
1215
+ `${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
1216
+ );
1217
+ return {
1218
+ text: `${lines.join("\n")}
1219
+ `,
1220
+ json: envelope("doctor", report),
1221
+ exitCode: report.summary.fail > 0 ? 1 : 0
1222
+ };
1223
+ }
1224
+ }
1225
+ }
1226
+
1227
+ // src/mcp.ts
1228
+ var import_node_readline = require("readline");
1229
+ var VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
1230
+ var text = (value) => ({ type: "text", text: value });
1231
+ var string = (description) => ({ type: "string", description });
1232
+ var TOOLS = [
1233
+ {
1234
+ 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.",
1236
+ inputSchema: {
1237
+ type: "object",
1238
+ properties: {
1239
+ query: string("The need, in plain words."),
1240
+ limit: { type: "number", description: "How many results to return. Default 10." }
1241
+ },
1242
+ required: ["query"]
1243
+ }
1244
+ },
1245
+ {
1246
+ 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.",
1248
+ inputSchema: {
1249
+ type: "object",
1250
+ properties: {
1251
+ name: string("The component's name, such as Button or DataGrid."),
1252
+ dense: {
1253
+ type: "boolean",
1254
+ description: "The short form, which is the default. Pass false for full descriptions."
1255
+ }
1256
+ },
1257
+ required: ["name"]
1258
+ }
1259
+ },
1260
+ {
1261
+ name: "list_components",
1262
+ description: "Every OPERATOR component in one line each, with its parts.",
1263
+ inputSchema: { type: "object", properties: {} }
1264
+ },
1265
+ {
1266
+ 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
+ {
1271
+ 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.",
1273
+ inputSchema: {
1274
+ type: "object",
1275
+ properties: { code: string("The TSX source of one file.") },
1276
+ required: ["code"]
1277
+ }
1278
+ }
1279
+ ];
1280
+ function runTool(name, args, context) {
1281
+ 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
+ };
1288
+ switch (name) {
1289
+ 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}]`}
1299
+ ${next}`
1300
+ ).join("\n");
1301
+ }
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
+ case "get_instructions":
1311
+ return renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n");
1312
+ 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");
1327
+ }
1328
+ default:
1329
+ throw new UnknownTool(name);
1330
+ }
1331
+ }
1332
+ var UnknownTool = class extends Error {
1333
+ constructor(name) {
1334
+ super(
1335
+ `Unknown tool ${JSON.stringify(name)}. Its tools: ${TOOLS.map((tool) => tool.name).join(", ")}.`
1336
+ );
1337
+ }
1338
+ };
1339
+ function handleMessage(raw, context) {
1340
+ const failure = (id2, code, message) => ({
1341
+ jsonrpc: "2.0",
1342
+ id: id2 ?? null,
1343
+ error: { code, message }
1344
+ });
1345
+ if (typeof raw !== "object" || raw === null) return failure(null, -32700, "Parse error");
1346
+ const { id, method, params } = raw;
1347
+ if (typeof method !== "string") return failure(id, -32600, "Invalid request");
1348
+ if (id === void 0) return null;
1349
+ const result = (value) => ({ jsonrpc: "2.0", id, result: value });
1350
+ switch (method) {
1351
+ case "initialize": {
1352
+ const asked = params?.protocolVersion;
1353
+ return result({
1354
+ protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
1355
+ 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."
1358
+ });
1359
+ }
1360
+ case "ping":
1361
+ return result({});
1362
+ case "tools/list":
1363
+ return result({ tools: TOOLS });
1364
+ case "tools/call": {
1365
+ const { name, arguments: args } = params ?? {};
1366
+ try {
1367
+ return result({ content: [text(runTool(String(name), args ?? {}, context))] });
1368
+ } catch (error) {
1369
+ if (error instanceof UnknownTool) return failure(id, -32602, error.message);
1370
+ return result({
1371
+ content: [text(error instanceof Error ? error.message : String(error))],
1372
+ isError: true
1373
+ });
1374
+ }
1375
+ }
1376
+ default:
1377
+ return failure(id, -32601, `Method not found: ${method}`);
1378
+ }
1379
+ }
1380
+ function serve(input, output, context) {
1381
+ const lines = (0, import_node_readline.createInterface)({ input, crlfDelay: Infinity });
1382
+ lines.on("line", (line) => {
1383
+ if (line.trim() === "") return;
1384
+ let message;
1385
+ try {
1386
+ message = JSON.parse(line);
1387
+ } catch {
1388
+ message = line;
1389
+ }
1390
+ const response = handleMessage(message, context);
1391
+ if (response !== null) output.write(`${JSON.stringify(response)}
1392
+ `);
1393
+ });
1394
+ return new Promise((done) => lines.on("close", done));
1395
+ }
1396
+
1397
+ // src/index.ts
1398
+ var VERSION = package_default.version;
1399
+ function loadRegistry() {
1400
+ const here = (0, import_node_path6.dirname)((0, import_node_url3.fileURLToPath)(importMetaUrl));
1401
+ const candidates = [(0, import_node_path6.join)(here, "registry.json"), (0, import_node_path6.join)(here, "../generated/registry.json")];
1402
+ const found = candidates.find((candidate) => (0, import_node_fs6.existsSync)(candidate));
1403
+ if (found === void 0) {
1404
+ throw new Error(
1405
+ `registry.json is missing next to ${here}. Install @oprt/cli again: the registry ships beside the executable, and the executable cannot be copied without it.`
1406
+ );
1407
+ }
1408
+ return JSON.parse((0, import_node_fs6.readFileSync)(found, "utf8"));
1409
+ }
1410
+ var registry = loadRegistry();
1411
+ var loaded;
1412
+ var contract = () => loaded ??= loadContract();
1413
+ function install(options, context = {}) {
1414
+ return installSources(options, {
1415
+ cwd: context.cwd ?? process.cwd(),
1416
+ registry: context.registry ?? registry
1417
+ });
1418
+ }
1419
+ // Annotate the CommonJS export names for ESM import in node:
1420
+ 0 && (module.exports = {
1421
+ CliError,
1422
+ DEFAULT_OUT,
1423
+ TOOLS,
1424
+ VERSION,
1425
+ applyBlock,
1426
+ blockVersion,
1427
+ checkPaths,
1428
+ checkSource,
1429
+ componentDetail,
1430
+ componentIndex,
1431
+ contract,
1432
+ doctor,
1433
+ handleMessage,
1434
+ init,
1435
+ install,
1436
+ parseArgs,
1437
+ registry,
1438
+ removeBlock,
1439
+ renderAgentBlock,
1440
+ renderComponent,
1441
+ resolveEntries,
1442
+ runAgentCommand,
1443
+ search,
1444
+ serve
1445
+ });