@parity-app/cli 0.7.1 → 0.7.2

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/bin.mjs CHANGED
@@ -1,11 +1,29 @@
1
1
  #!/usr/bin/env node
2
- import { c as closest, d as readErrorDetails, l as errorCodes, s as CodedError, u as errorDetails } from "./schema-DcQLoUPP.mjs";
3
- import { a as describeExec, c as describeProps, f as setCaller, i as describeConsole, l as describeStatus, n as describeAccessibility, o as describeModes, p as mcpCallerName, r as describeComponents, s as describeProblems, t as call, u as describeTokens } from "./client-D5kbiMmn.mjs";
2
+ import { c as CodedError, d as errorDetails, f as readErrorDetails, i as jsonText, l as closest, s as plainObject, t as checkData, u as errorCodes } from "./schema-CemdtCeF.mjs";
3
+ import { _ as requests, a as describeExec, c as describeProps, f as setCaller, g as requestJsonSchema, h as replies, i as describeConsole, l as describeStatus, m as readRequest, n as describeAccessibility, o as describeModes, p as mcpCallerName, r as describeComponents, s as describeProblems, t as call, u as describeTokens, v as skillRequest } from "./client-BtxrNeIk.mjs";
4
4
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
5
  import { dirname, join, normalize, relative, resolve, sep } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
+ import { z } from "zod";
7
8
  import { createInterface } from "node:readline";
8
9
  // src/errors.ts
10
+ /**
11
+ * A failed call, the same in CLI `--json` and MCP `structuredContent`: a stable code to branch on, the message people
12
+ * read, whether the same request can succeed when sent again, the input at fault, and what to use instead. What each
13
+ * field means is in the CLI's README and the skill, too.
14
+ */
15
+ const failure = z.object({
16
+ code: z.enum(errorCodes),
17
+ message: z.string(),
18
+ /** Whether sending the same request again, unchanged, can succeed: the app was starting, or edits collided. */
19
+ retryable: z.boolean(),
20
+ /** A JSON pointer to the input at fault, such as `/design` or `/edits/0/find`, in the operation's field names. */
21
+ path: z.string().optional(),
22
+ /** Valid ids to use instead of an unknown or ambiguous one, closest first, at most 20. */
23
+ alternatives: z.array(z.string()).optional(),
24
+ /** A failed snippet's id: retry it with `editId` and `edits` instead of sending it again. */
25
+ editId: z.string().optional()
26
+ });
9
27
  /** A request field that isn't valid, rejected before anything reaches the app. */
