@penpair/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +1 -0
- package/dist/browser-wake.d.ts +22 -0
- package/dist/browser-wake.js +241 -0
- package/dist/cli-main.js +140 -15
- package/dist/clipboard-payload.d.ts +2 -0
- package/dist/clipboard-payload.js +89 -0
- package/dist/design-commands/context.d.ts +5 -0
- package/dist/design-commands/copy-command.d.ts +3 -0
- package/dist/design-commands/copy-command.js +101 -0
- package/dist/design-commands/release-command.js +6 -1
- package/dist/design-releases/copy-evidence.d.ts +33 -0
- package/dist/design-releases/copy-evidence.js +278 -0
- package/dist/design-releases/copy-plan.d.ts +49 -0
- package/dist/design-releases/copy-plan.js +55 -0
- package/dist/design-releases/copy-snippets.d.ts +11 -0
- package/dist/design-releases/copy-snippets.js +293 -0
- package/dist/design-releases/copy.d.ts +28 -0
- package/dist/design-releases/copy.js +466 -0
- package/dist/design-releases/index.d.ts +1 -0
- package/dist/design-releases/index.js +1 -0
- package/dist/errors.js +1 -1
- package/dist/instance/connection.d.ts +2 -0
- package/dist/instance/connection.js +15 -3
- package/dist/kit-source.js +1 -1
- package/dist/layout-policy.js +21 -11
- package/dist/layout-settling.d.ts +1 -0
- package/dist/layout-settling.js +51 -17
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +19 -3
- package/dist/mcp-server.js +99 -3
- package/dist/native-editor-clipboard.d.ts +17 -0
- package/dist/native-editor-clipboard.js +470 -0
- package/dist/plugin-health.d.ts +13 -0
- package/dist/plugin-health.js +39 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface Adapter {
|
|
|
51
51
|
/** Declares an adapter. Returns the definition unchanged, so a module can `export default defineAdapter({...})` with types checked. */
|
|
52
52
|
export declare const defineAdapter: <T extends Adapter>(adapter: T) => T;
|
|
53
53
|
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
54
|
-
export declare const reservedCommands: readonly ["journey-map", "integration-map", "design-release", "formats", "snapshot", "approve", "mcp", "spec", "inspect", "probe", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "snippets", "run", "run-file", "kit", "recipes", "skills", "init", "setup", "connect", "up", "down", "open", "status", "doctor", "backup", "restore", "baselines", "config", "runs", "onboard", "help"];
|
|
54
|
+
export declare const reservedCommands: readonly ["journey-map", "integration-map", "design-release", "formats", "snapshot", "approve", "mcp", "spec", "inspect", "probe", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "snippets", "run", "run-file", "kit", "recipes", "skills", "init", "setup", "connect", "up", "down", "open", "status", "doctor", "wake", "backup", "restore", "baselines", "config", "runs", "onboard", "help"];
|
|
55
55
|
export interface LoadedAdapters {
|
|
56
56
|
adapters: Adapter[];
|
|
57
57
|
problems: string[];
|
package/dist/adapters.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BrowserTargetOptions {
|
|
2
|
+
origin: string;
|
|
3
|
+
fileId: string;
|
|
4
|
+
port: number;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
allowLoopbackAlias?: boolean;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export interface BrowserWakeResult {
|
|
11
|
+
activated: true;
|
|
12
|
+
targetId: string;
|
|
13
|
+
keepAlive: "none";
|
|
14
|
+
}
|
|
15
|
+
export interface BrowserInspectionResult {
|
|
16
|
+
targetId: string;
|
|
17
|
+
keepAlive: "unknown";
|
|
18
|
+
}
|
|
19
|
+
/** Activate only the existing page for the pinned file; never reload or evaluate page code. */
|
|
20
|
+
export declare function wakeBrowserTarget(options: BrowserTargetOptions): Promise<BrowserWakeResult>;
|
|
21
|
+
/** Read debugging metadata only; a listed tab does not establish plugin readiness or keep-alive. */
|
|
22
|
+
export declare function inspectBrowserTarget(options: BrowserTargetOptions): Promise<BrowserInspectionResult>;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const maxTargetBytes = 1024 * 1024;
|
|
2
|
+
const maxTargetCount = 1000;
|
|
3
|
+
/** Activate only the existing page for the pinned file; never reload or evaluate page code. */
|
|
4
|
+
export async function wakeBrowserTarget(options) {
|
|
5
|
+
return withBrowserDeadline(options, async (request) => {
|
|
6
|
+
const targetId = await findBrowserTarget(request);
|
|
7
|
+
const response = await browserRequest(request, `/json/activate/${encodeURIComponent(targetId)}`);
|
|
8
|
+
void response.body?.cancel().catch(() => undefined);
|
|
9
|
+
request.signal.throwIfAborted();
|
|
10
|
+
return {
|
|
11
|
+
activated: true,
|
|
12
|
+
targetId,
|
|
13
|
+
keepAlive: "none",
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Read debugging metadata only; a listed tab does not establish plugin readiness or keep-alive. */
|
|
18
|
+
export async function inspectBrowserTarget(options) {
|
|
19
|
+
return withBrowserDeadline(options, async (request) => ({
|
|
20
|
+
targetId: await findBrowserTarget(request),
|
|
21
|
+
keepAlive: "unknown",
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
async function withBrowserDeadline(options, work) {
|
|
25
|
+
const origin = validateBrowserOptions(options);
|
|
26
|
+
if (options.signal?.aborted) {
|
|
27
|
+
throw new Error("Browser debugging cancelled");
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
const externalAbort = () => controller.abort();
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
controller.abort();
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
options.signal?.addEventListener("abort", externalAbort, {
|
|
38
|
+
once: true,
|
|
39
|
+
});
|
|
40
|
+
let abort = () => undefined;
|
|
41
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
42
|
+
abort = () => reject(new Error(timedOut ? "Browser debugging deadline exceeded" : "Browser debugging cancelled"));
|
|
43
|
+
controller.signal.addEventListener("abort", abort, {
|
|
44
|
+
once: true,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
if (options.signal?.aborted) {
|
|
49
|
+
controller.abort();
|
|
50
|
+
}
|
|
51
|
+
controller.signal.throwIfAborted();
|
|
52
|
+
return await Promise.race([
|
|
53
|
+
interrupted,
|
|
54
|
+
work({
|
|
55
|
+
origin,
|
|
56
|
+
fileId: options.fileId,
|
|
57
|
+
allowLoopbackAlias: options.allowLoopbackAlias === true,
|
|
58
|
+
endpoint: `http://127.0.0.1:${options.port}`,
|
|
59
|
+
fetch: options.fetch ?? fetch,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (controller.signal.aborted) {
|
|
66
|
+
throw new Error(timedOut ? "Browser debugging deadline exceeded" : "Browser debugging cancelled");
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
73
|
+
controller.signal.removeEventListener("abort", abort);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validateBrowserOptions(options) {
|
|
77
|
+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
|
|
78
|
+
throw new Error("Browser debugging port must be an integer from 1 to 65535");
|
|
79
|
+
}
|
|
80
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
81
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 30000) {
|
|
82
|
+
throw new Error("Browser debugging timeout must be an integer from 1000 to 30000 ms");
|
|
83
|
+
}
|
|
84
|
+
if (typeof options.fileId !== "string" ||
|
|
85
|
+
!options.fileId.trim() ||
|
|
86
|
+
options.fileId.length > 256 ||
|
|
87
|
+
/[\u0000-\u0020\u007f]/.test(options.fileId)) {
|
|
88
|
+
throw new Error("Browser activation requires a pinned file ID");
|
|
89
|
+
}
|
|
90
|
+
let origin;
|
|
91
|
+
try {
|
|
92
|
+
origin = new URL(options.origin);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error("Browser activation requires a valid Penpot origin");
|
|
96
|
+
}
|
|
97
|
+
if (![
|
|
98
|
+
"http:",
|
|
99
|
+
"https:",
|
|
100
|
+
].includes(origin.protocol) ||
|
|
101
|
+
origin.username ||
|
|
102
|
+
origin.password ||
|
|
103
|
+
origin.pathname !== "/" ||
|
|
104
|
+
origin.search ||
|
|
105
|
+
origin.hash) {
|
|
106
|
+
throw new Error("Browser activation requires a Penpot origin without credentials, path or query");
|
|
107
|
+
}
|
|
108
|
+
if (options.allowLoopbackAlias && !isHttpLoopback(origin)) {
|
|
109
|
+
throw new Error("Browser loopback aliases require an HTTP localhost or 127.0.0.1 origin");
|
|
110
|
+
}
|
|
111
|
+
return origin.origin;
|
|
112
|
+
}
|
|
113
|
+
async function browserRequest(request, path) {
|
|
114
|
+
request.signal.throwIfAborted();
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await request.fetch(`${request.endpoint}${path}`, {
|
|
118
|
+
method: "GET",
|
|
119
|
+
redirect: "error",
|
|
120
|
+
signal: request.signal,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error("Cannot reach the local browser debugging endpoint; start a dedicated browser with a loopback debugging port");
|
|
125
|
+
}
|
|
126
|
+
request.signal.throwIfAborted();
|
|
127
|
+
if (!response.ok || response.redirected) {
|
|
128
|
+
void response.body?.cancel().catch(() => undefined);
|
|
129
|
+
throw new Error("The local browser debugging endpoint refused the request");
|
|
130
|
+
}
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
async function findBrowserTarget(request) {
|
|
134
|
+
const response = await browserRequest(request, "/json/list");
|
|
135
|
+
const text = await readTargetList(response);
|
|
136
|
+
request.signal.throwIfAborted();
|
|
137
|
+
let targets;
|
|
138
|
+
try {
|
|
139
|
+
targets = JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error("Invalid browser debugging target list");
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(targets) ||
|
|
145
|
+
targets.length > maxTargetCount ||
|
|
146
|
+
!targets.every(isBrowserTarget)) {
|
|
147
|
+
throw new Error("Invalid browser debugging target list");
|
|
148
|
+
}
|
|
149
|
+
const matching = targets.filter((target) => target.type === "page" && matchesFile(target.url, request));
|
|
150
|
+
if (matching.length !== 1) {
|
|
151
|
+
throw new Error(matching.length === 0
|
|
152
|
+
? "No browser page matches the project's Penpot origin and pinned file ID"
|
|
153
|
+
: "Multiple browser pages match the pinned file; close duplicate tabs before waking");
|
|
154
|
+
}
|
|
155
|
+
const targetId = matching[0].id;
|
|
156
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(targetId)) {
|
|
157
|
+
throw new Error("Invalid browser debugging target ID");
|
|
158
|
+
}
|
|
159
|
+
return targetId;
|
|
160
|
+
}
|
|
161
|
+
function isBrowserTarget(value) {
|
|
162
|
+
if (!value || typeof value !== "object") {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const target = value;
|
|
166
|
+
return (typeof target.id === "string" &&
|
|
167
|
+
typeof target.type === "string" &&
|
|
168
|
+
typeof target.url === "string");
|
|
169
|
+
}
|
|
170
|
+
function isHttpLoopback(url) {
|
|
171
|
+
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
172
|
+
}
|
|
173
|
+
function matchesOrigin(target, request) {
|
|
174
|
+
if (target.origin === request.origin) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (!request.allowLoopbackAlias || !isHttpLoopback(target)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const expected = new URL(request.origin);
|
|
181
|
+
return isHttpLoopback(expected) && target.port === expected.port;
|
|
182
|
+
}
|
|
183
|
+
function matchesFile(targetUrl, request) {
|
|
184
|
+
let url;
|
|
185
|
+
try {
|
|
186
|
+
url = new URL(targetUrl);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (!matchesOrigin(url, request) || url.username || url.password) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const hashRoute = url.hash.slice(1);
|
|
195
|
+
const hashQuery = hashRoute.indexOf("?");
|
|
196
|
+
const parameters = hashQuery >= 0 && hashRoute.slice(0, hashQuery) === "/workspace"
|
|
197
|
+
? new URLSearchParams(hashRoute.slice(hashQuery + 1))
|
|
198
|
+
: url.pathname === "/workspace" && !url.hash
|
|
199
|
+
? url.searchParams
|
|
200
|
+
: undefined;
|
|
201
|
+
const fileIds = parameters?.getAll("file-id");
|
|
202
|
+
return fileIds?.length === 1 && fileIds[0] === request.fileId;
|
|
203
|
+
}
|
|
204
|
+
async function readTargetList(response) {
|
|
205
|
+
const declaredBytes = Number(response.headers.get("content-length"));
|
|
206
|
+
if (declaredBytes > maxTargetBytes) {
|
|
207
|
+
void response.body?.cancel().catch(() => undefined);
|
|
208
|
+
throw new Error("Browser debugging target list exceeds the size limit");
|
|
209
|
+
}
|
|
210
|
+
if (!response.body) {
|
|
211
|
+
throw new Error("Invalid browser debugging target list");
|
|
212
|
+
}
|
|
213
|
+
const reader = response.body.getReader();
|
|
214
|
+
const chunks = [];
|
|
215
|
+
let bytes = 0;
|
|
216
|
+
try {
|
|
217
|
+
while (true) {
|
|
218
|
+
const next = await reader.read();
|
|
219
|
+
if (next.done) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
bytes += next.value.byteLength;
|
|
223
|
+
if (bytes > maxTargetBytes) {
|
|
224
|
+
throw new Error("Browser debugging target list exceeds the size limit");
|
|
225
|
+
}
|
|
226
|
+
chunks.push(next.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
void reader.cancel().catch(() => undefined);
|
|
231
|
+
if (error instanceof Error &&
|
|
232
|
+
error.message === "Browser debugging target list exceeds the size limit") {
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
throw new Error("Cannot read browser debugging target list");
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
reader.releaseLock();
|
|
239
|
+
}
|
|
240
|
+
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
241
|
+
}
|
package/dist/cli-main.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { auditDesignRules } from "./audit-rules.js";
|
|
2
2
|
import { projectStatus, projectFileUrl, openProject } from "./project-status.js";
|
|
3
|
+
import { wakeBrowserTarget } from "./browser-wake.js";
|
|
4
|
+
import { pluginHealthFromError } from "./plugin-health.js";
|
|
3
5
|
import { artifactSchemas, migrateProjectConfig } from "./formats.js";
|
|
4
6
|
import { importDtcg, exportDtcg } from "./portable-tokens.js";
|
|
5
7
|
import { captureStructuralSnapshot, writeStructuralSnapshot, readStructuralSnapshot, diffStructuralSnapshots, renderStructuralDiff, } from "./structure-snapshot.js";
|
|
@@ -44,7 +46,7 @@ import { askHidden, askLine, openInBrowser } from "./instance/tty.js";
|
|
|
44
46
|
import { validateSkillAgent } from "./skill-hosts.js";
|
|
45
47
|
import { loadAdapters, readRawConfig, reservedCommands, } from "./adapters.js";
|
|
46
48
|
import { requireLocalProfile } from "./local-profile.js";
|
|
47
|
-
import { assertEndpointIsInstance, doctorExternal, LifecycleError, } from "./instance/connection.js";
|
|
49
|
+
import { assertEndpointIsInstance, doctorExternal, doctorEditor, frontendUrl, LifecycleError, } from "./instance/connection.js";
|
|
48
50
|
import { nodeRunner } from "./instance/runner.js";
|
|
49
51
|
import { version } from "./version.js";
|
|
50
52
|
import { guardFile } from "./file-guard.js";
|
|
@@ -59,6 +61,7 @@ Design agreements and release pages:
|
|
|
59
61
|
penpair journey-map check|render|publish|import|accept|approve|context --from FILE
|
|
60
62
|
penpair integration-map check|render|publish|import|accept|approve|context --from FILE
|
|
61
63
|
penpair design-release plan|assemble|verify|finalize|check|context --from FILE
|
|
64
|
+
penpair design-release copy --source-page PAGE --frame ID --release VERSION [--debug-port PORT --apply]
|
|
62
65
|
(publication previews by default; --apply writes Penpot, --write records local acceptance/review)
|
|
63
66
|
Start here (guided, plain language; safe to run again):
|
|
64
67
|
penpair setup asks where your Penpot is (web is the default), sets everything up and checks it
|
|
@@ -71,6 +74,7 @@ Connect / diagnose:
|
|
|
71
74
|
penpair config check [--json] validate configuration offline
|
|
72
75
|
penpair config schema print its JSON Schema
|
|
73
76
|
penpair doctor [--json] [--timeout-ms 5000] origin, transport, fresh editor read and file pin (1000–30000 ms)
|
|
77
|
+
penpair wake --debug-port 9222 [--json] activate the pinned browser tab, then verify the editor; no reload or replay
|
|
74
78
|
penpair status [--adapters] observed project health and optional companion readiness
|
|
75
79
|
penpair open [--print] open the pinned public file URL, or print it
|
|
76
80
|
penpair probe [--integrity] observed APIs and limits
|
|
@@ -84,7 +88,7 @@ Build:
|
|
|
84
88
|
penpair snippets / snippets check list or validate configured trusted scripts
|
|
85
89
|
penpair tokens add-set|set|remove|promote inspect token edits; --apply writes once
|
|
86
90
|
penpair archive --page PAGE --name Archive --keep PREFIX [--apply]
|
|
87
|
-
(run-file/export/shots accept --layout auto|native|geometry
|
|
91
|
+
(run-file/run/archive/token edits/export/shots accept --layout auto|native|geometry, repeatable --layout-root ID, --layout-max-nodes 1..100000)
|
|
88
92
|
(journaled writes accept --resume RUN_ID --apply, --checkpoint and --trace)
|
|
89
93
|
Inspect / spec:
|
|
90
94
|
penpair pages file identity, revision and pages
|
|
@@ -235,9 +239,19 @@ async function main(argv, cli) {
|
|
|
235
239
|
"timeout-ms": {
|
|
236
240
|
type: "string",
|
|
237
241
|
},
|
|
242
|
+
"debug-port": {
|
|
243
|
+
type: "string",
|
|
244
|
+
},
|
|
238
245
|
layout: {
|
|
239
246
|
type: "string",
|
|
240
247
|
},
|
|
248
|
+
"layout-root": {
|
|
249
|
+
type: "string",
|
|
250
|
+
multiple: true,
|
|
251
|
+
},
|
|
252
|
+
"layout-max-nodes": {
|
|
253
|
+
type: "string",
|
|
254
|
+
},
|
|
241
255
|
page: {
|
|
242
256
|
type: "string",
|
|
243
257
|
},
|
|
@@ -432,6 +446,11 @@ async function main(argv, cli) {
|
|
|
432
446
|
"max-total-bytes",
|
|
433
447
|
"capture-timeout-ms",
|
|
434
448
|
];
|
|
449
|
+
const layoutFlags = [
|
|
450
|
+
"layout",
|
|
451
|
+
"layout-root",
|
|
452
|
+
"layout-max-nodes",
|
|
453
|
+
];
|
|
435
454
|
const flags = {
|
|
436
455
|
formats: [
|
|
437
456
|
"out",
|
|
@@ -486,6 +505,7 @@ async function main(argv, cli) {
|
|
|
486
505
|
"theme",
|
|
487
506
|
]
|
|
488
507
|
: [
|
|
508
|
+
...layoutFlags,
|
|
489
509
|
"type",
|
|
490
510
|
"value",
|
|
491
511
|
"description",
|
|
@@ -499,16 +519,17 @@ async function main(argv, cli) {
|
|
|
499
519
|
"page",
|
|
500
520
|
"scale",
|
|
501
521
|
"name",
|
|
502
|
-
|
|
522
|
+
...layoutFlags,
|
|
503
523
|
],
|
|
504
524
|
shots: [
|
|
505
525
|
"page",
|
|
506
526
|
"prefix",
|
|
507
527
|
"out",
|
|
508
528
|
"scale",
|
|
509
|
-
|
|
529
|
+
...layoutFlags,
|
|
510
530
|
],
|
|
511
531
|
archive: [
|
|
532
|
+
...layoutFlags,
|
|
512
533
|
"page",
|
|
513
534
|
"name",
|
|
514
535
|
"keep",
|
|
@@ -527,7 +548,7 @@ async function main(argv, cli) {
|
|
|
527
548
|
"include",
|
|
528
549
|
],
|
|
529
550
|
"run-file": [
|
|
530
|
-
|
|
551
|
+
...layoutFlags,
|
|
531
552
|
"include",
|
|
532
553
|
"inspect",
|
|
533
554
|
"readback",
|
|
@@ -540,6 +561,7 @@ async function main(argv, cli) {
|
|
|
540
561
|
"trace",
|
|
541
562
|
],
|
|
542
563
|
run: [
|
|
564
|
+
...layoutFlags,
|
|
543
565
|
"builtins-only",
|
|
544
566
|
"param",
|
|
545
567
|
"apply",
|
|
@@ -594,6 +616,10 @@ async function main(argv, cli) {
|
|
|
594
616
|
doctor: [
|
|
595
617
|
"timeout-ms",
|
|
596
618
|
],
|
|
619
|
+
wake: [
|
|
620
|
+
"debug-port",
|
|
621
|
+
"timeout-ms",
|
|
622
|
+
],
|
|
597
623
|
up: [],
|
|
598
624
|
down: [],
|
|
599
625
|
open: [
|
|
@@ -710,12 +736,31 @@ async function main(argv, cli) {
|
|
|
710
736
|
].includes(values.layout)) {
|
|
711
737
|
throw new CommandArgumentError("--layout must be auto, native or geometry");
|
|
712
738
|
}
|
|
713
|
-
if (
|
|
739
|
+
if (values["layout-max-nodes"] !== undefined &&
|
|
740
|
+
(!/^\d+$/.test(values["layout-max-nodes"]) ||
|
|
741
|
+
Number(values["layout-max-nodes"]) < 1 ||
|
|
742
|
+
Number(values["layout-max-nodes"]) > 100000)) {
|
|
743
|
+
throw new CommandArgumentError("--layout-max-nodes must be an integer from 1 to 100000");
|
|
744
|
+
}
|
|
745
|
+
if (values["layout-root"] &&
|
|
746
|
+
(values["layout-root"].some((id) => !id.trim()) ||
|
|
747
|
+
new Set(values["layout-root"]).size !== values["layout-root"].length)) {
|
|
748
|
+
throw new CommandArgumentError("--layout-root must name distinct nonempty shape IDs");
|
|
749
|
+
}
|
|
750
|
+
if ((command === "doctor" || command === "wake") &&
|
|
714
751
|
values["timeout-ms"] !== undefined &&
|
|
715
752
|
(!/^\d+$/.test(values["timeout-ms"]) ||
|
|
716
753
|
Number(values["timeout-ms"]) < 1000 ||
|
|
717
754
|
Number(values["timeout-ms"]) > 30000)) {
|
|
718
|
-
throw new CommandArgumentError(
|
|
755
|
+
throw new CommandArgumentError(`${command} --timeout-ms must be an integer from 1000 to 30000`);
|
|
756
|
+
}
|
|
757
|
+
if (command === "wake" &&
|
|
758
|
+
(rest.length ||
|
|
759
|
+
!values["debug-port"] ||
|
|
760
|
+
!/^\d+$/.test(values["debug-port"]) ||
|
|
761
|
+
Number(values["debug-port"]) < 1 ||
|
|
762
|
+
Number(values["debug-port"]) > 65535)) {
|
|
763
|
+
throw new CommandArgumentError("wake requires --debug-port with an integer from 1 to 65535");
|
|
719
764
|
}
|
|
720
765
|
let tokenEdit;
|
|
721
766
|
if (command === "tokens" &&
|
|
@@ -1358,7 +1403,11 @@ async function main(argv, cli) {
|
|
|
1358
1403
|
{
|
|
1359
1404
|
check: "editor and plugin",
|
|
1360
1405
|
ok: false,
|
|
1361
|
-
detail: "No resolved project credential/connection; editor identity was not observed",
|
|
1406
|
+
detail: "No resolved project credential/connection; editor identity was not observed; heartbeat age unavailable; keep-alive unknown",
|
|
1407
|
+
evidence: {
|
|
1408
|
+
heartbeat: "unknown",
|
|
1409
|
+
pluginHealth: pluginHealthFromError(undefined),
|
|
1410
|
+
},
|
|
1362
1411
|
remedy: "Configure the private credential source, connect the intended editor, then rerun doctor.",
|
|
1363
1412
|
},
|
|
1364
1413
|
]
|
|
@@ -1392,6 +1441,57 @@ async function main(argv, cli) {
|
|
|
1392
1441
|
}
|
|
1393
1442
|
return ok ? 0 : 1;
|
|
1394
1443
|
}
|
|
1444
|
+
if (command === "wake") {
|
|
1445
|
+
const config = await loadCommandConfig(root);
|
|
1446
|
+
if (!config.file?.id) {
|
|
1447
|
+
throw new CommandArgumentError("Pin the owner-confirmed file before waking its browser tab");
|
|
1448
|
+
}
|
|
1449
|
+
const mcp = await mcpProbeFor();
|
|
1450
|
+
if (!mcp) {
|
|
1451
|
+
throw new Error("Configure the project's MCP connection before waking and verifying its tab");
|
|
1452
|
+
}
|
|
1453
|
+
assertEndpointIsInstance({
|
|
1454
|
+
origin: mcp.origin,
|
|
1455
|
+
source: "project connection",
|
|
1456
|
+
}, config);
|
|
1457
|
+
const timeoutMs = Number(values["timeout-ms"] ?? 5000);
|
|
1458
|
+
const started = Date.now();
|
|
1459
|
+
const activation = await wakeBrowserTarget({
|
|
1460
|
+
origin: isExternal(config) ? config.profile.origin : frontendUrl(config),
|
|
1461
|
+
allowLoopbackAlias: !isExternal(config),
|
|
1462
|
+
fileId: config.file.id,
|
|
1463
|
+
port: Number(values["debug-port"]),
|
|
1464
|
+
timeoutMs,
|
|
1465
|
+
signal: commandSignal(),
|
|
1466
|
+
});
|
|
1467
|
+
const remaining = timeoutMs - (Date.now() - started);
|
|
1468
|
+
const checks = remaining > 0
|
|
1469
|
+
? await doctorEditor(config, {
|
|
1470
|
+
mcp,
|
|
1471
|
+
timeoutMs: remaining,
|
|
1472
|
+
})
|
|
1473
|
+
: [
|
|
1474
|
+
{
|
|
1475
|
+
check: "editor and plugin",
|
|
1476
|
+
ok: false,
|
|
1477
|
+
detail: "Tab activation exhausted the verification deadline",
|
|
1478
|
+
},
|
|
1479
|
+
];
|
|
1480
|
+
const ready = checks.length > 0 && checks.every((check) => check.ok);
|
|
1481
|
+
output.result({
|
|
1482
|
+
...activation,
|
|
1483
|
+
ready,
|
|
1484
|
+
checks,
|
|
1485
|
+
});
|
|
1486
|
+
if (!values.json) {
|
|
1487
|
+
output.log("Browser activation acknowledged; sustained keep-alive: none. No write was replayed.");
|
|
1488
|
+
for (const check of checks) {
|
|
1489
|
+
output.log(`${check.ok ? "ok " : "FAIL"} ${check.check}: ${check.detail}`);
|
|
1490
|
+
}
|
|
1491
|
+
output.log("Reconcile any uncertain run independently before explicitly resuming it.");
|
|
1492
|
+
}
|
|
1493
|
+
return ready ? 0 : 1;
|
|
1494
|
+
}
|
|
1395
1495
|
if (command === "open") {
|
|
1396
1496
|
if (rest.length) {
|
|
1397
1497
|
throw new CommandArgumentError("open accepts only --print for a URL preview");
|
|
@@ -1683,10 +1783,24 @@ async function main(argv, cli) {
|
|
|
1683
1783
|
const client = await guarded();
|
|
1684
1784
|
commandRedactor(endpointRedactor(endpoint));
|
|
1685
1785
|
const options = {
|
|
1686
|
-
...(values.layout
|
|
1786
|
+
...(values.layout || values["layout-root"] || values["layout-max-nodes"]
|
|
1687
1787
|
? {
|
|
1688
1788
|
layout: {
|
|
1689
|
-
|
|
1789
|
+
...(values.layout
|
|
1790
|
+
? {
|
|
1791
|
+
strategy: values.layout,
|
|
1792
|
+
}
|
|
1793
|
+
: {}),
|
|
1794
|
+
...(values["layout-root"]
|
|
1795
|
+
? {
|
|
1796
|
+
shapeIds: values["layout-root"],
|
|
1797
|
+
}
|
|
1798
|
+
: {}),
|
|
1799
|
+
...(values["layout-max-nodes"]
|
|
1800
|
+
? {
|
|
1801
|
+
maxNodes: Number(values["layout-max-nodes"]),
|
|
1802
|
+
}
|
|
1803
|
+
: {}),
|
|
1690
1804
|
},
|
|
1691
1805
|
}
|
|
1692
1806
|
: {}),
|
|
@@ -2183,9 +2297,19 @@ async function runCliInternal(argv, options = {}) {
|
|
|
2183
2297
|
argv[0] === "integration-map" ||
|
|
2184
2298
|
argv[0] === "design-release") {
|
|
2185
2299
|
const { runDesignCommand } = await import("./design-commands/index.js");
|
|
2300
|
+
const copyConfig = argv[0] === "design-release" && argv[1] === "copy"
|
|
2301
|
+
? await loadCommandConfig(root)
|
|
2302
|
+
: undefined;
|
|
2186
2303
|
let designRedactor = (text) => redactEndpoint(text);
|
|
2187
2304
|
return await runDesignCommand(argv[0], argv.slice(1), {
|
|
2188
2305
|
root,
|
|
2306
|
+
copyTarget: copyConfig?.file?.id
|
|
2307
|
+
? {
|
|
2308
|
+
fileId: copyConfig.file.id,
|
|
2309
|
+
origin: isExternal(copyConfig) ? copyConfig.profile.origin : frontendUrl(copyConfig),
|
|
2310
|
+
allowLoopbackAlias: !isExternal(copyConfig),
|
|
2311
|
+
}
|
|
2312
|
+
: undefined,
|
|
2189
2313
|
output,
|
|
2190
2314
|
redact: (text) => designRedactor(text),
|
|
2191
2315
|
async withPenpot(work) {
|
|
@@ -2391,15 +2515,16 @@ async function runCliBound(argv, options = {}) {
|
|
|
2391
2515
|
...argv,
|
|
2392
2516
|
];
|
|
2393
2517
|
const timeoutAt = args.indexOf("--timeout-ms");
|
|
2518
|
+
const boundedDiagnosis = args[0] === "doctor" || args[0] === "wake";
|
|
2394
2519
|
let invalidTimeout = false;
|
|
2395
2520
|
if (timeoutAt !== -1) {
|
|
2396
2521
|
const milliseconds = Number(args[timeoutAt + 1]);
|
|
2397
2522
|
invalidTimeout =
|
|
2398
2523
|
!Number.isInteger(milliseconds) ||
|
|
2399
|
-
milliseconds < (
|
|
2400
|
-
milliseconds > (
|
|
2524
|
+
milliseconds < (boundedDiagnosis ? 1000 : 1) ||
|
|
2525
|
+
milliseconds > (boundedDiagnosis ? 30000 : 1_800_000) ||
|
|
2401
2526
|
args.indexOf("--timeout-ms", timeoutAt + 1) !== -1;
|
|
2402
|
-
if (
|
|
2527
|
+
if (!boundedDiagnosis) {
|
|
2403
2528
|
args.splice(timeoutAt, 2);
|
|
2404
2529
|
}
|
|
2405
2530
|
if (!invalidTimeout) {
|
|
@@ -2426,8 +2551,8 @@ async function runCliBound(argv, options = {}) {
|
|
|
2426
2551
|
try {
|
|
2427
2552
|
if (invalidTimeout) {
|
|
2428
2553
|
const result = await collectCommand(args[0] ?? "help", async () => {
|
|
2429
|
-
throw new Error(
|
|
2430
|
-
?
|
|
2554
|
+
throw new Error(boundedDiagnosis
|
|
2555
|
+
? `${args[0]} --timeout-ms must be one integer from 1000 to 30000`
|
|
2431
2556
|
: "--timeout-ms must be one integer from 1 to 1800000");
|
|
2432
2557
|
});
|
|
2433
2558
|
if (json) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** A bounded identity check, evaluated inside the browser; clipboard text never leaves that page. */
|
|
2
|
+
export function nativeClipboardPayloadMatches(text, fileId, frameIds) {
|
|
3
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
4
|
+
const identity = (value) => {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const plain = value.startsWith("~u") ? value.slice(2) : value;
|
|
9
|
+
return uuid.test(plain) ? plain.toLowerCase() : undefined;
|
|
10
|
+
};
|
|
11
|
+
const field = (record, name) => {
|
|
12
|
+
const ordinary = Object.hasOwn(record, name);
|
|
13
|
+
const encoded = Object.hasOwn(record, `~:${name}`);
|
|
14
|
+
return ordinary === encoded ? undefined : record[ordinary ? name : `~:${name}`];
|
|
15
|
+
};
|
|
16
|
+
const object = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
17
|
+
if (typeof text !== "string" ||
|
|
18
|
+
text.length > 16 * 1024 * 1024 ||
|
|
19
|
+
!uuid.test(fileId) ||
|
|
20
|
+
!Array.isArray(frameIds) ||
|
|
21
|
+
!frameIds.length ||
|
|
22
|
+
frameIds.length > 100 ||
|
|
23
|
+
frameIds.some((id) => typeof id !== "string" || !uuid.test(id)) ||
|
|
24
|
+
new Set(frameIds.map((id) => id.toLowerCase())).size !== frameIds.length) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (!object(raw)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const type = field(raw, "type");
|
|
38
|
+
if ((type !== "copied-shapes" && type !== "~:copied-shapes") ||
|
|
39
|
+
identity(field(raw, "file-id")) !== fileId.toLowerCase()) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
let selected = field(raw, "selected");
|
|
43
|
+
if (object(selected)) {
|
|
44
|
+
const tags = Object.keys(selected);
|
|
45
|
+
if (tags.length !== 1 ||
|
|
46
|
+
![
|
|
47
|
+
"~#set",
|
|
48
|
+
"~#ordered-set",
|
|
49
|
+
].includes(tags[0])) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
selected = selected[tags[0]];
|
|
53
|
+
}
|
|
54
|
+
else if (Array.isArray(selected) &&
|
|
55
|
+
selected.length === 2 &&
|
|
56
|
+
typeof selected[0] === "string" &&
|
|
57
|
+
[
|
|
58
|
+
"~#set",
|
|
59
|
+
"~#ordered-set",
|
|
60
|
+
].includes(selected[0])) {
|
|
61
|
+
selected = selected[1];
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(selected) || selected.length !== frameIds.length) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const selectedIds = selected.map(identity);
|
|
67
|
+
if (selectedIds.some((id) => !id) ||
|
|
68
|
+
new Set(selectedIds).size !== frameIds.length ||
|
|
69
|
+
frameIds.some((id) => !selectedIds.includes(id.toLowerCase()))) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const objects = field(raw, "objects");
|
|
73
|
+
if (!object(objects)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const entries = Object.entries(objects);
|
|
77
|
+
if (entries.length > 20000 || entries.length < frameIds.length) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const objectIds = new Set();
|
|
81
|
+
for (const [key, value] of entries) {
|
|
82
|
+
const id = identity(key);
|
|
83
|
+
if (!id || objectIds.has(id) || !object(value) || identity(field(value, "id")) !== id) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
objectIds.add(id);
|
|
87
|
+
}
|
|
88
|
+
return frameIds.every((id) => objectIds.has(id.toLowerCase()));
|
|
89
|
+
}
|