@parity-app/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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -2
  3. package/dist/bin.mjs +831 -0
  4. package/dist/client-DsTLQ2lT.mjs +286 -0
  5. package/dist/in-app.json +10 -0
  6. package/dist/index.d.mts +160 -0
  7. package/dist/index.mjs +1 -0
  8. package/dist/libraries/aria.lib.par +14516 -0
  9. package/dist/libraries/baseui.lib.par +8298 -0
  10. package/dist/libraries/buildkit-baseui.edits.json +3 -0
  11. package/dist/libraries/chakra.lib.par +539225 -0
  12. package/dist/libraries/fluent-icons.lib.par +2674 -0
  13. package/dist/libraries/fluent.lib.par +33703 -0
  14. package/dist/libraries/heroicons.lib.par +33 -0
  15. package/dist/libraries/hugeicons.lib.par +708 -0
  16. package/dist/libraries/lucide.lib.par +2123 -0
  17. package/dist/libraries/luma.lib.par +16362 -0
  18. package/dist/libraries/lyra.lib.par +16362 -0
  19. package/dist/libraries/maia.lib.par +16362 -0
  20. package/dist/libraries/mantine.lib.par +32935 -0
  21. package/dist/libraries/mira.lib.par +16362 -0
  22. package/dist/libraries/mui.lib.par +9817 -0
  23. package/dist/libraries/nova.lib.par +16363 -0
  24. package/dist/libraries/phosphor.lib.par +3060 -0
  25. package/dist/libraries/radix.lib.par +6890 -0
  26. package/dist/libraries/recharts.lib.par +5445 -0
  27. package/dist/libraries/rhea.lib.par +16362 -0
  28. package/dist/libraries/sera.lib.par +16362 -0
  29. package/dist/libraries/tabler.lib.par +5325 -0
  30. package/dist/libraries/vega.lib.par +16362 -0
  31. package/dist/published-BtL8RM6W.mjs +1 -0
  32. package/dist/published-CdXoUgfD.mjs +200 -0
  33. package/dist/src-8MhDV_sA.mjs +4780 -0
  34. package/dist/src-D9EMN48r.mjs +1306 -0
  35. package/dist/src-DXZqtm6z.mjs +53 -0
  36. package/dist/src-DYi23Ypb.mjs +1 -0
  37. package/dist/src-FF8eY6EI.mjs +2 -0
  38. package/package.json +50 -3
  39. package/skills/SKILL.md +96 -0
  40. package/skills/document.md +170 -0
  41. package/skills/execute.md +298 -0
  42. package/skills/guide/checking.md +56 -0
  43. package/skills/guide/components.md +48 -0
  44. package/skills/guide/design-system.md +52 -0
  45. package/skills/guide/mobile.md +32 -0
  46. package/skills/guide/states-and-modes.md +55 -0
  47. package/skills/guide/web-app.md +43 -0