10
28
  function invalidInput(field, message) {
11
29
  return new CodedError(message, {
@@ -47,30 +65,12 @@ function renamePath(error, names) {
47
65
  ].join("/")
48
66
  };
49
67
  }
50
- /** The error's schema. What each field means is in the CLI's README and the skill, to keep every tool's schema small. */
51
- const operationErrorSchema = {
52
- type: "object",
53
- properties: {
54
- code: {
55
- type: "string",
56
- enum: [...errorCodes]
57
- },
58
- message: { type: "string" },
59
- retryable: { type: "boolean" },
60
- path: { type: "string" },
61
- alternatives: {
62
- type: "array",
63
- items: { type: "string" }
64
- },
65
- editId: { type: "string" }
66
- },
67
- required: [
68
- "code",
69
- "message",
70
- "retryable"
71
- ]
72
- };
73
68
  // src/execute.ts
69
+ replies.exec.omit({
70
+ error: true,
71
+ details: true,
72
+ editId: true
73
+ }).extend({ error: failure.optional() });
74
74
  /** The same request fields in both adapters' paths, before MCP renames label and code. */
75
75
  const executeFields = [
76
76
  "tab",
@@ -84,11 +84,11 @@ function execResponse(reply, fields = executeFields) {
84
84
  const { error, details, editId, ...rest } = reply;
85
85
  if (reply.ok || error === void 0) return rest;
86
86
  const coded = readErrorDetails(details);
87
- const failure = operationError(coded ? new CodedError(error, coded) : new CodedError(error, { code: "execution_failed" }), fields);
87
+ const failed = operationError(coded ? new CodedError(error, coded) : new CodedError(error, { code: "execution_failed" }), fields);
88
88
  return {
89
89
  ...rest,
90
90
  error: {
91
- ...failure,
91
+ ...failed,
92
92
  ...editId ? { editId } : {}
93
93
  }
94
94
  };
@@ -100,285 +100,58 @@ function structuredExec(result) {
100
100
  screenshots: result.screenshots.map(({ png: _png, ...shot }) => shot)
101
101
  };
102
102
  }
103
- const screenshotSchema = {
104
- type: "object",
105
- properties: {
106
- path: {
107
- type: "string",
108
- description: "Where the PNG was saved."
109
- },
110
- design: { type: "string" },
111
- state: { type: "string" },
112
- mode: { type: "string" }
113
- },
114
- required: [
115
- "path",
116
- "design",
117
- "state",
118
- "mode"
119
- ]
120
- };
121
- const strings$1 = {
122
- type: "array",
123
- items: { type: "string" }
124
- };
125
- const discardedSchema = {
126
- type: "array",
127
- description: "Patches each Replace threw away with the node it swapped out. Present only when there were some; reapply what's still wanted to the replacement with Responsive, Update with a state, or UpdateState.",
128
- items: {
129
- type: "object",
130
- properties: {
131
- design: { type: "string" },
132
- replaced: {
133
- type: "string",
134
- description: "The node Replace swapped out."
135
- },
136
- replacement: {
137
- type: "string",
138
- description: "The node that took its place."
139
- },
140
- nodes: {
141
- type: "array",
142
- items: {
143
- type: "object",
144
- properties: {
145
- id: {
146
- type: "string",
147
- description: "The old node's id, or instance/inner path."
148
- },
149
- name: { type: "string" },
150
- viewports: {
151
- ...strings$1,
152
- description: "Viewport ids it had responsive patches for."
153
- },
154
- states: {
155
- ...strings$1,
156
- description: "State ids it had patches in."
157
- },
158
- variants: {
159
- ...strings$1,
160
- description: "Variant options it had patches in, as param=option."
161
- }
162
- },
163
- required: ["id"]
164
- }
165
- }
166
- },
167
- required: [
168
- "design",
169
- "replaced",
170
- "replacement",
171
- "nodes"
172
- ]
173
- }
174
- };
175
- const warningGroupsSchema = {
176
- type: "array",
177
- description: "The warnings as data, in the same order: each finding once, with how many places and which. Errors come first, then warnings, then suggestions.",
178
- items: {
179
- type: "object",
180
- properties: {
181
- category: {
182
- type: "string",
183
- description: "Stable: render_failed, unknown_component, unknown_component_design, unknown_prop, invalid_option, not_a_token, not_a_length_token, overlap, title_clearance, canvas_overlaps, kind_changed or unnamed_layout. overlap and title_clearance cover boards this call created, changed or moved: overlap when two boards collide, title_clearance when they're clear but one's title covers the other. canvas_overlaps counts the rest of the canvas's; call Overlaps() to check every board."
184
- },
185
- severity: {
186
- type: "string",
187
- enum: [
188
- "error",
189
- "warning",
190
- "suggestion"
191
- ],
192
- description: "error: a render failed, so a check or screenshot is missing. suggestion: worth doing, not wrong."
193
- },
194
- message: {
195
- type: "string",
196
- description: "What's wrong and what to do about it."
197
- },
198
- count: {
199
- type: "integer",
200
- minimum: 1,
201
- description: "How many places it was found."
202
- },
203
- at: {
204
- ...strings$1,
205
- description: "Every place: a node's design/node path, or a frame. Empty for a whole board."
206
- }
207
- },
208
- required: [
209
- "category",
210
- "severity",
211
- "message",
212
- "count",
213
- "at"
214
- ]
215
- }
216
- };
217
- /** Execute's and screenshot's structured result. */
218
- const execOutputSchema = {
219
- type: "object",
220
- properties: {
221
- ok: { type: "boolean" },
222
- applied: {
223
- type: "boolean",
224
- description: "Changes landed on the canvas as one undoable step."
225
- },
226
- output: {
227
- type: "array",
228
- items: { type: "string" },
229
- description: "Lines from Print."
230
- },
231
- warnings: {
232
- ...strings$1,
233
- description: "One line per warning group, places first."
234
- },
235
- warningGroups: warningGroupsSchema,
236
- created: {
237
- type: "object",
238
- additionalProperties: { type: "string" },
239
- description: "Names of designs and nodes created, mapped to their ids."
240
- },
241
- discarded: discardedSchema,
242
- screenshots: {
243
- type: "array",
244
- items: screenshotSchema
245
- },
246
- error: operationErrorSchema
247
- },
248
- required: [
249
- "ok",
250
- "applied",
251
- "output",
252
- "warnings",
253
- "created",
254
- "screenshots"
255
- ]
256
- };
257
- const defaultLabel = "Edit designs";
258
- /** What every call's tab field says, the same in the CLI's help, MCP tools and the skill (PAR-514). */
259
- const tabDescription = "Tab id, project root or name, or \"active\" for what the person is looking at. Defaults to the open project the current folder is in, else \"active\".";
260
- /** Canonical properties, also used by MCP discovery after renaming label and code. */
261
- const executeProperties = {
262
- tab: {
263
- type: "string",
264
- minLength: 1,
265
- description: tabDescription
266
- },
267
- label: {
268
- type: "string",
269
- minLength: 1,
270
- default: defaultLabel,
271
- description: "Shown while it runs. Prefer 3 to 8 words, e.g. \"Add pricing tiers\"."
272
- },
273
- code: {
274
- type: "string",
275
- minLength: 1,
276
- description: "The snippet. Omit when retrying with editId."
277
- },
278
- editId: {
279
- type: "string",
280
- minLength: 1,
281
- description: "The failed call's editId, to retry with edits."
282
- },
283
- edits: {
284
- type: "array",
285
- description: "Exact find and replace edits to the failed snippet, applied in order.",
286
- items: {
287
- type: "object",
288
- additionalProperties: false,
289
- properties: {
290
- find: {
291
- type: "string",
292
- minLength: 1
293
- },
294
- replace: { type: "string" },
295
- all: { type: "boolean" }
296
- },
297
- required: ["find", "replace"]
298
- }
299
- }
300
- };
301
- function optionalText(value, field) {
302
- if (value === void 0) return void 0;
303
- if (typeof value !== "string" || !value.trim()) throw invalidInput(field, `${field} must be a nonblank string`);
304
- return value;
305
- }
306
- function edit(value, index) {
307
- const at = `edits/${index}`;
308
- if (!value || typeof value !== "object" || Array.isArray(value)) throw invalidInput(at, "Each edit must be an object");
309
- if (!("find" in value) || typeof value.find !== "string" || !value.find.length) throw invalidInput(`${at}/find`, "Each edit needs a nonempty find string");
310
- if (!("replace" in value) || typeof value.replace !== "string") throw invalidInput(`${at}/replace`, "Each edit needs a replace string");
311
- if ("all" in value && typeof value.all !== "boolean") throw invalidInput(`${at}/all`, "Edit all must be a boolean");
312
- const unknown = Object.keys(value).find((key) => ![
313
- "find",
314
- "replace",
315
- "all"
316
- ].includes(key));
317
- if (unknown) throw invalidInput(`${at}/${unknown}`, "Unknown edit field");
318
- return {
319
- find: value.find,
320
- replace: value.replace,
321
- ..."all" in value && typeof value.all === "boolean" ? { all: value.all } : {}
322
- };
323
- }
324
- /** Validate once, before either transport sends a request to the desktop. Never rewrite snippet or patch text. */
103
+ /** Checks the request once, before either transport sends it to the desktop. Snippet and patch text stay as written. */
325
104
  function executeRequest(input) {
326
- const tab = optionalText(input.tab, "tab");
327
- const label = optionalText(input.label, "label") ?? defaultLabel;
328
- const code = optionalText(input.code, "code");
329
- const editId = optionalText(input.editId, "editId");
330
- if (!code && !editId) throw invalidInput("code", "Nothing to run: pass a snippet, or an editId with edits");
331
- if (code !== void 0 && editId !== void 0) throw invalidInput("editId", "Pass a snippet or an editId, not both");
332
- if (input.edits !== void 0 && !Array.isArray(input.edits)) throw invalidInput("edits", "edits must be an array");
333
- if (input.edits !== void 0 && !editId) throw invalidInput("edits", "edits requires an editId");
334
- const edits = Array.isArray(input.edits) ? input.edits.map(edit) : void 0;
335
- return {
336
- label,
337
- ...tab === void 0 ? {} : { tab },
338
- ...code === void 0 ? {} : { code },
339
- ...editId === void 0 ? {} : {
340
- editId,
341
- edits: edits ?? []
342
- }
343
- };
105
+ return readRequest(requests.exec, input);
344
106
  }
345
107
  /** Execution failures have the same meaning for CLI exits and MCP tool errors. */
346
108
  function executionFailed(result) {
347
109
  return !result.ok;
348
110
  }
111
+ /**
112
+ * Discovery's schema: every field either kind of request takes, under the adapter's names, and which ones each kind
113
+ * needs and refuses, from the two kinds' own schemas. Clients read one object, so the kinds become `oneOf` rules on it.
114
+ */
115
+ function discoverySchema(names) {
116
+ const renamed = {
117
+ label: names.label,
118
+ code: names.code
119
+ };
120
+ const rename = (key) => renamed[key] ?? key;
121
+ const kinds = requests.exec.options.map((kind) => ({
122
+ json: requestJsonSchema(kind),
123
+ keys: Object.keys(kind.shape)
124
+ }));
125
+ const every = [...new Set(kinds.flatMap(({ keys }) => keys))];
126
+ return {
127
+ type: "object",
128
+ properties: Object.fromEntries(kinds.flatMap(({ json }) => Object.entries(json.properties ?? {})).map(([key, schema]) => [rename(key), schema])),
129
+ additionalProperties: false,
130
+ oneOf: kinds.map(({ json, keys }) => {
131
+ const refused = every.filter((key) => !keys.includes(key)).map((key) => ({ required: [rename(key)] }));
132
+ return {
133
+ required: (json.required ?? []).map(rename),
134
+ not: refused.length === 1 ? refused[0] : { anyOf: refused }
135
+ };
136
+ })
137
+ };
138
+ }
349
139
  /** One handler for both adapters. Caller identity stays in the transport's connection context. */
350
140
  const executeOperation = {
351
141
  failed: executionFailed,
352
142
  fields: executeFields,
353
- outputSchema: execOutputSchema,
354
143
  structured: structuredExec,
355
144
  /** Discovery uses the same contract, with just the two historical MCP field aliases. */
356
- inputSchema(names = {
357
- label: "label",
358
- code: "code"
359
- }) {
360
- const { label, code, ...rest } = executeProperties;
361
- return {
362
- type: "object",
363
- properties: {
364
- ...rest,
365
- [names.label]: label,
366
- [names.code]: code
367
- },
368
- oneOf: [{
369
- required: [names.code],
370
- not: { anyOf: [{ required: ["editId"] }, { required: ["edits"] }] }
371
- }, {
372
- required: ["editId"],
373
- not: { required: [names.code] }
374
- }]
375
- };
376
- },
145
+ inputSchema: discoverySchema,
377
146
  async run(input, send) {
378
- return execResponse(await send("exec", { ...executeRequest(input) }));
147
+ return execResponse(await send("exec", executeRequest(input), "tab", replies.exec));
379
148
  }
380
149
  };
381
150
  // src/check.ts
151
+ /** The registry's least share of placed parts each library must render, by library, in `in-app.json`. */
152
+ const floorsFile = jsonText(z.record(z.string(), z.number()));
153
+ /** The app's errors kept for a library, by part. */
154
+ const appErrorsFile = jsonText(z.record(z.string(), z.string()));
382
155
  /**
383
156
  * `parity lib check <library>`: inserts every component of a registry library file into the running app, once on its
384
157
  * own and once the way the Assets panel inserts it, inside its parents with its sample, and reports the parts that fail
@@ -387,8 +160,8 @@ const executeOperation = {
387
160
  */
388
161
  async function runCheck(library, options) {
389
162
  if (!library) throw new Error("parity lib check <library> [--tab t] [--mode m]");
390
- const { componentNode, generateId, librarySource, withLibraryContext } = await import("./src-_5ETJOb0.mjs");
391
- const { newestEntries, registryDir } = await import("./src-B5sEtTw_.mjs");
163
+ const { componentNode, generateId, librarySource, withLibraryContext } = await import("./src-tY0sf-AQ.mjs");
164
+ const { newestEntries, registryDir } = await import("./src-Chy2scQg.mjs");
392
165
  const entry = (await newestEntries()).find((item) => item.manifest.id === library);
393
166
  if (!entry) throw new Error(`The registry has no library "${library}"`);
394
167
  const list = entry.manifest.components?.list ?? {};
@@ -399,7 +172,8 @@ async function runCheck(library, options) {
399
172
  declaration: ""
400
173
  }]));
