@parity-app/cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -1
- package/dist/bin.mjs +1132 -272
- package/dist/client-Cf66-n3R.mjs +724 -0
- package/dist/index.d.mts +211 -17
- package/dist/index.mjs +1 -1
- package/dist/libraries/aria.lib.par +5 -5
- package/dist/libraries/baseui.lib.par +25 -25
- package/dist/libraries/chakra.lib.par +17 -17
- package/dist/libraries/fluent.lib.par +27 -27
- package/dist/libraries/luma.lib.par +20 -20
- package/dist/libraries/lyra.lib.par +20 -20
- package/dist/libraries/maia.lib.par +20 -20
- package/dist/libraries/mantine.lib.par +14 -14
- package/dist/libraries/mira.lib.par +20 -20
- package/dist/libraries/mui.lib.par +5 -5
- package/dist/libraries/nova.lib.par +20 -20
- package/dist/libraries/radix.lib.par +11 -11
- package/dist/libraries/rhea.lib.par +20 -20
- package/dist/libraries/sera.lib.par +20 -20
- package/dist/libraries/vega.lib.par +20 -20
- package/dist/{src-8MhDV_sA.mjs → src-Aoey5Uq0.mjs} +498 -207
- package/dist/{src-DYi23Ypb.mjs → src-BGF7szJh.mjs} +1 -1
- package/dist/{src-FF8eY6EI.mjs → src-DZKi9702.mjs} +3 -2
- package/dist/{src-DXZqtm6z.mjs → src-eso2QXtH.mjs} +1 -1
- package/dist/{src-D9EMN48r.mjs → src-lVLSuOgh.mjs} +138 -38
- package/package.json +8 -6
- package/skills/SKILL.md +61 -14
- package/skills/document.md +11 -1
- package/skills/execute.md +82 -13
- package/skills/guide/states-and-modes.md +5 -3
- package/LICENSE +0 -21
- package/dist/client-DsTLQ2lT.mjs +0 -286
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
/** Reads a registry entry's text, or nothing when it isn't one. A file from before the registry stands for its copy. */
|
|
6
|
+
function readCopyEntry(text) {
|
|
7
|
+
if (text === void 0) return void 0;
|
|
8
|
+
let value;
|
|
9
|
+
try {
|
|
10
|
+
value = JSON.parse(text);
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
15
|
+
const fields = new Map(Object.entries(value));
|
|
16
|
+
const [version, url, token, app, pid, startedAt] = [
|
|
17
|
+
"version",
|
|
18
|
+
"url",
|
|
19
|
+
"token",
|
|
20
|
+
"app",
|
|
21
|
+
"pid",
|
|
22
|
+
"startedAt"
|
|
23
|
+
].map((key) => fields.get(key));
|
|
24
|
+
if (typeof url !== "string" || typeof token !== "string") return void 0;
|
|
25
|
+
const number = (field) => typeof field === "number" ? field : void 0;
|
|
26
|
+
return {
|
|
27
|
+
version: number(version) ?? 0,
|
|
28
|
+
url,
|
|
29
|
+
token,
|
|
30
|
+
app: number(app) ?? number(pid) ?? 0,
|
|
31
|
+
startedAt: number(startedAt) ?? 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A project folder as routing compares it: forward slashes, no trailing slash, and on Windows (a drive letter or a
|
|
36
|
+
* share) without case, since `C:\Work` and `c:/work` are the same folder there.
|
|
37
|
+
*/
|
|
38
|
+
function normalizeRoot(path) {
|
|
39
|
+
const windows = /^(?:[A-Za-z]:|[\\/]{2})/.test(path);
|
|
40
|
+
let normal = windows ? path.replaceAll("\\", "/") : path;
|
|
41
|
+
normal = normal.replace(/(?<!^)\/{2,}/g, "/");
|
|
42
|
+
if (normal.length > 1 && !/^[A-Za-z]:\/$/.test(normal)) normal = normal.replace(/\/+$/, "");
|
|
43
|
+
return windows ? normal.toLowerCase() : normal;
|
|
44
|
+
}
|
|
45
|
+
/** The tab a reference names: its id, then its project folder, then its name. */
|
|
46
|
+
function matchTab(tabs, reference) {
|
|
47
|
+
const root = normalizeRoot(reference);
|
|
48
|
+
return tabs.find((tab) => tab.id === reference) ?? tabs.find((tab) => normalizeRoot(tab.root) === root) ?? tabs.find((tab) => tab.name === reference);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The open project a folder is in: the deepest one whose folder holds it, so a nested project wins over its parent.
|
|
52
|
+
* `real` gives a folder's real path, so a project reached through a link, as macOS's `/var` is `/private/var`, still
|
|
53
|
+
* holds a folder inside it however each was named; routing itself reads no files, so the caller passes it.
|
|
54
|
+
*/
|
|
55
|
+
function projectFor(folder, tabs, real = (path) => path) {
|
|
56
|
+
const inside = normalizeRoot(real(folder));
|
|
57
|
+
let best;
|
|
58
|
+
for (const tab of tabs) {
|
|
59
|
+
const root = normalizeRoot(real(tab.root));
|
|
60
|
+
if ((inside === root || inside.startsWith(root.endsWith("/") ? root : `${root}/`)) && (!best || root.length > normalizeRoot(real(best.root)).length)) best = tab;
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where an agent's call goes. A tab reference goes to the copy that has that project open; `active` to the active tab
|
|
66
|
+
* of the copy the person used last. Without one, a call goes to the project the agent's folder is in when it's open,
|
|
67
|
+
* and to `active` when it isn't. A project open in two copies goes to the one that has been running longest, which is
|
|
68
|
+
* the person's own app more often than a copy a session started after it. `open` goes to a copy that has the project
|
|
69
|
+
* already, and otherwise to the one the person used last.
|
|
70
|
+
*/
|
|
71
|
+
function route(copies, request) {
|
|
72
|
+
const open = openTabs(copies);
|
|
73
|
+
if (copies.length === 0) return {
|
|
74
|
+
kind: "none",
|
|
75
|
+
code: "not_running",
|
|
76
|
+
message: "Parity isn't running. Start the app, then try again.",
|
|
77
|
+
open
|
|
78
|
+
};
|
|
79
|
+
if (request.kind === "open") return {
|
|
80
|
+
kind: "copy",
|
|
81
|
+
copy: holdersOf(copies, request.root)[0]?.copy ?? lastUsed(copies)
|
|
82
|
+
};
|
|
83
|
+
const reference = request.tab?.trim();
|
|
84
|
+
if (reference && reference !== "active") {
|
|
85
|
+
const holding = holdersOf(copies, reference);
|
|
86
|
+
if (holding[0]) return {
|
|
87
|
+
kind: "copy",
|
|
88
|
+
copy: holding[0].copy,
|
|
89
|
+
tab: holding[0].tab
|
|
90
|
+
};
|
|
91
|
+
const ids = open.map((tab) => tab.id);
|
|
92
|
+
return {
|
|
93
|
+
kind: "none",
|
|
94
|
+
code: "unknown_tab",
|
|
95
|
+
message: `No open tab matches "${reference}". Tabs: ${ids.length ? ids.join(", ") : "none"}`,
|
|
96
|
+
open
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!reference) {
|
|
100
|
+
const project = projectFor(request.folder, open, request.real);
|
|
101
|
+
if (project) {
|
|
102
|
+
const holding = holdersOf(copies, project.root);
|
|
103
|
+
if (holding[0]) return {
|
|
104
|
+
kind: "copy",
|
|
105
|
+
copy: holding[0].copy,
|
|
106
|
+
tab: holding[0].tab
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const looking = [...copies].sort(byLastUsed).find((copy) => copy.report.tabs.some((tab) => tab.active));
|
|
111
|
+
const tab = looking?.report.tabs.find((candidate) => candidate.active);
|
|
112
|
+
if (looking && tab) return {
|
|
113
|
+
kind: "copy",
|
|
114
|
+
copy: looking,
|
|
115
|
+
tab
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
kind: "none",
|
|
119
|
+
code: "unknown_tab",
|
|
120
|
+
message: "No project is open in Parity",
|
|
121
|
+
open
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every copy's tabs as one list, one entry per project: from the copy its calls go to, and active only for the tab
|
|
126
|
+
* `active` names.
|
|
127
|
+
*/
|
|
128
|
+
function openTabs(copies) {
|
|
129
|
+
const shown = [...copies].sort(byLastUsed).find((copy) => copy.report.tabs.some((tab) => tab.active))?.report.tabs.find((tab) => tab.active);
|
|
130
|
+
const tabs = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const copy of [...copies].sort(byAge)) for (const tab of copy.report.tabs) {
|
|
132
|
+
const key = normalizeRoot(tab.root);
|
|
133
|
+
if (tabs.has(key)) continue;
|
|
134
|
+
tabs.set(key, {
|
|
135
|
+
...tab,
|
|
136
|
+
active: !!shown && normalizeRoot(shown.root) === key
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return [...tabs.values()];
|
|
140
|
+
}
|
|
141
|
+
/** The copies that have a project open, oldest first, with the tab they have it in. */
|
|
142
|
+
function holdersOf(copies, reference) {
|
|
143
|
+
return [...copies].sort(byAge).flatMap((copy) => {
|
|
144
|
+
const tab = matchTab(copy.report.tabs, reference);
|
|
145
|
+
return tab ? [{
|
|
146
|
+
copy,
|
|
147
|
+
tab
|
|
148
|
+
}] : [];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function lastUsed(copies) {
|
|
152
|
+
return [...copies].sort(byLastUsed)[0];
|
|
153
|
+
}
|
|
154
|
+
/** The copy focused last first; one that can't say, or a tie, goes to the one that has been running longest. */
|
|
155
|
+
function byLastUsed(a, b) {
|
|
156
|
+
return (b.report.focusedAt ?? 0) - (a.report.focusedAt ?? 0) || byAge(a, b);
|
|
157
|
+
}
|
|
158
|
+
/** The copy running longest first, then by process, so the order never depends on how the registry was listed. */
|
|
159
|
+
function byAge(a, b) {
|
|
160
|
+
return a.entry.startedAt - b.entry.startedAt || a.entry.app - b.entry.app;
|
|
161
|
+
}
|
|
162
|
+
// ../agent/src/registry.ts
|
|
163
|
+
/** The registry these settings name: `PARITY_AGENT_FILE`, else `PARITY_AGENT_DIR`, else `~/.parity/agents`. */
|
|
164
|
+
function agentRegistry(env, home) {
|
|
165
|
+
if (env.PARITY_AGENT_FILE) return {
|
|
166
|
+
kind: "file",
|
|
167
|
+
path: env.PARITY_AGENT_FILE
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
kind: "folder",
|
|
171
|
+
path: env.PARITY_AGENT_DIR || join(home, ".parity", "agents")
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** The file before the registry, where an app from before it says how to reach it. */
|
|
175
|
+
function legacyAgentFile(home) {
|
|
176
|
+
return join(home, ".parity", "agent.json");
|
|
177
|
+
}
|
|
178
|
+
/** Where a copy's own entry goes. */
|
|
179
|
+
function entryPath(registry, app) {
|
|
180
|
+
return registry.kind === "file" ? registry.path : join(registry.path, `${app}.json`);
|
|
181
|
+
}
|
|
182
|
+
/** Removes an entry the CLI found for a copy that isn't running. */
|
|
183
|
+
function removeStaleEntry(registry, entry) {
|
|
184
|
+
if (registry.kind === "folder") rmSync(entryPath(registry, entry.app), { force: true });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Every entry the registry has, with the file before the registry when an older app wrote it, so a CLI reaches an app
|
|
188
|
+
* from before the registry too.
|
|
189
|
+
*/
|
|
190
|
+
function readEntries(registry, home) {
|
|
191
|
+
if (registry.kind === "file") {
|
|
192
|
+
const entry = readCopyEntry(readText(registry.path));
|
|
193
|
+
return entry ? [entry] : [];
|
|
194
|
+
}
|
|
195
|
+
let names = [];
|
|
196
|
+
try {
|
|
197
|
+
names = readdirSync(registry.path).filter((name) => /^\d+\.json$/.test(name));
|
|
198
|
+
} catch {}
|
|
199
|
+
const entries = names.flatMap((name) => {
|
|
200
|
+
const entry = readCopyEntry(readText(join(registry.path, name)));
|
|
201
|
+
return entry ? [entry] : [];
|
|
202
|
+
});
|
|
203
|
+
const legacy = readCopyEntry(readText(legacyAgentFile(home)));
|
|
204
|
+
if (legacy && !entries.some((entry) => entry.url === legacy.url)) entries.push(legacy);
|
|
205
|
+
return entries;
|
|
206
|
+
}
|
|
207
|
+
/** Whether a process is still running. One the person's account can't signal still is. */
|
|
208
|
+
function isRunning(pid) {
|
|
209
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
210
|
+
try {
|
|
211
|
+
process.kill(pid, 0);
|
|
212
|
+
return true;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function readText(path) {
|
|
218
|
+
try {
|
|
219
|
+
return readFileSync(path, "utf8");
|
|
220
|
+
} catch {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ../core/src/errors.ts
|
|
225
|
+
/**
|
|
226
|
+
* Why an agent's call failed, as a stable code it can act on without reading the message. The CLI and MCP publish
|
|
227
|
+
* these; the app throws them where it knows the cause.
|
|
228
|
+
*/
|
|
229
|
+
const errorCodes = [
|
|
230
|
+
"invalid_input",
|
|
231
|
+
"not_running",
|
|
232
|
+
"not_answering",
|
|
233
|
+
"unknown_tab",
|
|
234
|
+
"tab_not_ready",
|
|
235
|
+
"unknown_design",
|
|
236
|
+
"unknown_node",
|
|
237
|
+
"ambiguous_reference",
|
|
238
|
+
"unknown_state",
|
|
239
|
+
"unknown_mode",
|
|
240
|
+
"unknown_viewport",
|
|
241
|
+
"unknown_component",
|
|
242
|
+
"unknown_icon_set",
|
|
243
|
+
"unknown_edit_id",
|
|
244
|
+
"conflict",
|
|
245
|
+
"execution_failed",
|
|
246
|
+
"app_error"
|
|
247
|
+
];
|
|
248
|
+
/** An error with a stable code, the field it's about and what to use instead. */
|
|
249
|
+
var CodedError = class extends Error {
|
|
250
|
+
details;
|
|
251
|
+
constructor(message, details) {
|
|
252
|
+
super(message);
|
|
253
|
+
this.name = "CodedError";
|
|
254
|
+
this.details = {
|
|
255
|
+
...details,
|
|
256
|
+
retryable: details.retryable ?? false
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
/** An unknown reference, naming the valid ones closest to what was asked for, up to `alternativeLimit`. */
|
|
261
|
+
function unknownReference(code, message, reference, candidates, field) {
|
|
262
|
+
return new CodedError(message, {
|
|
263
|
+
code,
|
|
264
|
+
...field === void 0 ? {} : { field },
|
|
265
|
+
alternatives: closest(reference, candidates)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Candidates sharing text with the reference first, ignoring case, then the rest by how few single-character edits
|
|
270
|
+
* away they are, so a typo's intended id makes the cut; ties keep their own order. All of them when they fit.
|
|
271
|
+
*/
|
|
272
|
+
function closest(reference, candidates, limit = 20) {
|
|
273
|
+
const unique = [...new Set(candidates)];
|
|
274
|
+
if (unique.length <= limit) return unique;
|
|
275
|
+
const wanted = reference.toLowerCase();
|
|
276
|
+
const ranked = unique.map((candidate, index) => {
|
|
277
|
+
const lower = candidate.toLowerCase();
|
|
278
|
+
const near = wanted.length > 0 && (lower.includes(wanted) || wanted.includes(lower));
|
|
279
|
+
return {
|
|
280
|
+
candidate,
|
|
281
|
+
index,
|
|
282
|
+
near: near ? 0 : 1,
|
|
283
|
+
distance: near ? 0 : editDistance(wanted, lower)
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
ranked.sort((a, b) => a.near - b.near || a.distance - b.distance || a.index - b.index);
|
|
287
|
+
return ranked.slice(0, limit).map(({ candidate }) => candidate);
|
|
288
|
+
}
|
|
289
|
+
/** How many single-character insertions, deletions or substitutions turn one string into the other. */
|
|
290
|
+
function editDistance(a, b) {
|
|
291
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
292
|
+
for (let i = 1; i <= a.length; i++) {
|
|
293
|
+
const row = [i];
|
|
294
|
+
for (let j = 1; j <= b.length; j++) row[j] = Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
295
|
+
previous = row;
|
|
296
|
+
}
|
|
297
|
+
return previous[b.length];
|
|
298
|
+
}
|
|
299
|
+
/** A thrown value's details, when it carries them. */
|
|
300
|
+
function errorDetails(error) {
|
|
301
|
+
return error instanceof CodedError ? error.details : void 0;
|
|
302
|
+
}
|
|
303
|
+
/** Details that came from another process, checked before they're trusted. Anything malformed reads as none. */
|
|
304
|
+
function readErrorDetails(value) {
|
|
305
|
+
if (!value || typeof value !== "object") return void 0;
|
|
306
|
+
const code = "code" in value ? value.code : void 0;
|
|
307
|
+
const known = errorCodes.find((candidate) => candidate === code);
|
|
308
|
+
if (!known) return void 0;
|
|
309
|
+
const retryable = "retryable" in value && value.retryable === true;
|
|
310
|
+
const field = "field" in value && typeof value.field === "string" && value.field ? value.field : void 0;
|
|
311
|
+
const alternatives = "alternatives" in value && Array.isArray(value.alternatives) ? value.alternatives.filter((item) => typeof item === "string").slice(0, 20) : void 0;
|
|
312
|
+
return {
|
|
313
|
+
code: known,
|
|
314
|
+
retryable,
|
|
315
|
+
...field === void 0 ? {} : { field },
|
|
316
|
+
...alternatives === void 0 ? {} : { alternatives }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// src/caller.ts
|
|
320
|
+
/** Declared caller names are displayed verbatim apart from surrounding whitespace. */
|
|
321
|
+
function declaredName(value) {
|
|
322
|
+
return typeof value === "string" ? value.trim() || void 0 : void 0;
|
|
323
|
+
}
|
|
324
|
+
/** Reserve the session suffix inside the desktop boundary's 80-character caller-id limit. */
|
|
325
|
+
function sessionCallerId(name, session) {
|
|
326
|
+
return `${name.slice(0, 71)}#${session.slice(0, 8)}`;
|
|
327
|
+
}
|
|
328
|
+
/** MCP supplies its own identity in initialize; prefer its display title over its protocol name. */
|
|
329
|
+
function mcpCallerName(clientInfo) {
|
|
330
|
+
return declaredName(clientInfo?.title) ?? declaredName(clientInfo?.name) ?? "Agent";
|
|
331
|
+
}
|
|
332
|
+
/** CLI callers opt in to attribution explicitly; ambient tool configuration is not identity. */
|
|
333
|
+
function cliCallerName(env) {
|
|
334
|
+
return declaredName(env.PARITY_AGENT_NAME) ?? "Terminal";
|
|
335
|
+
}
|
|
336
|
+
// src/client.ts
|
|
337
|
+
/**
|
|
338
|
+
* Where running copies of the app say how to reach them: `~/.parity/agents`, or the one file `PARITY_AGENT_FILE` names
|
|
339
|
+
* for a copy kept apart from the person's.
|
|
340
|
+
*/
|
|
341
|
+
const registry = agentRegistry(process.env, homedir());
|
|
342
|
+
let caller = cliCallerName(process.env);
|
|
343
|
+
/** Tells callers with the same name apart. A terminal's calls are separate processes, so they share their name. */
|
|
344
|
+
const explicitCallerId = process.env.PARITY_AGENT_ID?.trim() || void 0;
|
|
345
|
+
let callerId = explicitCallerId;
|
|
346
|
+
/**
|
|
347
|
+
* Names who's calling in later calls, so the canvas can say who's at work. The MCP server learns the name from its
|
|
348
|
+
* client, and is one session: its calls share an id, apart from any other session of the same agent.
|
|
349
|
+
*/
|
|
350
|
+
function setCaller(name, session = false) {
|
|
351
|
+
caller = name ?? caller;
|
|
352
|
+
callerId = session ? sessionCallerId(caller, randomUUID()) : explicitCallerId;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Calls the running Parity app: the copy that has the call's project open, with the exact tab it chose, by the rules
|
|
356
|
+
* in `@parity-app/agent/routing`. Fails with a clear message, and a code, when no copy is running, none has the
|
|
357
|
+
* project, or the call fails: the app's own code when it gives one.
|
|
358
|
+
*/
|
|
359
|
+
async function call(method, params = {}, routing = "tab") {
|
|
360
|
+
const copies = await runningCopies();
|
|
361
|
+
if (routing === "copies") return openTabs(copies);
|
|
362
|
+
const target = route(copies, routing === "open" ? {
|
|
363
|
+
kind: "open",
|
|
364
|
+
root: String(params.root)
|
|
365
|
+
} : {
|
|
366
|
+
kind: "tab",
|
|
367
|
+
...typeof params.tab === "string" ? { tab: params.tab } : {},
|
|
368
|
+
folder: process.cwd(),
|
|
369
|
+
real
|
|
370
|
+
});
|
|
371
|
+
if (target.kind === "none") {
|
|
372
|
+
if (target.code === "not_running") throw new CodedError(target.message, {
|
|
373
|
+
code: target.code,
|
|
374
|
+
retryable: true
|
|
375
|
+
});
|
|
376
|
+
const ids = target.open.map((tab) => tab.id);
|
|
377
|
+
throw unknownReference("unknown_tab", target.message, typeof params.tab === "string" ? params.tab : "", ids, "tab");
|
|
378
|
+
}
|
|
379
|
+
return send(target.copy.entry, method, target.tab ? {
|
|
380
|
+
...params,
|
|
381
|
+
tab: target.tab.id
|
|
382
|
+
} : params);
|
|
383
|
+
}
|
|
384
|
+
/** A folder's real path, through any link, or the folder as named when it can't be read. */
|
|
385
|
+
function real(path) {
|
|
386
|
+
try {
|
|
387
|
+
return realpathSync.native(path);
|
|
388
|
+
} catch {
|
|
389
|
+
return path;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Every copy that answers, with what it has open. An entry whose copy has quit is removed on the way. */
|
|
393
|
+
async function runningCopies() {
|
|
394
|
+
const entries = readEntries(registry, homedir());
|
|
395
|
+
const newer = entries.find((entry) => entry.version > 1);
|
|
396
|
+
if (newer) throw new CodedError(`The Parity at ${newer.url} is newer than this CLI. Update @parity-app/cli, then try again.`, {
|
|
397
|
+
code: "not_running",
|
|
398
|
+
retryable: false
|
|
399
|
+
});
|
|
400
|
+
const copies = (await Promise.all(entries.map(async (entry) => {
|
|
401
|
+
const report = await reportOf(entry);
|
|
402
|
+
if (!report && !isRunning(entry.app)) removeStaleEntry(registry, entry);
|
|
403
|
+
return report ? [{
|
|
404
|
+
entry,
|
|
405
|
+
report
|
|
406
|
+
}] : [];
|
|
407
|
+
}))).flat();
|
|
408
|
+
if (entries.length > 0 && copies.length === 0) throw new CodedError(`Parity isn't answering at ${entries.map((entry) => entry.url).join(", ")}. Is the app still running?`, {
|
|
409
|
+
code: "not_answering",
|
|
410
|
+
retryable: true
|
|
411
|
+
});
|
|
412
|
+
if (copies.length === 0) throw new CodedError(`Parity isn't running (nothing in ${registry.path}). Start the app, then try again.`, {
|
|
413
|
+
code: "not_running",
|
|
414
|
+
retryable: true
|
|
415
|
+
});
|
|
416
|
+
return copies;
|
|
417
|
+
}
|
|
418
|
+
/** What a copy has open and when it was last used; just its tabs from an app too old to say more. */
|
|
419
|
+
async function reportOf(entry) {
|
|
420
|
+
try {
|
|
421
|
+
return await send(entry, "copy", {}, 5e3);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (!(error instanceof CodedError) || error.details.code !== "app_error") return void 0;
|
|
424
|
+
try {
|
|
425
|
+
return { tabs: await send(entry, "tabs", {}, 5e3) };
|
|
426
|
+
} catch {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function send(entry, method, params, timeout) {
|
|
432
|
+
let response;
|
|
433
|
+
try {
|
|
434
|
+
response = await fetch(`${entry.url}/rpc`, {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: {
|
|
437
|
+
"content-type": "application/json",
|
|
438
|
+
authorization: `Bearer ${entry.token}`
|
|
439
|
+
},
|
|
440
|
+
body: JSON.stringify({
|
|
441
|
+
method,
|
|
442
|
+
params,
|
|
443
|
+
agent: caller,
|
|
444
|
+
agentId: callerId
|
|
445
|
+
}),
|
|
446
|
+
...timeout === void 0 ? {} : { signal: AbortSignal.timeout(timeout) }
|
|
447
|
+
});
|
|
448
|
+
} catch {
|
|
449
|
+
throw new CodedError(`Parity isn't answering at ${entry.url}. Is the app still running?`, {
|
|
450
|
+
code: "not_answering",
|
|
451
|
+
retryable: true
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const body = await response.json();
|
|
455
|
+
if (body.error !== void 0) throw new CodedError(body.error, readErrorDetails(body.details) ?? { code: "app_error" });
|
|
456
|
+
return body.result;
|
|
457
|
+
}
|
|
458
|
+
/** A readable account of an exec call, for terminals and for agents. */
|
|
459
|
+
function describeExec(result) {
|
|
460
|
+
const lines = [];
|
|
461
|
+
if (result.ok) lines.push(result.applied ? "Done. The changes are on the canvas as one undoable step." : "Done. Nothing changed.");
|
|
462
|
+
else {
|
|
463
|
+
const error = result.error;
|
|
464
|
+
lines.push(`Failed, nothing changed: ${error?.message ?? "unknown error"}`);
|
|
465
|
+
if (error?.retryable) lines.push("Nothing in the request was wrong; run it again.");
|
|
466
|
+
if (error?.editId) lines.push(`Fix it by retrying with editId "${error.editId}" and edits ({find, replace}) to the snippet.`);
|
|
467
|
+
}
|
|
468
|
+
if (result.output.length > 0) lines.push("", "Output:", ...result.output);
|
|
469
|
+
const created = Object.entries(result.created);
|
|
470
|
+
if (created.length > 0) lines.push("", "Created:", ...created.map(([name, id]) => ` ${name} = ${id}`));
|
|
471
|
+
if (result.warnings.length > 0) lines.push("", "Warnings (fix these next):", ...result.warnings.map((warning) => ` ${warning}`));
|
|
472
|
+
if (result.screenshots.length > 0) lines.push("", "Screenshots:", ...result.screenshots.map((shot) => ` ${shot.design} / ${shot.state} / ${shot.mode}: ${shot.path}`));
|
|
473
|
+
return lines.join("\n");
|
|
474
|
+
}
|
|
475
|
+
/** Where a tab's edits stand: the revision, what's applied and undoable, what's on disk, and what failed to write. */
|
|
476
|
+
function describeStatus(result) {
|
|
477
|
+
const { tab, revision, unsaved, undo, redo, save, errors } = result;
|
|
478
|
+
const steps = (count) => count === 1 ? "1 step" : `${count} steps`;
|
|
479
|
+
const lines = [`${tab.name} is at revision ${revision}.`];
|
|
480
|
+
if (unsaved.length === 0) lines.push("Everything applied is saved.");
|
|
481
|
+
else {
|
|
482
|
+
const how = result.autosave ? "Automatic saving is on, so they save themselves." : "They wait for the person to save; agents can't.";
|
|
483
|
+
lines.push(`${unsaved.length === 1 ? "1 file isn't" : `${unsaved.length} files aren't`} saved. ${how}`);
|
|
484
|
+
for (const file of unsaved) {
|
|
485
|
+
const parts = [];
|
|
486
|
+
if (file.designs) {
|
|
487
|
+
const { added, changed, removed } = file.designs;
|
|
488
|
+
if (added.length > 0) parts.push(`added ${added.join(", ")}`);
|
|
489
|
+
if (changed.length > 0) parts.push(`changed ${changed.join(", ")}`);
|
|
490
|
+
if (removed.length > 0) parts.push(`removed ${removed.join(", ")}`);
|
|
491
|
+
}
|
|
492
|
+
if (file.modes) parts.push("changed the modes");
|
|
493
|
+
if (file.tokens?.length) parts.push(`token values ${file.tokens.join(", ")}`);
|
|
494
|
+
if (file.more) parts.push(`and ${file.more} more`);
|
|
495
|
+
lines.push(` ${file.path}${parts.length > 0 ? `: ${parts.join("; ")}` : ""}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (save.status === "saving") lines.push("Saving now.");
|
|
499
|
+
if (errors.length > 0) lines.push("", "Failed to write:", ...errors.map((entry) => ` ${entry.path}: ${entry.error}`));
|
|
500
|
+
lines.push("", `Undo has ${steps(undo.count)}${undo.labels?.length ? `: ${undo.labels.join(", ")}` : ""}.`);
|
|
501
|
+
if (redo.count > 0) lines.push(`Redo has ${steps(redo.count)}${redo.labels?.length ? `: ${redo.labels.join(", ")}` : ""}.`);
|
|
502
|
+
return lines.join("\n");
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The project's viewports (what Responsive takes), its other variables, its modes (what renders take) and what each
|
|
506
|
+
* design's board shows, with a named design's frames.
|
|
507
|
+
*/
|
|
508
|
+
function describeModes(result) {
|
|
509
|
+
const { variables, viewports, modes, designs } = result;
|
|
510
|
+
const lines = ["Viewports, the ids Responsive(node, viewport, patch) takes:", ...viewports.map((viewport) => ` ${viewport.id}: ${viewport.width} wide, in a window ${viewport.height} tall (${viewport.name})`)];
|
|
511
|
+
const others = variables.filter((variable) => variable.kind !== "viewport");
|
|
512
|
+
if (others.length > 0) {
|
|
513
|
+
lines.push("", "Other variables:");
|
|
514
|
+
for (const variable of others) {
|
|
515
|
+
const options = variable.options.map((option) => {
|
|
516
|
+
const effects = [...option.className ? [`class ${option.className}`] : [], ...Object.entries(option.attributes ?? {}).map(([name, value]) => `${name}="${value}"`)];
|
|
517
|
+
return effects.length > 0 ? `${option.id} (${effects.join(", ")})` : option.id;
|
|
518
|
+
});
|
|
519
|
+
lines.push(` ${variable.id} (${variable.kind}): ${options.join(", ")}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
lines.push("", "Modes, the ids screenshots and checks take, primary first:");
|
|
523
|
+
for (const mode of modes) {
|
|
524
|
+
const values = Object.entries(mode.values).map(([variable, option]) => `${variable} ${option}`).join(", ");
|
|
525
|
+
lines.push(` ${mode.id}${mode.primary ? " (primary)" : ""}: ${values}; ${mode.width} by ${mode.height}`);
|
|
526
|
+
}
|
|
527
|
+
lines.push("", designs.length === 1 ? "Design:" : "Designs:");
|
|
528
|
+
for (const design of designs) {
|
|
529
|
+
const states = design.states.length > 1 ? `every state (${design.states.join(", ")})` : `state ${design.state}`;
|
|
530
|
+
const beside = design.beside.length > 0 ? `, beside it ${design.beside.join(", ")}` : "";
|
|
531
|
+
lines.push(` ${design.id} (${design.name}): ${states}; main mode ${design.main}${beside}`);
|
|
532
|
+
for (const frame of design.frames ?? []) {
|
|
533
|
+
const size = frame.height === void 0 ? `${frame.width} wide, growing` : `${frame.width} by ${frame.height}`;
|
|
534
|
+
lines.push(` ${frame.mode}: ${size}, in a ${frame.window.width} by ${frame.window.height} window`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return lines.join("\n");
|
|
538
|
+
}
|
|
539
|
+
const impactOrder = [
|
|
540
|
+
"critical",
|
|
541
|
+
"serious",
|
|
542
|
+
"moderate",
|
|
543
|
+
"minor"
|
|
544
|
+
];
|
|
545
|
+
/**
|
|
546
|
+
* A readable account of an accessibility check: each rule broken, worst first, with the layers that break it, the
|
|
547
|
+
* frames they break it in and what's wrong. Alike layers, such as the rows of a list, share a line.
|
|
548
|
+
*/
|
|
549
|
+
function describeAccessibility(result) {
|
|
550
|
+
const { design, findings, frames } = result;
|
|
551
|
+
const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
|
|
552
|
+
const rules = /* @__PURE__ */ new Map();
|
|
553
|
+
for (const finding of findings) rules.set(finding.rule, [...rules.get(finding.rule) ?? [], finding]);
|
|
554
|
+
const lines = [rules.size === 0 ? `No issues found in ${checked}, with axe-core against WCAG 2.2 AA.` : `${rules.size === 1 ? "1 rule" : `${rules.size} rules`} broken in ${checked}, with axe-core against WCAG 2.2 AA.`];
|
|
555
|
+
const groups = [...rules.values()].sort((a, b) => impactOrder.indexOf(a[0].impact) - impactOrder.indexOf(b[0].impact));
|
|
556
|
+
for (const group of groups) {
|
|
557
|
+
const { impact, rule, help, helpUrl } = group[0];
|
|
558
|
+
lines.push("", `${impact}: ${help} (${rule})`);
|
|
559
|
+
const byLayer = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const finding of group) {
|
|
561
|
+
const key = `${finding.node ?? ""}\n${finding.detail}`;
|
|
562
|
+
const layer = byLayer.get(key) ?? {
|
|
563
|
+
name: finding.name ?? "The design",
|
|
564
|
+
nodes: finding.node ? [finding.node] : [],
|
|
565
|
+
detail: finding.detail,
|
|
566
|
+
frames: []
|
|
567
|
+
};
|
|
568
|
+
layer.frames.push({
|
|
569
|
+
state: finding.state,
|
|
570
|
+
mode: finding.mode
|
|
571
|
+
});
|
|
572
|
+
byLayer.set(key, layer);
|
|
573
|
+
}
|
|
574
|
+
const alike = /* @__PURE__ */ new Map();
|
|
575
|
+
for (const layer of byLayer.values()) {
|
|
576
|
+
const same = JSON.stringify([
|
|
577
|
+
layer.name,
|
|
578
|
+
layer.detail,
|
|
579
|
+
layer.frames
|
|
580
|
+
]);
|
|
581
|
+
const existing = alike.get(same);
|
|
582
|
+
if (existing) existing.nodes.push(...layer.nodes);
|
|
583
|
+
else alike.set(same, layer);
|
|
584
|
+
}
|
|
585
|
+
for (const layer of alike.values()) {
|
|
586
|
+
const where = layer.frames.length === frames && frames > 1 ? "every frame" : frameList(layer.frames);
|
|
587
|
+
const ids = layer.nodes.length > 0 ? ` (${layer.nodes.join(", ")})` : "";
|
|
588
|
+
lines.push(` ${layer.name}${ids}, in ${where}: ${layer.detail}`);
|
|
589
|
+
}
|
|
590
|
+
lines.push(` How to fix it: ${helpUrl}`);
|
|
591
|
+
}
|
|
592
|
+
if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
|
|
593
|
+
lines.push("", "Automated checks find some issues, not all. Try the design with a keyboard and a screen reader too.");
|
|
594
|
+
return lines.join("\n");
|
|
595
|
+
}
|
|
596
|
+
/** A readable account of the layout problems in a design's frames, each once with the frames it's in. */
|
|
597
|
+
function describeProblems(result) {
|
|
598
|
+
const { design, problems, frames } = result;
|
|
599
|
+
const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
|
|
600
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
601
|
+
for (const { problem, node, state, mode } of problems) {
|
|
602
|
+
const key = `${node ?? ""}\n${problem}`;
|
|
603
|
+
const entry = grouped.get(key) ?? {
|
|
604
|
+
problem,
|
|
605
|
+
...node ? { node } : {},
|
|
606
|
+
frames: []
|
|
607
|
+
};
|
|
608
|
+
entry.frames.push({
|
|
609
|
+
state,
|
|
610
|
+
mode
|
|
611
|
+
});
|
|
612
|
+
grouped.set(key, entry);
|
|
613
|
+
}
|
|
614
|
+
const count = grouped.size === 1 ? "1 problem" : `${grouped.size} problems`;
|
|
615
|
+
const lines = [grouped.size === 0 ? `No problems found in ${checked}.` : `${count} in ${checked}:`];
|
|
616
|
+
for (const { problem, node, frames: where } of grouped.values()) {
|
|
617
|
+
const every = where.length === frames && frames > 1 ? "every frame" : frameList(where);
|
|
618
|
+
lines.push(` ${node ? `${node}: ` : ""}${problem}, in ${every}`);
|
|
619
|
+
}
|
|
620
|
+
if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((warning) => ` ${warning}`));
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
/** Tokens by category, each with its value in every theme that sets one. */
|
|
624
|
+
function describeTokens({ tokens, total, truncated }) {
|
|
625
|
+
if (tokens.length === 0) return "No tokens match.";
|
|
626
|
+
const lines = [`${`${total === 1 ? "1 token" : `${total} tokens`}${truncated ? `, the first ${tokens.length} shown` : ""}`}. Name them in styles without "--".`];
|
|
627
|
+
if (truncated) lines.push("Narrow them with category, source or search, or raise limit, to see the rest.");
|
|
628
|
+
const categories = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const token of tokens) {
|
|
630
|
+
const key = `${token.category}, ${token.source}`;
|
|
631
|
+
categories.set(key, [...categories.get(key) ?? [], token]);
|
|
632
|
+
}
|
|
633
|
+
for (const [category, list] of categories) {
|
|
634
|
+
lines.push("", `${category}:`);
|
|
635
|
+
for (const token of list) {
|
|
636
|
+
const values = Object.entries(token.values).sort(([a], [b]) => first("default", a, b)).map(([theme, value]) => `${theme} ${value}`);
|
|
637
|
+
lines.push(` ${token.name}: ${values.join(", ")}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return lines.join("\n");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The project's own components, then each library's, then the icon libraries, then the exports left out and why, the
|
|
644
|
+
* components failing to draw and why, and what the person corrected by hand.
|
|
645
|
+
*/
|
|
646
|
+
function describeComponents({ components, iconLibraries = [], leftOut = [], drawn = 0, failing = [], corrections = {} }) {
|
|
647
|
+
const groups = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const component of components) {
|
|
649
|
+
const slash = component.indexOf("/");
|
|
650
|
+
const group = slash > 0 ? component.slice(0, slash) : "";
|
|
651
|
+
groups.set(group, [...groups.get(group) ?? [], slash > 0 ? component.slice(slash + 1) : component]);
|
|
652
|
+
}
|
|
653
|
+
const lines = [];
|
|
654
|
+
for (const [group, names] of [...groups].sort(([a], [b]) => first("", a, b))) lines.push(group ? `${group}, as ${group}/Name:` : "The project's own:", ` ${names.join(", ")}`, "");
|
|
655
|
+
if (iconLibraries.length > 0) lines.push(`Icons: ${iconLibraries.map((library) => `${library}, as ${library}/Name`).join("; ")}`, "");
|
|
656
|
+
if (leftOut.length > 0) {
|
|
657
|
+
lines.push("Not components:");
|
|
658
|
+
for (const entry of leftOut) lines.push(` ${entry.name}, in ${entry.path}: ${entry.undecided ? "can't tell, " : ""}${entry.reason}`);
|
|
659
|
+
lines.push("");
|
|
660
|
+
}
|
|
661
|
+
if (failing.length > 0) {
|
|
662
|
+
lines.push(`Failing to draw on the canvas, ${failing.length} of ${failing.length + drawn} drawn so far:`);
|
|
663
|
+
for (const entry of failing) lines.push(` ${entry.id}: ${entry.reason}`);
|
|
664
|
+
lines.push("");
|
|
665
|
+
}
|
|
666
|
+
const { include = [] } = corrections;
|
|
667
|
+
if (include.length > 0) {
|
|
668
|
+
const name = (ref) => ref.export === "default" ? "the default export" : ref.export;
|
|
669
|
+
lines.push("Corrected by hand in the project file:");
|
|
670
|
+
for (const ref of include) lines.push(` listed: ${name(ref)}, in ${ref.path}`);
|
|
671
|
+
}
|
|
672
|
+
return lines.join("\n").trimEnd();
|
|
673
|
+
}
|
|
674
|
+
/** A component's props, with each variant's options and default. */
|
|
675
|
+
function describeProps(result) {
|
|
676
|
+
if (!result.known) return `${result.component}'s props haven't been read from its source yet. Try again in a moment.`;
|
|
677
|
+
const imported = result.import && result.import !== result.path ? `, imported from "${result.import}"` : "";
|
|
678
|
+
const lines = [`${result.component}, from ${result.path}${imported}:`];
|
|
679
|
+
for (const prop of result.props) {
|
|
680
|
+
const options = prop.options ? `: ${prop.options.map((option) => JSON.stringify(option)).join(", ")}` : "";
|
|
681
|
+
const fallback = prop.default === void 0 ? "" : `, default ${JSON.stringify(prop.default)}`;
|
|
682
|
+
lines.push(` ${prop.name} (${prop.kind}${prop.required ? ", required" : ""})${options}${fallback}`);
|
|
683
|
+
}
|
|
684
|
+
if (result.props.length === 0) lines.push(" No props of its own. --all lists the attributes it passes on.");
|
|
685
|
+
return lines.join("\n");
|
|
686
|
+
}
|
|
687
|
+
/** Frames as state and mode ids, naming a mode once when they share it: "default, error in desktop-light". */
|
|
688
|
+
function frameList(frames) {
|
|
689
|
+
const modes = [...new Set(frames.map((frame) => frame.mode))];
|
|
690
|
+
if (modes.length === 1) return `${frames.map((frame) => frame.state).join(", ")} in ${modes[0]}`;
|
|
691
|
+
return frames.map((frame) => `${frame.state} in ${frame.mode}`).join(", ");
|
|
692
|
+
}
|
|
693
|
+
/** A readable account of what a project's code wrote to the console, oldest first, with where each came from. */
|
|
694
|
+
function describeConsole(result) {
|
|
695
|
+
const { tab, entries, total, truncated } = result;
|
|
696
|
+
if (entries.length === 0) return `${tab.name}'s code hasn't written anything to the console.`;
|
|
697
|
+
const count = total === 1 ? "1 message" : `${total} messages`;
|
|
698
|
+
const shown = truncated ? `, the newest ${entries.length} shown` : "";
|
|
699
|
+
const lines = [`${count} from ${tab.name}'s code${shown}, oldest first:`];
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
const time = new Date(entry.last).toLocaleTimeString([], { hour12: false });
|
|
702
|
+
const repeats = entry.count > 1 ? ` (${entry.count} times)` : "";
|
|
703
|
+
const [first = "", ...rest] = entry.text.split("\n");
|
|
704
|
+
lines.push("", `${time} ${entry.level}${repeats}: ${first}`, ...rest.map((line) => ` ${line}`));
|
|
705
|
+
const offscreen = entry.where === "offscreen" ? ", in an offscreen render" : "";
|
|
706
|
+
lines.push(` at ${scriptPath(entry.source)}:${entry.line}${offscreen}`);
|
|
707
|
+
}
|
|
708
|
+
return lines.join("\n");
|
|
709
|
+
}
|
|
710
|
+
/** Compares two names alphabetically, with `name` before every other. */
|
|
711
|
+
function first(name, a, b) {
|
|
712
|
+
if (a === name) return -1;
|
|
713
|
+
if (b === name) return 1;
|
|
714
|
+
return a.localeCompare(b);
|
|
715
|
+
}
|
|
716
|
+
/** A script's path on its server, without the cache-busting query Vite adds. */
|
|
717
|
+
function scriptPath(url) {
|
|
718
|
+
try {
|
|
719
|
+
return new URL(url).pathname;
|
|
720
|
+
} catch {
|
|
721
|
+
return url || "an unknown script";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
export { errorDetails as _, describeExec as a, describeProps as c, registry as d, setCaller as f, errorCodes as g, closest as h, describeConsole as i, describeStatus as l, CodedError as m, describeAccessibility as n, describeModes as o, mcpCallerName as p, describeComponents as r, describeProblems as s, call as t, describeTokens as u, readErrorDetails as v, unknownReference as y };
|