package/dist/bin.mjs ADDED
@@ -0,0 +1,831 @@
1
+ #!/usr/bin/env node
2
+ import { a as describeConsole, c as describeProps, d as mcpCallerName, i as describeComponents, l as describeTokens, n as call, o as describeExec, r as describeAccessibility, s as describeProblems, u as setCaller } from "./client-DsTLQ2lT.mjs";
3
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { dirname, join, normalize, relative, resolve, sep } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { createInterface } from "node:readline";
7
+ // src/check.ts
8
+ /**
9
+ * `parity lib check <library>`: inserts every component of a registry library file into the running app, once on its
10
+ * own and once the way the Assets panel inserts it, inside its parents with its sample, and reports the parts that fail
11
+ * alone but render inside. The tab's project must have the library installed. Exits with 1 below the library's floor
12
+ * in the registry's `in-app.json`.
13
+ */
14
+ async function runCheck(library, options) {
15
+ if (!library) throw new Error("parity lib check <library> [--tab t] [--mode m]");
16
+ const { applyLibraryEdits, componentNode, generateId, librarySource, parseLibraryEdits, withLibraryContext } = await import("./src-FF8eY6EI.mjs");
17
+ const { registryDir, registryEntries } = await import("./src-DYi23Ypb.mjs");
18
+ const entry = (await registryEntries()).find((item) => item.manifest.id === library);
19
+ if (!entry) throw new Error(`The registry has no library "${library}"`);
20
+ let manifest = entry.manifest;
21
+ if (manifest.edits) {
22
+ const edits = typeof manifest.edits === "string" ? parseLibraryEdits(JSON.parse(await readFile(join(dirname(entry.path), manifest.edits), "utf8"))) : manifest.edits;
23
+ manifest = applyLibraryEdits(manifest, edits).manifest;
24
+ }
25
+ const list = manifest.components?.list ?? {};
26
+ const names = Object.keys(list).filter((name) => list[name].visual !== false);
27
+ const id = (name) => `${library}/${name}`;
28
+ const sources = Object.fromEntries(Object.keys(list).map((name) => [id(name), {
29
+ ...librarySource(library, name, list[name], library),
30
+ declaration: ""
31
+ }]));
32
+ const mode = options.mode ?? "desktop-light";
33
+ const exec = async (label, code) => {
34
+ const result = await call("exec", {
35
+ tab: options.tab,
36
+ label,
37
+ code
38
+ });
39
+ if (!result.ok) throw new Error(result.error ?? `${label} failed`);
40
+ const line = result.output.find((item) => item.startsWith("{") || item.startsWith("["));
41
+ if (!line) throw new Error(`${label} printed nothing`);
42
+ return line;
43
+ };
44
+ const infos = JSON.parse(await exec(`Read ${library}'s props`, `const ids = ${JSON.stringify(names.map(id))}
45
+ const known = new Set(Components())
46
+ Print(JSON.stringify(Object.fromEntries(ids.filter((id) => known.has(id)).map((id) => [id, Props(id, { all: true })]))))`));
47
+ if (names.filter((name) => !infos[id(name)]).length === names.length) throw new Error(`The tab's project doesn't have ${library} installed`);
48
+ const taken = /* @__PURE__ */ new Set();
49
+ const alone = names.map((name) => {
50
+ const node = {
51
+ type: "component",
52
+ id: generateId(taken),
53
+ component: id(name)
54
+ };
55
+ taken.add(node.id);
56
+ return node;
57
+ });
58
+ const inside = names.filter((name) => list[name].parents?.length || list[name].sample).map((name) => withLibraryContext(componentNode(id(name), infos[id(name)], taken), sources, [], taken));
59
+ const failing = async (design, nodes) => {
60
+ const line = await exec(`Check ${design}`, `for (const design of Designs().filter((d) => d.name === ${JSON.stringify(design)})) DeleteDesign(design.id)
61
+ const spot = FindEmptySpace({ width: 1280, height: 2000 })
62
+ const id = CreateDesign({ name: ${JSON.stringify(design)}, x: spot.x, y: spot.y })
63
+ const grid = Insert(id, { type: "layout", name: "Grid", style: { direction: "row", wrap: true, gap: 4, padding: 4 }, children: [] })
64
+ for (const node of ${JSON.stringify(nodes)}) Insert(grid, node)
65
+ Print(JSON.stringify(await Problems(id, { mode: ${JSON.stringify(mode)} })))`);
66
+ return failures(JSON.parse(line), library);
67
+ };
68
+ const failAlone = await failing(`${library} alone`, alone);
69
+ const failInside = await failing(`${library} inside`, inside);
70
+ const report = score(library, mode, names.length, failAlone, failInside, list);
71
+ report.floor = JSON.parse(await readFile(join(registryDir(), "..", "in-app.json"), "utf8"))[library];
72
+ if (options.record) {
73
+ const kept = await recordAppErrors(registryDir(), library, report);
74
+ console.error(`Kept ${kept} errors from the app for ${library}; run parity lib with --registry to use them.`);
75
+ }
76
+ if (options.json) console.log(JSON.stringify(report, null, 2));
77
+ else console.log(describeCheck(report));
78
+ if (report.floor !== void 0 && report.score < report.floor) process.exitCode = 1;
79
+ }
80
+ /** Where a library's errors from the app are kept: `app-errors/<id>.json` beside the registry's files. */
81
+ function appErrorsPath(registry, library) {
82
+ return join(registry, "..", "app-errors", `${library}.json`);
83
+ }
84
+ /** The app's errors kept for a library, by part, or none. */
85
+ async function readAppErrors(registry, library) {
86
+ return readFile(appErrorsPath(registry, library), "utf8").then((text) => JSON.parse(text), () => void 0);
87
+ }
88
+ /**
89
+ * Adds the parts that failed in the app to those kept before, with their own errors rather than a parent's: one kept
90
+ * from an earlier check stays, so the parent its error named stays in the part's chain once it renders.
91
+ */
92
+ async function recordAppErrors(registry, library, report) {
93
+ const kept = { ...await readAppErrors(registry, library) };
94
+ for (const [name, why] of Object.entries(report.misses)) if (!why.startsWith("inside ")) kept[name] = why;
95
+ const path = appErrorsPath(registry, library);
96
+ await mkdir(dirname(path), { recursive: true });
97
+ const sorted = Object.fromEntries(Object.entries(kept).sort(([a], [b]) => a.localeCompare(b, "en")));
98
+ await writeFile(path, `${JSON.stringify(sorted, null, 2)}\n`);
99
+ return Object.keys(sorted).length;
100
+ }
101
+ /**
102
+ * The components of a library that a design's problems name, by why: `aria/Cell: Cell count must match…`, or
103
+ * `"aria/Cell" isn't one of the project's components` for one the project doesn't have.
104
+ */
105
+ function failures(problems, library) {
106
+ const failed = /* @__PURE__ */ new Map();
107
+ const escaped = library.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ const pattern = new RegExp(`${escaped}/([\\w.$]+)(?:: |" )(.*)`);
109
+ for (const { problem } of problems) for (const line of problem.split("\n")) {
110
+ const match = pattern.exec(line);
111
+ if (match && !failed.has(match[1])) failed.set(match[1], match[2].slice(0, 160));
112
+ }
113
+ return failed;
114
+ }
115
+ /** A part counts once it and every parent it's placed in render. */
116
+ function score(library, mode, components, failAlone, failInside, list) {
117
+ const placed = [...failAlone.keys()].filter((name) => list[name]?.parents?.length || list[name]?.sample);
118
+ const hits = placed.filter((name) => !failInside.has(name) && !(list[name].parents ?? []).some((parent) => failInside.has(parent)));
119
+ const misses = Object.fromEntries(placed.filter((name) => !hits.includes(name)).map((name) => [name, failInside.get(name) ?? `inside ${(list[name].parents ?? []).filter((parent) => failInside.has(parent)).join(", ")}, which fails`]));
120
+ return {
121
+ library,
122
+ mode,
123
+ components,
124
+ failAlone: Object.fromEntries(failAlone),
125
+ placed,
126
+ hits,
127
+ misses,
128
+ score: placed.length === 0 ? 100 : Math.floor(100 * hits.length / placed.length)
129
+ };
130
+ }
131
+ function describeCheck(report) {
132
+ const unplaced = Object.keys(report.failAlone).filter((name) => !report.placed.includes(name));
133
+ const lines = [`${report.library}: ${report.components} components, ${Object.keys(report.failAlone).length} fail on their own, ${report.placed.length} of them placed, ${report.hits.length} render inside their parents (${report.score}%${report.floor === void 0 ? "" : `, at least ${report.floor}%`})`];
134
+ for (const [name, why] of Object.entries(report.misses)) lines.push(` ${name}: ${why}`);
135
+ if (unplaced.length > 0) lines.push(` Not placed: ${unplaced.join(", ")}`);
136
+ if (report.floor !== void 0 && report.score < report.floor) lines.push(`Below ${report.library}'s floor.`);
137
+ return lines.join("\n");
138
+ }
139
+ // src/lib.ts
140
+ /**
141
+ * `parity lib`: writes a `.lib.par` by inspecting a package or the app itself. It runs on its own, with no app open,
142
+ * because it reads the project's files and its own copy of TypeScript.
143
+ */
144
+ async function runLib(packageName, options) {
145
+ const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-8MhDV_sA.mjs");
146
+ const { covers, registryDir, registryEntries } = await import("./src-DYi23Ypb.mjs");
147
+ const spec = packageName === void 0 ? void 0 : packageSpec(packageName);
148
+ let root = resolve(options.root ?? ".");
149
+ if (spec && (spec.version !== void 0 || options.registry && !options.root)) {
150
+ const { installedVersion, publishedVersion } = await import("./published-BtL8RM6W.mjs");
151
+ const listed = (await registryEntries()).filter((entry) => entry.manifest.package?.name === spec.name);
152
+ if (spec.version === void 0 && listed.length > 1) throw new Error(`The registry has ${listed.length} files for ${spec.name}; name a version, as ${spec.name}@1.2.3`);
153
+ const version = await publishedVersion(spec.name, spec.version ?? listed[0]?.manifest.package?.version ?? "latest");
154
+ console.error(`Installing ${spec.name} ${version} on its own…`);
155
+ root = await installedVersion(spec.name, version);
156
+ }
157
+ const { parseLibraryEdits } = await import("./src-8MhDV_sA.mjs");
158
+ const trialEdits = options.edits ? parseLibraryEdits(JSON.parse(await readFile(resolve(options.edits), "utf8"))) : void 0;
159
+ const recordedId = options.registry && spec ? options.id ?? (await registryEntries()).find((entry) => entry.manifest.package?.name === spec.name)?.manifest.id : void 0;
160
+ const appErrors = recordedId ? await readAppErrors(registryDir(), recordedId) : void 0;
161
+ const generated = await generateLibrary({
162
+ root,
163
+ ...trialEdits ? { trialEdits } : {},
164
+ ...appErrors ? { appErrors } : {},
165
+ ...options.noExamples ? { examples: false } : {},
166
+ ...options.noTags ? { tags: false } : {},
167
+ ...spec ? { package: spec.name } : {},
168
+ ...options.id ? { id: options.id } : {},
169
+ ...options.name ? { name: options.name } : {},
170
+ ...options.description ? { description: options.description } : {},
171
+ ...options.range ? { range: options.range } : {}
172
+ });
173
+ const id = String(generated.manifest.id);
174
+ let target;
175
+ if (options.out === "-") target = void 0;
176
+ else if (options.out) target = resolve(options.out);
177
+ else if (options.registry) target = join(registryDir(), `${id}.lib.par`);
178
+ else if (packageName) target = resolve(`${id}.lib.par`);
179
+ else target = await appManifestPath(root);
180
+ const existing = target ? await readFile(target, "utf8").then((text) => JSON.parse(text), () => void 0) : void 0;
181
+ if (!options.range) {
182
+ const read = generated.manifest.package;
183
+ const range = read && keptRange(existing, read, (versions) => covers({ package: versions }, read.version));
184
+ if (range) generated.manifest.package = {
185
+ ...read,
186
+ range
187
+ };
188
+ }
189
+ let only;
190
+ if (!options.only || options.only === "all") only = "all";
191
+ else only = options.only === "new" ? "new" : options.only.split(",");
192
+ const edits = options.edits && relative(dirname(target ?? resolve(".")), resolve(options.edits)).replaceAll("\\", "/");
193
+ const { manifest, report } = await reviseLibrary(generated, {
194
+ root,
195
+ previous: existing,
196
+ only,
197
+ ...edits ? { edits } : {},
198
+ editsBase: dirname(target ?? resolve("."))
199
+ });
200
+ if (!target) {
201
+ console.log(JSON.stringify(manifest, null, 2));
202
+ console.error(describeReport(report));
203
+ return;
204
+ }
205
+ const written = packageName ? manifest : mergeManifest(existing, manifest);
206
+ await mkdir(dirname(target), { recursive: true });
207
+ await writeFile(target, manifestText(written));
208
+ if (options.json) console.log(JSON.stringify({
209
+ path: target,
210
+ report
211
+ }, null, 2));
212
+ else console.log(`Wrote ${relative(process.cwd(), target) || target}\n${describeReport(report)}`);
213
+ }
214
+ /**
215
+ * The range a file being written over keeps: its own, while it still covers the version just read, so a range widened
216
+ * by hand (Lucide's, from 0.400) survives regenerating. Undefined when the new range stands.
217
+ */
218
+ function keptRange(existing, read, covers) {
219
+ const range = existing?.package?.range;
220
+ if (typeof range !== "string" || range === read.range) return void 0;
221
+ return covers({
222
+ ...read,
223
+ range
224
+ }) ? range : void 0;
225
+ }
226
+ /** A package as `parity lib` is given it: a name, with a version or tag after an `@` (`@tabler/icons-react@3.50.0`). */
227
+ function packageSpec(text) {
228
+ const at = text.lastIndexOf("@");
229
+ return at > 0 ? {
230
+ name: text.slice(0, at),
231
+ version: text.slice(at + 1)
232
+ } : { name: text };
233
+ }
234
+ /** The report in plain words: what was found, then what couldn't be decided. */
235
+ function describeReport(report) {
236
+ const lines = [];
237
+ const from = report.package ? ` from ${report.package.name} ${report.package.version}` : "";
238
+ let kind = "a component library";
239
+ if (report.kind === "icons") kind = "an icon set";
240
+ else if (report.kind === "app") kind = "an app";
241
+ lines.push(`Described ${kind}${from}.`);
242
+ if (report.kind === "icons") lines.push(` ${report.icons ?? 0} icons`);
243
+ if (report.iconTags) {
244
+ const { matched, of, source } = report.iconTags;
245
+ lines.push(` Tags for ${matched} of ${of} icons, from ${source}`);
246
+ } else lines.push(` ${report.components} components`);
247
+ const tokens = Object.entries(report.tokens);
248
+ const total = tokens.reduce((sum, [, count]) => sum + (count ?? 0), 0);
249
+ if (total > 0) {
250
+ const themes = report.themes.length > 0 ? `, with ${report.themes.join(", ")} themes` : "";
251
+ lines.push(` ${total} tokens${themes}: ${tokens.map(([category, count]) => `${count} ${category}`).join(", ")}`);
252
+ }
253
+ if (report.breakpoints > 0) lines.push(` ${report.breakpoints} breakpoints`);
254
+ for (const decorator of report.decorators) lines.push(` Decorator: ${decorator}`);
255
+ if (report.providers.length > 0) lines.push(` Providers and hosts it exports: ${report.providers.join(", ")}`);
256
+ if (report.ambiguous.length > 0) {
257
+ lines.push(`Left out, because the types can't say (${report.ambiguous.length}):`);
258
+ for (const entry of report.ambiguous.slice(0, 20)) lines.push(` ${entry.name}: ${entry.reason}`);
259
+ if (report.ambiguous.length > 20) lines.push(` and ${report.ambiguous.length - 20} more (--json lists them)`);
260
+ }
261
+ if (report.examples) {
262
+ const { repo, commit, how, files, used } = report.examples;
263
+ const matched = {
264
+ upstream: "the revision it records",
265
+ gitHead: "npm's gitHead",
266
+ tag: "its tag",
267
+ date: "its publish date"
268
+ }[how];
269
+ lines.push(` Examples: ${files} files from ${repo} at ${commit.slice(0, 10)} (by ${matched}), using ${used} of its components`);
270
+ }
271
+ if (report.anatomy) {
272
+ const { parts, placed, complete, differ } = report.anatomy;
273
+ lines.push(` Anatomy: of ${parts} parts its docs nest, ${placed} are placed as they show and ${complete} have every level`);
274
+ for (const entry of differ.slice(0, 10)) lines.push(` ${entry.name}: docs ${entry.docs.join(" > ")}; file ${entry.file.join(" > ") || "no parents"}`);
275
+ if (differ.length > 10) lines.push(` and ${differ.length - 10} more (--json lists them)`);
276
+ }
277
+ if (report.parts) {
278
+ const parts = report.parts;
279
+ const signals = Object.entries(parts.parents).map(([signal, count]) => `${count} by ${signal}`);
280
+ lines.push(` Parents: ${signals.join(", ") || "none"}`);
281
+ lines.push(` ${parts.failedAlone} components fail alone with no props; ${parts.fixed} render inside their parents with samples`);
282
+ if (parts.failing.length > 0) {
283
+ lines.push(`Still failing, for a hand edit (${parts.failing.length}):`);
284
+ for (const entry of parts.failing.slice(0, 20)) lines.push(` ${entry.name}: ${entry.error.slice(0, 120)}`);
285
+ if (parts.failing.length > 20) lines.push(` and ${parts.failing.length - 20} more (--json lists them)`);
286
+ }
287
+ }
288
+ if (report.changes) {
289
+ const changes = report.changes;
290
+ lines.push(`Since the last file: ${changes.added.length} new, ${changes.changed.length} changed, ${changes.removed.length} removed, ${changes.unchanged} unchanged`);
291
+ if (changes.added.length > 0) lines.push(` New: ${changes.added.slice(0, 30).join(", ")}`);
292
+ for (const change of changes.changed.slice(0, 20)) {
293
+ const parts = [
294
+ change.added.length > 0 ? `props added ${change.added.join(", ")}` : "",
295
+ change.removed.length > 0 ? `removed ${change.removed.join(", ")}` : "",
296
+ change.retyped.length > 0 ? `retyped ${change.retyped.join(", ")}` : "",
297
+ change.as ? `now ${change.as}` : ""
298
+ ].filter(Boolean);
299
+ lines.push(` Changed ${change.name}: ${parts.join("; ") || "no fingerprint before"}`);
300
+ }
301
+ if (changes.removed.length > 0) lines.push(` Removed: ${changes.removed.join(", ")}`);
302
+ }
303
+ if (report.orphanEdits?.length) lines.push(`Hand edits for exports it no longer has, kept for you to move or delete: ${report.orphanEdits.join(", ")}`);
304
+ for (const note of report.notes) lines.push(`Note: ${note}`);
305
+ return lines.join("\n");
306
+ }
307
+ // src/skills.ts
308
+ const root = join(import.meta.dirname, "..", "skills");
309
+ /** Reads the Parity design skill: SKILL.md, or a file it references. */
310
+ async function readSkill(path = "SKILL.md") {
311
+ const target = normalize(join(root, path));
312
+ if (relative(root, target).startsWith("..") || relative(root, target).includes(`..${sep}`)) throw new Error(`${path} isn't part of the skill`);
313
+ try {
314
+ return await readFile(target, "utf8");
315
+ } catch {
316
+ const files = await list();
317
+ throw new Error(`No skill file ${path}. Files: ${files.join(", ")}`);
318
+ }
319
+ }
320
+ async function list(directory = root) {
321
+ const entries = await readdir(directory, { withFileTypes: true });
322
+ return (await Promise.all(entries.map(async (entry) => entry.isDirectory() ? list(join(directory, entry.name)) : [relative(root, join(directory, entry.name)).replaceAll("\\", "/")]))).flat();
323
+ }
324
+ // src/mcp.ts
325
+ const tab = {
326
+ type: "string",
327
+ description: "Tab id, project root or name. Defaults to the active tab."
328
+ };
329
+ const tools = [
330
+ {
331
+ name: "get_tabs",
332
+ description: "List the projects open in Parity, one per tab. The active tab is the one the person is looking at.",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {}
336
+ },
337
+ run: async () => [text(JSON.stringify(await call("tabs"), null, 2))]
338
+ },
339
+ {
340
+ name: "get_app_state",
341
+ description: "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.",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: { tab }
345
+ },
346
+ run: async (input) => [text(JSON.stringify(await call("state", { tab: input.tab }), null, 2))]
347
+ },
348
+ {
349
+ name: "execute",
350
+ description: "Run a JavaScript snippet against a tab's designs with the Parity API (read_skill \"execute.md\"). All changes land as one undoable step, or none if it fails. On failure, retry with editId and edits instead of resending.",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ description: {
355
+ type: "string",
356
+ description: "Shown to the person while it runs. 3 to 8 words, imperative, e.g. \"Add pricing tiers\"."
357
+ },
358
+ input: {
359
+ type: "string",
360
+ description: "The snippet. Omit when retrying with editId and edits."
361
+ },
362
+ editId: {
363
+ type: "string",
364
+ description: "The failed call's editId, to patch and rerun its snippet."
365
+ },
366
+ edits: {
367
+ type: "array",
368
+ description: "Exact find and replace edits to the failed snippet, applied in order.",
369
+ items: {
370
+ type: "object",
371
+ properties: {
372
+ find: { type: "string" },
373
+ replace: { type: "string" },
374
+ all: { type: "boolean" }
375
+ },
376
+ required: ["find", "replace"]
377
+ }
378
+ },
379
+ tab
380
+ },
381
+ required: ["description"]
382
+ },
383
+ run: async (input) => {
384
+ const result = await call("exec", {
385
+ tab: input.tab,
386
+ label: input.description,
387
+ code: input.input,
388
+ editId: input.editId,
389
+ edits: input.edits
390
+ });
391
+ return [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))];
392
+ }
393
+ },
394
+ {
395
+ name: "screenshot",
396
+ description: "Capture a design in any of its states and modes, rendered offscreen at real widths. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".",
397
+ inputSchema: {
398
+ type: "object",
399
+ properties: {
400
+ design: {
401
+ type: "string",
402
+ description: "Design id or name."
403
+ },
404
+ state: { description: "State id, list of ids, or \"all\". Defaults to the state the design shows." },
405
+ mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to the primary mode." },
406
+ scale: {
407
+ type: "number",
408
+ description: "Pixel density, 0.25 to 3. Defaults to 1."
409
+ },
410
+ tab
411
+ },
412
+ required: ["design"]
413
+ },
414
+ run: async (input) => {
415
+ const result = await call("screenshot", input);
416
+ return [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))];
417
+ }
418
+ },
419
+ {
420
+ name: "find_problems",
421
+ description: "Find layout problems in a design, rendered offscreen in every state and the modes it shows unless you narrow it: render errors, content wider than the frame, nodes cut off by a parent that clips, and components that aren't the project's. Run it after changes, with screenshot. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".",
422
+ inputSchema: {
423
+ type: "object",
424
+ properties: {
425
+ design: {
426
+ type: "string",
427
+ description: "Design id or name."
428
+ },
429
+ state: { description: "State id, list of ids, or \"all\". Defaults to \"all\"." },
430
+ mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to \"shown\"." },
431
+ tab
432
+ },
433
+ required: ["design"]
434
+ },
435
+ run: async (input) => [text(describeProblems(await call("problems", input)))]
436
+ },
437
+ {
438
+ name: "check_accessibility",
439
+ description: "Check a design's accessibility with axe-core against WCAG 2.2 AA, rendered offscreen in every state and the modes it shows unless you narrow it. Lists each rule broken, worst first, with the layers that break it (ids you can pass to execute), the frames, what's wrong and a link on how to fix it. state and mode take an id, a list, or \"all\"; mode also takes \"shown\". Automated checks find some issues, not all.",
440
+ inputSchema: {
441
+ type: "object",
442
+ properties: {
443
+ design: {
444
+ type: "string",
445
+ description: "Design id or name."
446
+ },
447
+ state: { description: "State id, list of ids, or \"all\". Defaults to \"all\"." },
448
+ mode: { description: "Mode id, list of ids, \"shown\" or \"all\". Defaults to \"shown\"." },
449
+ tab
450
+ },
451
+ required: ["design"]
452
+ },
453
+ run: async (input) => [text(describeAccessibility(await call("accessibility", input)))]
454
+ },
455
+ {
456
+ name: "get_console",
457
+ description: "Read what the project's own code wrote to the console: errors, warnings and logs from its components on the canvas, including while the person interacts with them, and from offscreen renders such as screenshots. Oldest first; the same message several times in a row is counted once. clear: true empties it after reading, so the next read shows only what's new.",
458
+ inputSchema: {
459
+ type: "object",
460
+ properties: {
461
+ clear: {
462
+ type: "boolean",
463
+ description: "Empty it after reading."
464
+ },
465
+ tab
466
+ }
467
+ },
468
+ run: async (input) => [text(describeConsole(await call("console", input)))]
469
+ },
470
+ {
471
+ name: "open_project",
472
+ description: "Open a project folder in a new Parity tab, or switch to it if it's already open.",
473
+ inputSchema: {
474
+ type: "object",
475
+ properties: { root: {
476
+ type: "string",
477
+ description: "Absolute path."
478
+ } },
479
+ required: ["root"]
480
+ },
481
+ run: async (input) => [text(JSON.stringify(await call("open", { root: input.root }), null, 2))]
482
+ },
483
+ {
484
+ name: "read_skill",
485
+ description: "Read Parity's design skill: SKILL.md, or a file it references such as execute.md or guide/web-app.md.",
486
+ inputSchema: {
487
+ type: "object",
488
+ properties: { path: { type: "string" } }
489
+ },
490
+ run: async (input) => [text(await readSkill(input.path))]
491
+ }
492
+ ];
493
+ 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.";
494
+ /** Serves Parity's tools over MCP on stdin and stdout. */
495
+ function serveMcp() {
496
+ const lines = createInterface({ input: process.stdin });
497
+ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
498
+ lines.on("line", (line) => {
499
+ if (!line.trim()) return;
500
+ let message;
501
+ try {
502
+ message = JSON.parse(line);
503
+ } catch {
504
+ send({
505
+ jsonrpc: "2.0",
506
+ id: null,
507
+ error: {
508
+ code: -32700,
509
+ message: "Parse error"
510
+ }
511
+ });
512
+ return;
513
+ }
514
+ respond(message).then((reply) => {
515
+ if (message.id !== void 0 && reply) send({
516
+ jsonrpc: "2.0",
517
+ id: message.id,
518
+ ...reply
519
+ });
520
+ });
521
+ });
522
+ }
523
+ async function respond(message) {
524
+ switch (message.method) {
525
+ case "initialize": {
526
+ const name = mcpCallerName(message.params?.clientInfo);
527
+ setCaller(name, true);
528
+ return { result: {
529
+ protocolVersion: message.params?.protocolVersion ?? "2025-06-18",
530
+ capabilities: { tools: {} },
531
+ serverInfo: {
532
+ name: "parity",
533
+ version: "0.0.0"
534
+ },
535
+ instructions
536
+ } };
537
+ }
538
+ case "ping": return { result: {} };
539
+ case "tools/list": return { result: { tools: tools.map(({ name, description, inputSchema }) => ({
540
+ name,
541
+ description,
542
+ inputSchema
543
+ })) } };
544
+ case "tools/call": {
545
+ const tool = tools.find((candidate) => candidate.name === message.params?.name);
546
+ if (!tool) return { error: {
547
+ code: -32602,
548
+ message: `Unknown tool ${String(message.params?.name)}`
549
+ } };
550
+ try {
551
+ return { result: { content: await tool.run(message.params?.arguments ?? {}) } };
552
+ } catch (error) {
553
+ return { result: {
554
+ content: [text(error instanceof Error ? error.message : String(error))],
555
+ isError: true
556
+ } };
557
+ }
558
+ }
559
+ default: return message.method?.startsWith("notifications/") ? void 0 : { error: {
560
+ code: -32601,
561
+ message: `Unknown method ${message.method}`
562
+ } };
563
+ }
564
+ }
565
+ function text(value) {
566
+ return {
567
+ type: "text",
568
+ text: value
569
+ };
570
+ }
571
+ function image(data) {
572
+ return {
573
+ type: "image",
574
+ data,
575
+ mimeType: "image/png"
576
+ };
577
+ }
578
+ // src/bin.ts
579
+ const usage = `parity: drive the Parity app from a terminal or an agent
580
+
581
+ parity tabs list open project tabs
582
+ parity state [--tab t] designs, selection, modes, components and tokens of a tab
583
+ parity exec [file] --label "Do x" run a snippet (from a file, or stdin) as one undoable step
584
+ [--edit-id id --edits '[{"find":"a","replace":"b"}]'] retry a failed snippet with edits
585
+ parity screenshot <design> [--state s|all] [--mode m|shown|all] [--scale 2] [--tab t]
586
+ parity a11y <design> [--state s|all] [--mode m|shown|all] [--tab t]
587
+ check accessibility with axe-core, in every state and the modes shown
588
+ unless narrowed; exits with 1 when it finds issues
589
+ parity problems <design> [--state s|all] [--mode m|shown|all] [--tab t]
590
+ render errors, overflow, clipping and unknown components, in every state
591
+ and the modes shown unless narrowed; exits with 1 when it finds any
592
+ parity console [--clear] [--tab t] what the project's code wrote to the console, on the canvas and in
593
+ offscreen renders; --clear empties it after reading
594
+ parity tokens [--category c] [--source semantic|theme] [--search s] [--tab t]
595
+ the tokens styles can name, with their values per theme
596
+ parity components [--tab t] the components designs can use, by library
597
+ parity props <Component> [--all] [--tab t]
598
+ a component's props from its source; --all adds the attributes it passes on
599
+ parity lib [package] [--root dir] [--out file|-] [--registry] [--id id] [--name n] [--range r] [--json]
600
+ [--only new|all|A,B] [--edits file] [--no-examples] [--no-tags]
601
+ write a .lib.par: a package's components, tokens, breakpoints, icons and
602
+ decorators, found from its types and styles, with each part's parents and
603
+ what it needs to render, tried with React's server renderer. Without a
604
+ package, the app at --root (the current folder) is described, into the
605
+ .lib.par at its root. Runs without the app; --registry writes into
606
+ Parity's own registry. Over an existing file it reports new, changed and
607
+ removed components; --only new or --only A,B writes just those, keeping
608
+ the rest as they were. Hand edits (the file's edits, or a shared --edits
609
+ file) are kept, and any whose export has gone are reported. Parts
610
+ and samples come first from the library's own examples and stories,
611
+ fetched from its repository at the commit the installed version was
612
+ published from (cached under ~/.parity/cache); --no-examples skips them.
613
+ An icon set's tags are read from its published metadata on npm at the
614
+ same version; --no-tags skips them. A package named with a version or
615
+ tag (lucide-react@1.63.0, lucide-react@latest) is installed on its own at
616
+ that version, under ~/.parity/cache, rather than read from a project;
617
+ so is one written with --registry and no --root, at the version its
618
+ registry file covers. A file written over keeps its range while it
619
+ covers the version read
620
+ parity lib check <library> [--mode m] [--json] [--record] [--tab t]
621
+ insert every component of a registry library file into the tab's
622
+ project, once alone and once as the Assets panel inserts it, inside its
623
+ parents with its sample, and report the parts that fail alone but render
624
+ inside; exits with 1 below the library's floor in the registry's
625
+ in-app.json. --record keeps why parts failed in the registry's app-errors,
626
+ where parity lib --registry reads the parents they name
627
+ parity open <folder> open a project in a new tab
628
+ parity skill [file] print the design skill, or one of its files
629
+ parity mcp serve these as MCP tools on stdio
630
+
631
+ Options take a tab id, project root or name with --tab; the active tab is the default.`;
632
+ const { positionals, values } = parseArgs({
633
+ allowPositionals: true,
634
+ options: {
635
+ tab: { type: "string" },
636
+ label: { type: "string" },
637
+ "edit-id": { type: "string" },
638
+ edits: { type: "string" },
639
+ state: {
640
+ type: "string",
641
+ multiple: true
642
+ },
643
+ mode: {
644
+ type: "string",
645
+ multiple: true
646
+ },
647
+ scale: { type: "string" },
648
+ json: { type: "boolean" },
649
+ record: { type: "boolean" },
650
+ clear: { type: "boolean" },
651
+ all: { type: "boolean" },
652
+ category: { type: "string" },
653
+ source: { type: "string" },
654
+ search: { type: "string" },
655
+ root: { type: "string" },
656
+ out: { type: "string" },
657
+ registry: { type: "boolean" },
658
+ id: { type: "string" },
659
+ name: { type: "string" },
660
+ description: { type: "string" },
661
+ range: { type: "string" },
662
+ only: { type: "string" },
663
+ "no-examples": { type: "boolean" },
664
+ "no-tags": { type: "boolean" },
665
+ help: {
666
+ type: "boolean",
667
+ short: "h"
668
+ }
669
+ }
670
+ });
671
+ const [command, ...rest] = positionals;
672
+ try {
673
+ await main();
674
+ } catch (error) {
675
+ console.error(error instanceof Error ? error.message : String(error));
676
+ process.exitCode = 1;
677
+ }
678
+ async function main() {
679
+ if (!command || values.help) {
680
+ console.log(usage);
681
+ return;
682
+ }
683
+ switch (command) {
684
+ case "tabs": return print(await call("tabs"));
685
+ case "state": return print(await call("state", { tab: values.tab }));
686
+ case "open":
687
+ if (!rest[0]) throw new Error("parity open <folder>");
688
+ return print(await call("open", { root: resolve(rest[0]) }));
689
+ case "skill":
690
+ console.log(await readSkill(rest[0]));
691
+ return;
692
+ case "mcp":
693
+ serveMcp();
694
+ return;
695
+ case "lib":
696
+ if (rest[0] === "check") return runCheck(rest[1], {
697
+ ...values.tab ? { tab: values.tab } : {},
698
+ ...values.mode?.[0] ? { mode: values.mode[0] } : {},
699
+ ...values.json ? { json: true } : {},
700
+ ...values.record ? { record: true } : {}
701
+ });
702
+ return runLib(rest[0], {
703
+ ...values.root ? { root: values.root } : {},
704
+ ...values.out ? { out: values.out } : {},
705
+ ...values.registry ? { registry: true } : {},
706
+ ...values.id ? { id: values.id } : {},
707
+ ...values.name ? { name: values.name } : {},
708
+ ...values.description ? { description: values.description } : {},
709
+ ...values.range ? { range: values.range } : {},
710
+ ...values.only ? { only: values.only } : {},
711
+ ...values["no-examples"] ? { noExamples: true } : {},
712
+ ...values["no-tags"] ? { noTags: true } : {},
713
+ ...values.edits ? { edits: values.edits } : {},
714
+ ...values.json ? { json: true } : {}
715
+ });
716
+ case "exec": {
717
+ const editId = values["edit-id"];
718
+ let code;
719
+ if (!editId) code = rest[0] ? await readFile(rest[0], "utf8") : await stdin();
720
+ const edits = values.edits ? JSON.parse(values.edits) : void 0;
721
+ report(await call("exec", {
722
+ tab: values.tab,
723
+ label: values.label ?? "Edit from the CLI",
724
+ code,
725
+ editId,
726
+ edits
727
+ }));
728
+ return;
729
+ }
730
+ case "screenshot":
731
+ if (!rest[0]) throw new Error("parity screenshot <design> [--state s] [--mode m]");
732
+ report(await call("screenshot", {
733
+ tab: values.tab,
734
+ design: rest[0],
735
+ state: one(values.state),
736
+ mode: one(values.mode),
737
+ scale: values.scale ? Number(values.scale) : void 0
738
+ }));
739
+ return;
740
+ case "a11y":
741
+ case "accessibility": {
742
+ if (!rest[0]) throw new Error("parity a11y <design> [--state s] [--mode m]");
743
+ const result = await call("accessibility", {
744
+ tab: values.tab,
745
+ design: rest[0],
746
+ state: one(values.state),
747
+ mode: one(values.mode)
748
+ });
749
+ if (values.json) print(result);
750
+ else console.log(describeAccessibility(result));
751
+ if (result.findings.length > 0) process.exitCode = 1;
752
+ return;
753
+ }
754
+ case "problems": {
755
+ if (!rest[0]) throw new Error("parity problems <design> [--state s] [--mode m]");
756
+ const result = await call("problems", {
757
+ tab: values.tab,
758
+ design: rest[0],
759
+ state: one(values.state),
760
+ mode: one(values.mode)
761
+ });
762
+ if (values.json) print(result);
763
+ else console.log(describeProblems(result));
764
+ if (result.problems.length > 0) process.exitCode = 1;
765
+ return;
766
+ }
767
+ case "tokens": {
768
+ const filter = {
769
+ category: values.category,
770
+ source: values.source,
771
+ search: values.search
772
+ };
773
+ const tokens = await call("tokens", {
774
+ tab: values.tab,
775
+ ...filter
776
+ });
777
+ if (values.json) print(tokens);
778
+ else console.log(describeTokens(tokens));
779
+ return;
780
+ }
781
+ case "components": {
782
+ const result = await call("components", { tab: values.tab });
783
+ if (values.json) print(result);
784
+ else console.log(describeComponents(result));
785
+ return;
786
+ }
787
+ case "props": {
788
+ if (!rest[0]) throw new Error("parity props <Component> [--all]");
789
+ const result = await call("props", {
790
+ tab: values.tab,
791
+ component: rest[0],
792
+ all: values.all
793
+ });
794
+ if (values.json) print(result);
795
+ else console.log(describeProps(result));
796
+ return;
797
+ }
798
+ case "console": {
799
+ const result = await call("console", {
800
+ tab: values.tab,
801
+ clear: values.clear
802
+ });
803
+ if (values.json) print(result);
804
+ else console.log(describeConsole(result));
805
+ return;
806
+ }
807
+ default: throw new Error(`Unknown command ${command}\n\n${usage}`);
808
+ }
809
+ }
810
+ function report(result) {
811
+ if (values.json) print({
812
+ ...result,
813
+ screenshots: result.screenshots.map(({ png: _png, ...shot }) => shot)
814
+ });
815
+ else console.log(describeExec(result));
816
+ if (!result.ok) process.exitCode = 1;
817
+ }
818
+ /** A repeated option becomes a list; one value stays a string, so "all" and "shown" work. */
819
+ function one(list) {
820
+ if (!list || list.length === 0) return void 0;
821
+ return list.length === 1 ? list[0] : list;
822
+ }
823
+ function print(value) {
824
+ console.log(JSON.stringify(value, null, 2));
825
+ }
826
+ async function stdin() {
827
+ const chunks = [];
828
+ for await (const chunk of process.stdin) chunks.push(chunk);
829
+ return Buffer.concat(chunks).toString("utf8");
830
+ }
831
+ export {};