401
174
  const mode = options.mode ?? "desktop-light";
402
- const exec = async (label, code) => {
175
+ /** Runs one of these snippets and reads the JSON it prints, once `printed` has checked it. */
176
+ const exec = async (label, code, printed) => {
403
177
  const result = await executeOperation.run({
404
178
  tab: options.tab,
405
179
  label,
@@ -408,11 +182,13 @@ async function runCheck(library, options) {
408
182
  if (executeOperation.failed(result)) throw new Error(result.error?.message ?? `${label} failed`);
409
183
  const line = result.output.find((item) => item.startsWith("{") || item.startsWith("["));
410
184
  if (!line) throw new Error(`${label} printed nothing`);
411
- return line;
185
+ const checked = checkData(jsonText(printed), line);
186
+ if (checked.success) return checked.data;
187
+ throw new Error(`This CLI can't read what "${label}" printed (${checked.problem.pointer}: ${checked.problem.message})`);
412
188
  };
413
- const infos = JSON.parse(await exec(`Read ${library}'s props`, `const ids = ${JSON.stringify(names.map(id))}
189
+ const infos = await exec(`Read ${library}'s props`, `const ids = ${JSON.stringify(names.map(id))}
414
190
  const known = new Set(Components())
415
- Print(JSON.stringify(Object.fromEntries(ids.filter((id) => known.has(id)).map((id) => [id, Props(id, { all: true })]))))`));
191
+ Print(JSON.stringify(Object.fromEntries(ids.filter((id) => known.has(id)).map((id) => [id, Props(id, { all: true })]))))`, z.record(z.string(), replies.props));
416
192
  if (names.filter((name) => !infos[id(name)]).length === names.length) throw new Error(`The tab's project doesn't have ${library} installed`);
417
193
  const taken = /* @__PURE__ */ new Set();
418
194
  const alone = names.map((name) => {
@@ -426,18 +202,18 @@ Print(JSON.stringify(Object.fromEntries(ids.filter((id) => known.has(id)).map((i
426
202
  });
427
203
  const inside = names.filter((name) => list[name].parents?.length || list[name].sample).map((name) => withLibraryContext(componentNode(id(name), infos[id(name)], taken), sources, [], taken));
428
204
  const failing = async (design, nodes) => {
429
- const line = await exec(`Check ${design}`, `for (const design of Designs().filter((d) => d.name === ${JSON.stringify(design)})) DeleteDesign(design.id)
205
+ return failures(await exec(`Check ${design}`, `for (const design of Designs().filter((d) => d.name === ${JSON.stringify(design)})) DeleteDesign(design.id)
430
206
  const spot = FindEmptySpace({ width: 1280, height: 2000 })
431
207
  const id = CreateDesign({ name: ${JSON.stringify(design)}, x: spot.x, y: spot.y })
432
208
  const grid = Insert(id, { type: "layout", name: "Grid", style: { direction: "row", wrap: true, gap: 4, padding: 4 }, children: [] })
433
209
  for (const node of ${JSON.stringify(nodes)}) Insert(grid, node)
434
- Print(JSON.stringify(await Problems(id, { mode: ${JSON.stringify(mode)} })))`);
435
- return failures(JSON.parse(line), library);
210
+ Print(JSON.stringify(await Problems(id, { mode: ${JSON.stringify(mode)} })))`, replies.problems.shape.problems), library);
436
211
  };
437
212
  const failAlone = await failing(`${library} alone`, alone);
438
213
  const failInside = await failing(`${library} inside`, inside);
439
214
  const report = score(library, mode, names.length, failAlone, failInside, list);
440
- report.floor = JSON.parse(await readFile(join(registryDir(), "..", "in-app.json"), "utf8"))[library];
215
+ const floors = join(registryDir(), "..", "in-app.json");
216
+ report.floor = registryFile(floors, await readFile(floors, "utf8"), floorsFile)[library];
441
217
  if (options.record) {
442
218
  const kept = await recordAppErrors(registryDir(), library, report);
443
219
  console.error(`Kept ${kept} errors from the app for ${library}; run parity lib with --registry to use them.`);
@@ -452,7 +228,15 @@ function appErrorsPath(registry, library) {
452
228
  }
453
229
  /** The app's errors kept for a library, by part, or none. */
454
230
  async function readAppErrors(registry, library) {
455
- return readFile(appErrorsPath(registry, library), "utf8").then((text) => JSON.parse(text), () => void 0);
231
+ const path = appErrorsPath(registry, library);
232
+ const text = await readFile(path, "utf8").catch(() => void 0);
233
+ return text === void 0 ? void 0 : registryFile(path, text, appErrorsFile);
234
+ }
235
+ /** One of the registry's own JSON files once `schema` has checked it, or an error naming the file and what's wrong. */
236
+ function registryFile(path, text, schema) {
237
+ const checked = checkData(schema, text);
238
+ if (checked.success) return checked.data;
239
+ throw new TypeError(`${path}: ${checked.problem.pointer || "/"}: ${checked.problem.message}`);
456
240
  }
457
241
  /**
458
242
  * Adds the parts that failed in the app to those kept before, with their own errors rather than a parent's: one kept
@@ -511,8 +295,8 @@ function describeCheck(report) {
511
295
  * because it reads the project's files and its own copy of TypeScript.
512
296
  */
513
297
  async function runLib(packageName, options) {
514
- const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-DM51TWZ6.mjs");
515
- const { covers, registryDir, registryEntries } = await import("./src-B5sEtTw_.mjs");
298
+ const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-CPka3fhs.mjs");
299
+ const { covers, registryDir, registryEntries } = await import("./src-Chy2scQg.mjs");
516
300
  const spec = packageName === void 0 ? void 0 : packageSpec(packageName);
517
301
  let root = resolve(options.root ?? ".");
518
302
  if (spec && (spec.version !== void 0 || options.registry && !options.root)) {
@@ -523,7 +307,7 @@ async function runLib(packageName, options) {
523
307
  console.error(`Installing ${spec.name} ${version} on its own…`);
524
308
  root = await installedVersion(spec.name, version);
525
309
  }
526
- const { parseLibraryEdits } = await import("./src-DM51TWZ6.mjs");
310
+ const { parseLibraryEdits } = await import("./src-CPka3fhs.mjs");
527
311
  const trialEdits = options.edits ? parseLibraryEdits(JSON.parse(await readFile(resolve(options.edits), "utf8"))) : void 0;
528
312
  const recordedId = options.registry && spec ? options.id ?? (await registryEntries()).find((entry) => entry.manifest.package?.name === spec.name)?.manifest.id : void 0;
529
313
  const appErrors = recordedId ? await readAppErrors(registryDir(), recordedId) : void 0;
@@ -548,7 +332,7 @@ async function runLib(packageName, options) {
548
332
  else target = await appManifestPath(root);
549
333
  const existing = target ? await readFile(target, "utf8").then((text) => JSON.parse(text), () => void 0) : void 0;
550
334
  if (!options.range) {
551
- const read = generated.manifest.package;
335
+ const read = packageVersions.safeParse(generated.manifest.package).data;
552
336
  const range = read && keptRange(existing, read, (versions) => covers({ package: versions }, read.version));
553
337
  if (range) generated.manifest.package = {
554
338
  ...read,
@@ -580,13 +364,21 @@ async function runLib(packageName, options) {
580
364
  }, null, 2));
581
365
  else console.log(`Wrote ${relative(process.cwd(), target) || target}\n${describeReport(report)}`);
582
366
  }
367
+ /** The package a library file describes: the version read, and the range of versions the file covers. */
368
+ const packageVersions = z.looseObject({
369
+ name: z.string(),
370
+ version: z.string(),
371
+ range: z.string()
372
+ });
373
+ /** The range a file being written over has, which it keeps while that still covers the version read. */
374
+ const writtenRange = z.looseObject({ package: z.looseObject({ range: z.string() }) });
583
375
  /**
584
376
  * The range a file being written over keeps: its own, while it still covers the version just read, so a range widened
585
377
  * by hand (Lucide's, from 0.400) survives regenerating. Undefined when the new range stands.
586
378
  */
587
379
  function keptRange(existing, read, covers) {
588
- const range = existing?.package?.range;
589
- if (typeof range !== "string" || range === read.range) return void 0;
380
+ const range = writtenRange.safeParse(existing).data?.package.range;
381
+ if (range === void 0 || range === read.range) return void 0;
590
382
  return covers({
591
383
  ...read,
592
384
  range
@@ -674,140 +466,20 @@ function describeReport(report) {
674
466
  return lines.join("\n");
675
467
  }
676
468
  // src/operations.ts
677
- function text$1(description, required = false, fallback) {
678
- return {
679
- schema: {
680
- type: "string",
681
- minLength: 1,
682
- pattern: "\\S",
683
- description,
684
- ...fallback === void 0 ? {} : { default: fallback }
685
- },
686
- required,
687
- read(value, name) {
688
- if (value === void 0 && !required) return fallback;
689
- if (typeof value !== "string" || !value.trim()) throw invalidInput(name, `${name} must be a nonblank string`);
690
- return value;
691
- }
692
- };
693
- }
694
- function boolean(description) {
695
- return {
696
- schema: {
697
- type: "boolean",
698
- default: false,
699
- description
700
- },
701
- read(value, name) {
702
- if (value === void 0) return false;
703
- if (typeof value !== "boolean") throw invalidInput(name, `${name} must be a boolean`);
704
- return value;
705
- }
706
- };
707
- }
708
- function selector(description, fallback) {
709
- const item = {
710
- type: "string",
711
- minLength: 1,
712
- pattern: "\\S"
713
- };
714
- return {
715
- schema: {
716
- oneOf: [item, {
717
- type: "array",
718
- minItems: 1,
719
- items: item
720
- }],
721
- description,
722
- ...fallback ? { default: fallback } : {}
723
- },
724
- read(value, name) {
725
- if (value === void 0) return fallback;
726
- if (typeof value === "string" && value.trim()) return value;
727
- if (Array.isArray(value) && value.length && value.every((item) => typeof item === "string" && item.trim())) return value.length === 1 ? value[0] : value;
728
- throw invalidInput(name, `${name} must be a nonblank string or a nonempty list of nonblank strings`);
729
- }
730
- };
731
- }
732
- /** How many items a list read returns at most: a default that keeps a reply small, and a ceiling. */
733
- function limit(description, fallback, most) {
734
- return {
735
- schema: {
736
- type: "integer",
737
- minimum: 1,
738
- maximum: most,
739
- default: fallback,
740
- description
741
- },
742
- read(value, name) {
743
- if (value === void 0) return fallback;
744
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > most) throw invalidInput(name, `${name} must be a whole number from 1 to ${most}`);
745
- return value;
746
- }
747
- };
748
- }
749
- const tab = text$1(tabDescription);
750
- const design = text$1("Design id or name.", true);
751
- const scale = {
752
- schema: {
753
- type: "number",
754
- minimum: .25,
755
- maximum: 3,
756
- default: 1,
757
- description: "Pixel density."
758
- },
759
- read(value) {
760
- if (value === void 0) return 1;
761
- if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 3) throw invalidInput("scale", "scale must be a finite number from 0.25 to 3");
762
- return value;
763
- }
764
- };
765
- const rootText = text$1("Absolute project folder path.", true);
766
- const absoluteRoot = /^(?:\/|[A-Za-z]:[\\/]|\\\\)/;
767
- const root$1 = {
768
- ...rootText,
769
- schema: {
770
- ...rootText.schema,
771
- pattern: absoluteRoot.source
772
- },
773
- read(value) {
774
- const path = rootText.read(value, "root");
775
- if (typeof path !== "string" || !absoluteRoot.test(path)) throw invalidInput("root", "root must be an absolute project folder path");
776
- return path;
777
- }
778
- };
779
- function fieldsContract(fields) {
780
- return {
781
- inputSchema: {
782
- type: "object",
783
- additionalProperties: false,
784
- properties: Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.schema])),
785
- required: Object.entries(fields).filter(([, field]) => field.required).map(([key]) => key)
786
- },
787
- request(input) {
788
- if (!input || typeof input !== "object" || Array.isArray(input)) throw invalidInput(void 0, "Operation input must be an object");
789
- for (const key of Object.keys(input)) if (!Object.hasOwn(fields, key)) throw invalidInput(key, `Unknown input field ${key}`);
790
- const result = {};
791
- for (const [key, field] of Object.entries(fields)) {
792
- const value = field.read(input[key], key);
793
- if (value !== void 0) result[key] = value;
794
- }
795
- return result;
796
- }
797
- };
798
- }
799
- /** An operation whose canonical result is built from the app's reply, given the validated request. */
800
- function mapped(method, fields, outputSchema, result, options = {}) {
801
- const contract = fieldsContract(fields);
469
+ /**
470
+ * An operation whose canonical result is built from the app's checked answer, given the checked request. A request's
471
+ * error points at the field, since an operation's fields are flat.
472
+ */
473
+ function mapped(method, input, reply, result, options = {}) {
474
+ const fields = Object.keys(input.shape);
802
475
  const routing = routingFor(fields);
803
476
  return {
804
- inputSchema: contract.inputSchema,
805
- outputSchema,
806
- fields: Object.keys(fields),
477
+ inputSchema: requestJsonSchema(input),
478
+ fields,
807
479
  routing,
808
- async run(input, send) {
809
- const request = contract.request(input);
810
- return result(await send(method, request, routing), request);
480
+ async run(given, send) {
481
+ const request = readRequest(input, given, 1);
482
+ return result(await send(method, request, routing, reply), request);
811
483
  },
812
484
  failed: options.failed ?? (() => false),
813
485
  structured: options.structured ?? ((value) => Object.fromEntries(Object.entries(value)))
@@ -815,13 +487,13 @@ function mapped(method, fields, outputSchema, result, options = {}) {
815
487
  }
816
488
  /** Which copy a call goes to, from its fields: see `Operation.routing`. */
817
489
  function routingFor(fields) {
818
- if ("tab" in fields) return "tab";
819
- if ("root" in fields) return "open";
490
+ if (fields.includes("tab")) return "tab";
491
+ if (fields.includes("root")) return "open";
820
492
  return "copies";
821
493
  }
822
- /** An operation whose canonical result is the app's reply. */
823
- function operation(method, fields, outputSchema, options = {}) {
824
- return mapped(method, fields, outputSchema, (reply) => reply, options);
494
+ /** An operation whose canonical result is the app's answer. */
495
+ function operation(method, input, reply, options = {}) {
496
+ return mapped(method, input, reply, (answer) => answer, options);
825
497
  }
826
498
  /** The first `count` items, with how many there were, so a reply says when it left some out. */
827
499
  function bounded(items, count, keep = "first") {
@@ -832,660 +504,56 @@ function bounded(items, count, keep = "first") {
832
504
  truncated: kept.length < items.length
833
505
  };
834
506
  }
835
- const string = { type: "string" };
836
- const strings = {
837
- type: "array",
838
- items: string
839
- };
840
- const integer = { type: "integer" };
841
- const boolean_ = { type: "boolean" };
842
- function object(properties, optional = []) {
843
- return {
844
- type: "object",
845
- properties,
846
- required: Object.keys(properties).filter((key) => !optional.includes(key))
847
- };
848
- }
849
- const ref = object({
850
- id: string,
851
- name: string
852
- });
853
- const tabSchema = object({
854
- id: string,
855
- name: string,
856
- root: string,
857
- active: boolean_,
858
- status: {
859
- type: "string",
860
- enum: [
861
- "loading",
862
- "ready",
863
- "error"
864
- ]
865
- },
866
- error: string,
867
- designs: {
868
- ...integer,
869
- description: "How many designs the project has."
870
- }
871
- }, ["error"]);
872
- const frameSchema = {
873
- state: string,
874
- mode: string
875
- };
876
- const number = { type: "number" };
877
- const boxSchema = object({
878
- x: number,
879
- y: number,
880
- width: number,
881
- height: number
882
- });
883
- const designSchema = object({
884
- id: string,
885
- name: string,
886
- kind: string,
887
- file: string,
888
- x: { type: "number" },
889
- y: { type: "number" },
890
- width: { type: "number" },
891
- height: { type: "number" },
892
- states: {
893
- type: "array",
894
- items: ref
895
- },
896
- showing: object({
897
- state: string,
898
- mode: string,
899
- beside: strings
900
- }),
901
- nodes: integer
902
- }, ["width", "height"]);
903
- const stateSchema = object({
904
- tab: tabSchema,
905
- selection: object({
906
- design: string,
907
- node: string,
908
- mode: string
909
- }, [
910
- "design",
911
- "node",
912
- "mode"
913
- ]),
914
- designs: {
915
- type: "array",
916
- items: designSchema
917
- },
918
- overlaps: {
919
- type: "array",
920
- description: "Every pair of boards on the canvas that's too close, as execute's Overlaps() lists them. A pair with titleOnly doesn't collide: the lower board's title covers the upper one.",
921
- items: object({
922
- designs: strings,
923
- names: strings,
924
- width: {
925
- ...number,
926
- description: "How far they share, title band included."
927
- },
928
- height: number,
929
- titleOnly: {
930
- ...boolean_,
931
- description: "Only the lower board's title covers the other; the boards are clear."
932
- },
933
- estimated: {
934
- ...boolean_,
935
- description: "One isn't drawn yet, so its size is estimated from its frames. Absent: both measured."
936
- }
937
- }, [
938
- "width",
939
- "height",
940
- "titleOnly",
941
- "estimated"
942
- ])
943
- },
944
- modes: {
945
- type: "array",
946
- description: "The project's modes, primary first.",
947
- items: object({
948
- id: string,
949
- name: string,
950
- width: { type: "number" },
951
- primary: {
952
- ...boolean_,
953
- description: "The project's first mode: every design's main frame unless it picks another. Only one is."
954
- }
955
- })
956
- },
957
- components: strings,
958
- tokens: object({
959
- count: integer,
960
- semantic: strings,
961
- themeCategories: strings
962
- }),
963
- viewport: { type: "object" },
964
- detection: { type: "object" }
965
- }, ["viewport", "detection"]);
966
507
  const truncation = {
967
- total: {
968
- ...integer,
969
- description: "How many there were before the limit."
970
- },
971
- truncated: {
972
- ...boolean_,
973
- description: "Some were left out; narrow the read or raise limit."
974
- }
508
+ total: z.int().describe("How many there were before the limit."),
509
+ truncated: z.boolean().describe("Some were left out; narrow the read or raise limit.")
975
510
  };
976
- const tokensSchema = object({
977
- tokens: {
978
- type: "array",
979
- items: object({
980
- name: string,
981
- category: string,
982
- source: string,
983
- values: {
984
- type: "object",
985
- additionalProperties: string,
986
- description: "Value per theme."
987
- }
988
- })
989
- },
511
+ z.looseObject({ tabs: replies.tabs });
512
+ replies.console.extend({
513
+ entries: replies.console.shape.entries.describe("The newest entries, up to the limit, oldest first."),
990
514
  ...truncation
991
515
  });
992
- const consoleSchema = object({
993
- tab: ref,
994
- entries: {
995
- type: "array",
996
- items: object({
997
- level: {
998
- type: "string",
999
- enum: [
1000
- "info",
1001
- "warning",
1002
- "error",
1003
- "debug"
1004
- ]
1005
- },
1006
- text: string,
1007
- source: string,
1008
- line: integer,
1009
- where: {
1010
- type: "string",
1011
- enum: ["canvas", "offscreen"]
1012
- },
1013
- count: integer,
1014
- first: { type: "number" },
1015
- last: { type: "number" }
1016
- })
1017
- },
516
+ z.looseObject({
517
+ tokens: replies.tokens,
1018
518
  ...truncation
1019
519
  });
1020
- const problemsSchema = object({
1021
- tab: ref,
1022
- design: ref,
1023
- problems: {
1024
- type: "array",
1025
- items: object({
1026
- ...frameSchema,
1027
- kind: {
1028
- type: "string",
1029
- enum: [
1030
- "render-error",
1031
- "unknown-component",
1032
- "content-wider",
1033
- "frame-overflow",
1034
- "clipped"
1035
- ]
1036
- },
1037
- problem: string,
1038
- node: string,
1039
- name: string,
1040
- box: boxSchema,
1041
- container: object({
1042
- node: string,
1043
- name: string,
1044
- box: boxSchema
1045
- }, ["node", "name"]),
1046
- overflow: object({
1047
- left: number,
1048
- right: number,
1049
- top: number,
1050
- bottom: number
1051
- }, [
1052
- "left",
1053
- "right",
1054
- "top",
1055
- "bottom"
1056
- ])
1057
- }, [
1058
- "kind",
1059
- "node",
1060
- "name",
1061
- "box",
1062
- "container",
1063
- "overflow"
1064
- ])
1065
- },
1066
- frames: integer,
1067
- warnings: strings
1068
- });
1069
- const accessibilitySchema = object({
1070
- tab: ref,
1071
- design: ref,
1072
- findings: {
1073
- type: "array",
1074
- items: object({
1075
- ...frameSchema,
1076
- rule: string,
1077
- impact: {
1078
- type: "string",
1079
- enum: [
1080
- "critical",
1081
- "serious",
1082
- "moderate",
1083
- "minor"
1084
- ]
1085
- },
1086
- help: string,
1087
- helpUrl: string,
1088
- node: string,
1089
- name: string,
1090
- detail: string
1091
- }, ["node", "name"])
1092
- },
1093
- frames: integer,
1094
- warnings: strings
1095
- });
1096
- const componentsSchema = object({
1097
- components: strings,
1098
- iconLibraries: strings,
1099
- leftOut: {
1100
- type: "array",
1101
- items: object({
1102
- name: string,
1103
- path: string,
1104
- reason: string,
1105
- undecided: boolean_
1106
- }, ["undecided"])
1107
- },
1108
- drawn: integer,
1109
- failing: {
1110
- type: "array",
1111
- items: object({
1112
- id: string,
1113
- problem: {
1114
- type: "string",
1115
- enum: [
1116
- "provider",
1117
- "server",
1118
- "async",
1119
- "error"
1120
- ]
1121
- },
1122
- reason: string
1123
- })
1124
- },
1125
- corrections: { type: "object" }
1126
- }, [
1127
- "iconLibraries",
1128
- "leftOut",
1129
- "drawn",
1130
- "failing",
1131
- "corrections"
1132
- ]);
1133
- const propsSchema = object({
1134
- component: string,
1135
- path: string,
1136
- import: string,
1137
- known: boolean_,
1138
- props: {
1139
- type: "array",
1140
- items: object({
1141
- name: string,
1142
- kind: string,
1143
- group: string,
1144
- options: {
1145
- type: "array",
1146
- items: { type: [
1147
- "string",
1148
- "number",
1149
- "boolean"
1150
- ] }
1151
- },
1152
- default: { type: [
1153
- "string",
1154
- "number",
1155
- "boolean",
1156
- "null"
1157
- ] },
1158
- required: boolean_
1159
- }, [
1160
- "group",
1161
- "options",
1162
- "default",
1163
- "required"
1164
- ])
1165
- },
1166
- composition: object({
1167
- role: {
1168
- type: "string",
1169
- enum: [
1170
- "provider",
1171
- "wrapper",
1172
- "component"
1173
- ],
1174
- description: "provider draws nothing and gives parts inside it context; wrapper draws and holds other parts."
1175
- },
1176
- parents: object({
1177
- status: {
1178
- type: "string",
1179
- enum: [
1180
- "listed",
1181
- "none",
1182
- "unknown"
1183
- ],
1184
- description: "listed: insert it inside chain; none: it needs nothing; unknown: nothing says, see reason."
1185
- },
1186
- chain: {
1187
- type: "array",
1188
- description: "Outermost first.",
1189
- items: object({
1190
- component: string,
1191
- optional: boolean_
1192
- }, ["optional"])
1193
- },
1194
- from: {
1195
- ...string,
1196
- description: "What said so, such as the library's examples or the error it throws."
1197
- },
1198
- reason: string
1199
- }, [
1200
- "chain",
1201
- "from",
1202
- "reason"
1203
- ]),
1204
- holds: {
1205
- ...strings,
1206
- description: "Parts that go directly inside it."
1207
- },
1208
- slots: {
1209
- type: "array",
1210
- items: object({
1211
- name: string,
1212
- key: string,
1213
- kind: {
1214
- type: "string",
1215
- enum: [
1216
- "children",
1217
- "element",
1218
- "render"
1219
- ]
1220
- }
1221
- })
1222
- },
1223
- example: object({
1224
- node: {
1225
- type: "object",
1226
- description: "A node Insert takes as it is: the component inside its parents."
1227
- },
1228
- status: {
1229
- type: "string",
1230
- enum: [
1231
- "drawn",
1232
- "failing",
1233
- "untried"
1234
- ],
1235
- description: "Whether the component has drawn on the canvas."
1236
- },
1237
- reason: string
1238
- }, ["reason"])
1239
- })
1240
- }, [
1241
- "path",
1242
- "import",
1243
- "composition"
1244
- ]);
1245
- const viewportSchema = object({
1246
- id: string,
1247
- name: string,
1248
- width: { type: "number" },
1249
- height: { type: "number" },
1250
- device: string
1251
- }, ["device"]);
1252
- const frameReportSchema = object({
1253
- mode: string,
1254
- width: { type: "number" },
1255
- height: {
1256
- type: "number",
1257
- description: "Fixed frame height, which clips; absent when it grows."
1258
- },
1259
- window: object({
1260
- width: { type: "number" },
1261
- height: { type: "number" }
1262
- })
1263
- }, ["height"]);
1264
- const modesSchema = object({
1265
- tab: ref,
1266
- variables: {
1267
- type: "array",
1268
- items: object({
1269
- id: string,
1270
- name: string,
1271
- kind: {
1272
- type: "string",
1273
- enum: [
1274
- "viewport",
1275
- "theme",
1276
- "locale",
1277
- "custom"
1278
- ]
1279
- },
1280
- options: {
1281
- type: "array",
1282
- items: { type: "object" }
1283
- }
1284
- })
1285
- },
1286
- viewports: {
1287
- type: "array",
1288
- items: viewportSchema,
1289
- description: "Viewport ids, which Responsive and a node's responsive patches take. Not mode ids."
1290
- },
1291
- modes: {
1292
- type: "array",
1293
- description: "Mode ids, which renders (screenshot, problems, accessibility) and a design's modes take.",
1294
- items: object({
1295
- id: string,
1296
- name: string,
1297
- primary: boolean_,
1298
- viewport: string,
1299
- width: { type: "number" },
1300
- height: { type: "number" },
1301
- values: {
1302
- type: "object",
1303
- additionalProperties: string
1304
- },
1305
- classNames: strings,
1306
- attributes: {
1307
- type: "object",
1308
- additionalProperties: string
1309
- }
1310
- })
1311
- },
1312
- designs: {
1313
- type: "array",
1314
- items: object({
1315
- id: string,
1316
- name: string,
1317
- state: string,
1318
- states: strings,
1319
- main: string,
1320
- beside: strings,
1321
- frames: {
1322
- type: "array",
1323
- items: frameReportSchema
1324
- }
1325
- }, ["frames"])
1326
- }
1327
- });
1328
- const historySide = object({
1329
- count: integer,
1330
- labels: {
1331
- ...strings,
1332
- description: "With summary: newest first, up to limit."
1333
- }
1334
- }, ["labels"]);
1335
- const statusSchema = object({
1336
- tab: ref,
1337
- revision: {
1338
- type: "string",
1339
- description: "Opaque; the same only while nothing in the tab has changed. Compare it with state's revision."
1340
- },
1341
- dirty: {
1342
- ...boolean_,
1343
- description: "Something applied (and undoable) isn't on disk yet."
1344
- },
1345
- autosave: {
1346
- ...boolean_,
1347
- description: "Changes save themselves; otherwise they wait for the person's Save."
1348
- },
1349
- unsaved: {
1350
- type: "array",
1351
- items: object({
1352
- path: string,
1353
- kind: {
1354
- type: "string",
1355
- enum: ["document", "stylesheet"]
1356
- },
1357
- designs: object({
1358
- added: strings,
1359
- changed: strings,
1360
- removed: strings
1361
- }),
1362
- modes: boolean_,
1363
- tokens: strings,
1364
- more: integer
1365
- }, [
1366
- "designs",
1367
- "modes",
1368
- "tokens",
1369
- "more"
1370
- ])
1371
- },
1372
- tokenEdits: {
1373
- ...integer,
1374
- description: "Token values waiting for Save to write them to the stylesheet."
1375
- },
1376
- save: object({
1377
- status: {
1378
- type: "string",
1379
- enum: [
1380
- "saved",
1381
- "unsaved",
1382
- "saving",
1383
- "failed"
1384
- ]
1385
- },
1386
- error: string
1387
- }, ["error"]),
1388
- errors: {
1389
- type: "array",
1390
- items: object({
1391
- path: string,
1392
- error: string
1393
- })
1394
- },
1395
- undo: historySide,
1396
- redo: historySide
1397
- }, []);
1398
- const checks = {
1399
- tab,
1400
- design,
1401
- state: selector("State id, list of ids, or \"all\".", "all"),
1402
- mode: selector("Mode id, list of ids, \"shown\" or \"all\".", "shown")
1403
- };
1404
520
  const operations = {
1405
- tabs: mapped("tabs", {}, object({ tabs: {
1406
- type: "array",
1407
- items: tabSchema
1408
- } }), (tabs) => ({ tabs })),
1409
- state: operation("state", { tab }, stateSchema),
1410
- status: operation("status", {
1411
- tab,
1412
- summary: boolean("Add each unsaved file's designs and token values, and the undo and redo labels."),
1413
- limit: limit("Most ids or labels in each list of the summary.", 10, 100)
1414
- }, statusSchema),
1415
- modes: operation("modes", {
1416
- tab,
1417
- design: text$1("Design id or name. With it, just that design, with each of its frames' sizes.")
1418
- }, modesSchema),
1419
- open: operation("open", { root: root$1 }, tabSchema),
1420
- screenshot: mapped("screenshot", {
1421
- tab,
1422
- design,
1423
- scale,
1424
- state: selector("State id, list of ids, or \"all\". Defaults to the state the design shows."),
1425
- mode: selector("Mode id, list of ids, \"shown\" or \"all\". Defaults to the primary mode.")
1426
- }, execOutputSchema, (reply) => execResponse(reply, [
1427
- "tab",
1428
- "design",
1429
- "scale",
1430
- "state",
1431
- "mode"
1432
- ]), {
521
+ tabs: mapped("tabs", requests.tabs, replies.tabs, (tabs) => ({ tabs })),
522
+ state: operation("state", requests.state, replies.state),
523
+ status: operation("status", requests.status, replies.status),
524
+ modes: operation("modes", requests.modes, replies.modes),
525
+ open: operation("open", requests.open, replies.open),
526
+ screenshot: mapped("screenshot", requests.screenshot, replies.screenshot, (reply) => execResponse(reply, Object.keys(requests.screenshot.shape)), {
1433
527
  failed: executionFailed,
1434
528
  structured: structuredExec
1435
529
  }),
1436
- problems: operation("problems", checks, problemsSchema, { failed: (result) => result.problems.length > 0 }),
1437
- accessibility: operation("accessibility", checks, accessibilitySchema, { failed: (result) => result.findings.length > 0 }),
1438
- console: mapped("console", {
1439
- tab,
1440
- clear: boolean("Empty the console after reading, including entries beyond limit."),
1441
- limit: limit("Most entries to return, newest kept.", 50, 1e3)
1442
- }, consoleSchema, ({ tab: from, entries }, request) => {
1443
- const { items, total, truncated } = bounded(entries, Number(request.limit), "last");
530
+ problems: operation("problems", requests.problems, replies.problems, { failed: (result) => result.problems.length > 0 }),
531
+ accessibility: operation("accessibility", requests.accessibility, replies.accessibility, { failed: (result) => result.findings.length > 0 }),
532
+ console: mapped("console", requests.console, replies.console, ({ tab, entries }, request) => {
533
+ const { items, total, truncated } = bounded(entries, request.limit, "last");
1444
534
  return {
1445
- tab: from,
535
+ tab,
1446
536
  entries: items,
1447
537
  total,
1448
538
  truncated
1449
539
  };
1450
540
  }),
1451
- tokens: mapped("tokens", {
1452
- tab,
1453
- category: text$1("Token category."),
1454
- source: text$1("Token source, such as semantic or theme."),
1455
- search: {
1456
- schema: {
1457
- type: "string",
1458
- description: "Token name search."
1459
- },
1460
- read(value) {
1461
- if (value !== void 0 && typeof value !== "string") throw invalidInput("search", "search must be a string");
1462
- return value;
1463
- }
1464
- },
1465
- limit: limit("Most tokens to return.", 100, 1e3)
1466
- }, tokensSchema, (tokens, request) => {
1467
- const { items, total, truncated } = bounded(tokens, Number(request.limit));
541
+ tokens: mapped("tokens", requests.tokens, replies.tokens, (tokens, request) => {
542
+ const { items, total, truncated } = bounded(tokens, request.limit);
1468
543
  return {
1469
544
  tokens: items,
1470
545
  total,
1471
546
  truncated
1472
547
  };
1473
548
  }),
1474
- components: operation("components", { tab }, componentsSchema),
1475
- props: operation("props", {
1476
- tab,
1477
- component: text$1("Component id or name.", true),
1478
- all: boolean("Include passed-through attributes.")
1479
- }, propsSchema)
549
+ components: operation("components", requests.components, replies.components),
550
+ props: operation("props", requests.props, replies.props)
1480
551
  };
1481
- const skill = fieldsContract({ path: text$1("Skill file to read.", false, "SKILL.md") });
1482
552
  const skillOperation = {
1483
- inputSchema: skill.inputSchema,
1484
- fields: ["path"],
553
+ inputSchema: requestJsonSchema(skillRequest),
554
+ fields: Object.keys(skillRequest.shape),
1485
555
  async run(input, read) {
1486
- const { path } = skill.request(input);
1487
- if (typeof path !== "string") throw invalidInput("path", "path must be a nonblank string");
1488
- return read(path);
556
+ return read(readRequest(skillRequest, input, 1).path);
1489
557
  }
1490
558
  };
1491
559
  // src/skills.ts
@@ -1516,7 +584,7 @@ async function list(directory = root) {
1516
584
  *
1517
585
  * Tools publish no outputSchema (PAR-510). Claude Code never shows one to the model, Codex's code mode puts it in the
1518
586
  * model's context, and with them tools/list was 33 KB rather than 11. structuredContent needs no schema; the contracts
1519
- * keep theirs for CLI --json and the tests.
587
+ * keep theirs, which the CLI checks the app's answers with.
1520
588
  */
1521
589
  function operationTool(name, description, operation, format) {
1522
590
  return {
@@ -1541,6 +609,17 @@ const executeNames = {
1541
609
  label: "description",
1542
610
  code: "input"
1543
611
  };
612
+ /** MCP's execute arguments under the contract's names: description is label and input is code. */
613
+ function contractNames(input) {
614
+ const fields = plainObject.safeParse(input).data;
615
+ if (!fields) return input;
616
+ const { description: label, input: code, ...rest } = fields;
617
+ return {
618
+ ...rest,
619
+ ...label === void 0 ? {} : { label },
620
+ ...code === void 0 ? {} : { code }
621
+ };
622
+ }
1544
623
  const tools = [
1545
624
  operationTool("get_tabs", "List the projects open in Parity, one per tab. The active tab is the one the person is looking at.", operations.tabs, json),
1546
625
  operationTool("get_app_state", "Get a tab's state: its designs (ids, states, modes shown, where their boards are and how big), boards that overlap or crowd each other's titles, the selection, project modes, components and tokens, and what Parity detected in the project (each tool's version, where it's installed and its config, the package manager, and a note naming versions Parity doesn't test). Call it first, and again after the person may have changed things. Keep boards apart: Overlaps() and FindEmptySpace() in execute help place them.", operations.state, json),
@@ -1553,13 +632,7 @@ const tools = [
1553
632
  fields: executeOperation.fields,
1554
633
  names: executeNames,
1555
634
  async run(input) {
1556
- const result = await executeOperation.run({
1557
- tab: input.tab,
1558
- label: input.description,
1559
- code: input.input,
1560
- editId: input.editId,
1561
- edits: input.edits
1562
- }, call);
635
+ const result = await executeOperation.run(contractNames(input), call);
1563
636
  const structured = executeOperation.structured(result);
1564
637
  return {
1565
638
  content: [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))],
@@ -1588,16 +661,36 @@ const tools = [
1588
661
  }
1589
662
  ];
1590
663
  const instructions = "Parity renders a project's real components on a canvas. Designs are trees of the project's own components, layouts and text, styled only with its tokens, with states (like stories) and modes (viewport, theme, locale). Read the skill (read_skill) before designing, start with get_app_state, change designs with execute, check every state and mode with screenshot and find_problems, check accessibility with check_accessibility, and read what the components log with get_console.";
664
+ /**
665
+ * One JSON-RPC message from the client, as far as the server reads it. `params` stays open, since each method reads
666
+ * its own. A message without an id is a notification, which gets no reply.
667
+ */
668
+ const message = z.looseObject({
669
+ id: z.union([z.string(), z.number()]).optional(),
670
+ method: z.string(),
671
+ params: z.looseObject({}).optional()
672
+ });
673
+ /** The client's side of `initialize`: who it is, for the canvas, and the protocol version it speaks. */
674
+ const initializeParams = z.looseObject({
675
+ protocolVersion: z.string().optional().catch(void 0),
676
+ clientInfo: z.looseObject({
677
+ name: z.unknown().optional(),
678
+ title: z.unknown().optional()
679
+ }).optional().catch(void 0)
680
+ });
681
+ /** `tools/call`'s params: which tool, and its arguments, which the tool's own schema checks. */
682
+ const toolCall = z.looseObject({
683
+ name: z.string(),
684
+ arguments: z.unknown()
685
+ });
1591
686
  /** Serves Parity's tools over MCP on stdin and stdout. */
1592
687
  function serveMcp() {
1593
688
  const lines = createInterface({ input: process.stdin });
1594
- const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
689
+ const send = (reply) => process.stdout.write(`${JSON.stringify(reply)}\n`);
1595
690
  lines.on("line", (line) => {
1596
691
  if (!line.trim()) return;
1597
- let message;
1598
- try {
1599
- message = JSON.parse(line);
1600
- } catch {
692
+ const json = jsonText(z.unknown()).safeParse(line);
693
+ if (!json.success) {
1601
694
  send({
1602
695
  jsonrpc: "2.0",
1603
696
  id: null,
@@ -1608,22 +701,37 @@ function serveMcp() {
1608
701
  });
1609
702
  return;
1610
703
  }
1611
- respond(message).then((reply) => {
1612
- if (message.id !== void 0 && reply) send({
704
+ const read = checkData(message, json.data);
705
+ if (!read.success) {
706
+ const id = z.union([z.string(), z.number()]).safeParse(plainObject.safeParse(json.data).data?.id).data ?? null;
707
+ const { pointer, message: problem } = read.problem;
708
+ send({
1613
709
  jsonrpc: "2.0",
1614
- id: message.id,
710
+ id,
711
+ error: {
712
+ code: -32600,
713
+ message: `Invalid request: ${pointer || "/"}: ${problem}`
714
+ }
715
+ });
716
+ return;
717
+ }
718
+ const request = read.data;
719
+ respond(request.method, request.params).then((reply) => {
720
+ if (request.id !== void 0 && reply) send({
721
+ jsonrpc: "2.0",
722
+ id: request.id,
1615
723
  ...reply
1616
724
  });
1617
725
  });
1618
726
  });
1619
727
  }
1620
- async function respond(message) {
1621
- switch (message.method) {
728
+ async function respond(method, params = {}) {
729
+ switch (method) {
1622
730
  case "initialize": {
1623
- const name = mcpCallerName(message.params?.clientInfo);
1624
- setCaller(name, true);
731
+ const { clientInfo, protocolVersion } = initializeParams.parse(params);
732
+ setCaller(mcpCallerName(clientInfo), true);
1625
733
  return { result: {
1626
- protocolVersion: message.params?.protocolVersion ?? "2025-06-18",
734
+ protocolVersion: protocolVersion ?? "2025-06-18",
1627
735
  capabilities: { tools: {} },
1628
736
  serverInfo: {
1629
737
  name: "parity",
@@ -1639,13 +747,21 @@ async function respond(message) {
1639
747
  inputSchema
1640
748
  })) } };
1641
749
  case "tools/call": {
1642
- const tool = tools.find((candidate) => candidate.name === message.params?.name);
750
+ const called = checkData(toolCall, params);
751
+ if (!called.success) {
752
+ const { pointer, message: problem } = called.problem;
753
+ return { error: {
754
+ code: -32602,
755
+ message: `Invalid params: ${pointer}: ${problem}`
756
+ } };
757
+ }
758
+ const tool = tools.find((candidate) => candidate.name === called.data.name);
1643
759
  if (!tool) return { error: {
1644
760
  code: -32602,
1645
- message: `Unknown tool ${String(message.params?.name)}`
761
+ message: `Unknown tool ${called.data.name}`
1646
762
  } };
1647
763
  try {
1648
- return { result: await tool.run(message.params?.arguments ?? {}) };
764
+ return { result: await tool.run(called.data.arguments ?? {}) };
1649
765
  } catch (error) {
1650
766
  const failure = renamePath(operationError(error, tool.fields), tool.names ?? {});
1651
767
  return { result: {
@@ -1655,9 +771,9 @@ async function respond(message) {
1655
771
  } };
1656
772
  }
1657
773
  }
1658
- default: return message.method?.startsWith("notifications/") ? void 0 : { error: {
774
+ default: return method.startsWith("notifications/") ? void 0 : { error: {
1659
775
  code: -32601,
1660
- message: `Unknown method ${message.method}`
776
+ message: `Unknown method ${method}`
1661
777
  } };
1662
778
  }
1663
779
  }