@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.
Files changed (38) hide show
  1. package/dist/adapters.d.ts +1 -1
  2. package/dist/adapters.js +1 -0
  3. package/dist/browser-wake.d.ts +22 -0
  4. package/dist/browser-wake.js +241 -0
  5. package/dist/cli-main.js +140 -15
  6. package/dist/clipboard-payload.d.ts +2 -0
  7. package/dist/clipboard-payload.js +89 -0
  8. package/dist/design-commands/context.d.ts +5 -0
  9. package/dist/design-commands/copy-command.d.ts +3 -0
  10. package/dist/design-commands/copy-command.js +101 -0
  11. package/dist/design-commands/release-command.js +6 -1
  12. package/dist/design-releases/copy-evidence.d.ts +33 -0
  13. package/dist/design-releases/copy-evidence.js +278 -0
  14. package/dist/design-releases/copy-plan.d.ts +49 -0
  15. package/dist/design-releases/copy-plan.js +55 -0
  16. package/dist/design-releases/copy-snippets.d.ts +11 -0
  17. package/dist/design-releases/copy-snippets.js +293 -0
  18. package/dist/design-releases/copy.d.ts +28 -0
  19. package/dist/design-releases/copy.js +466 -0
  20. package/dist/design-releases/index.d.ts +1 -0
  21. package/dist/design-releases/index.js +1 -0
  22. package/dist/errors.js +1 -1
  23. package/dist/instance/connection.d.ts +2 -0
  24. package/dist/instance/connection.js +15 -3
  25. package/dist/kit-source.js +1 -1
  26. package/dist/layout-policy.js +21 -11
  27. package/dist/layout-settling.d.ts +1 -0
  28. package/dist/layout-settling.js +51 -17
  29. package/dist/layout.d.ts +7 -1
  30. package/dist/layout.js +19 -3
  31. package/dist/mcp-server.js +99 -3
  32. package/dist/native-editor-clipboard.d.ts +17 -0
  33. package/dist/native-editor-clipboard.js +470 -0
  34. package/dist/plugin-health.d.ts +13 -0
  35. package/dist/plugin-health.js +39 -0
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +1 -1
@@ -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
@@ -38,6 +38,7 @@ export const reservedCommands = [
38
38
  "open",
39
39
  "status",
40
40
  "doctor",
41
+ "wake",
41
42
  "backup",
42
43
  "restore",
43
44
  "baselines",
@@ -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; default auto)
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
- "layout",
522
+ ...layoutFlags,
503
523
  ],
504
524
  shots: [
505
525
  "page",
506
526
  "prefix",
507
527
  "out",
508
528
  "scale",
509
- "layout",
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
- "layout",
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 (command === "doctor" &&
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("doctor --timeout-ms must be an integer from 1000 to 30000");
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
- strategy: values.layout,
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 < (args[0] === "doctor" ? 1000 : 1) ||
2400
- milliseconds > (args[0] === "doctor" ? 30000 : 1_800_000) ||
2524
+ milliseconds < (boundedDiagnosis ? 1000 : 1) ||
2525
+ milliseconds > (boundedDiagnosis ? 30000 : 1_800_000) ||
2401
2526
  args.indexOf("--timeout-ms", timeoutAt + 1) !== -1;
2402
- if (args[0] !== "doctor") {
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(args[0] === "doctor"
2430
- ? "doctor --timeout-ms must be one integer from 1000 to 30000"
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,2 @@
1
+ /** A bounded identity check, evaluated inside the browser; clipboard text never leaves that page. */
2
+ export declare function nativeClipboardPayloadMatches(text: string, fileId: string, frameIds: string[]): boolean;
@@ -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
+ }