@penpair/core 0.2.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.
Files changed (101) hide show
  1. package/dist/adapters.js +58 -11
  2. package/dist/approvals.js +143 -29
  3. package/dist/atomic-output.js +9 -4
  4. package/dist/audit-rules.js +345 -79
  5. package/dist/backup-note.js +29 -8
  6. package/dist/baseline-coverage.js +169 -35
  7. package/dist/baselines-config.js +119 -29
  8. package/dist/baselines.js +280 -57
  9. package/dist/cli-arguments.js +18 -6
  10. package/dist/cli-main.js +1621 -392
  11. package/dist/client.d.ts +5 -2
  12. package/dist/client.js +127 -28
  13. package/dist/command-result.js +341 -52
  14. package/dist/coordination.js +91 -29
  15. package/dist/credentials.js +57 -13
  16. package/dist/deep-check.js +56 -12
  17. package/dist/design-spec.js +189 -49
  18. package/dist/endpoint.js +102 -34
  19. package/dist/export.js +86 -18
  20. package/dist/file-archive.js +33 -7
  21. package/dist/file-guard.js +29 -11
  22. package/dist/formats.js +215 -14
  23. package/dist/index.d.ts +8 -7
  24. package/dist/index.js +8 -7
  25. package/dist/inspection.d.ts +1 -1
  26. package/dist/inspection.js +378 -65
  27. package/dist/instance/config-schema.js +400 -41
  28. package/dist/instance/config.js +101 -30
  29. package/dist/instance/connect.js +151 -44
  30. package/dist/instance/connection.d.ts +26 -5
  31. package/dist/instance/connection.js +194 -21
  32. package/dist/instance/gitignore.js +5 -2
  33. package/dist/instance/init.js +32 -7
  34. package/dist/instance/runner.js +32 -10
  35. package/dist/instance/setup.js +46 -13
  36. package/dist/instance/tty.js +26 -8
  37. package/dist/journal.d.ts +10 -0
  38. package/dist/journal.js +212 -49
  39. package/dist/kit-smoke.js +82 -20
  40. package/dist/kit-source.js +10 -40
  41. package/dist/kit.js +102 -34
  42. package/dist/layout-policy.d.ts +2 -0
  43. package/dist/layout-policy.js +86 -0
  44. package/dist/layout.d.ts +7 -1
  45. package/dist/layout.js +27 -70
  46. package/dist/live-smoke.js +13 -4
  47. package/dist/local-profile.js +2 -1
  48. package/dist/mcp-server.js +360 -47
  49. package/dist/native-api.js +47 -11
  50. package/dist/onboard.js +33 -7
  51. package/dist/operation.d.ts +2 -2
  52. package/dist/operation.js +279 -69
  53. package/dist/page-session.d.ts +23 -0
  54. package/dist/page-session.js +142 -0
  55. package/dist/png.js +18 -3
  56. package/dist/portable-tokens.js +254 -57
  57. package/dist/probe.d.ts +1 -1
  58. package/dist/probe.js +48 -11
  59. package/dist/project-input.js +24 -10
  60. package/dist/project-snippets.js +18 -6
  61. package/dist/project-status.js +133 -24
  62. package/dist/registry.js +217 -60
  63. package/dist/run-file.js +129 -30
  64. package/dist/shots.js +108 -26
  65. package/dist/skill-instructions.js +91 -22
  66. package/dist/skills.js +129 -39
  67. package/dist/snippet.js +27 -7
  68. package/dist/snippets/read.js +8 -2
  69. package/dist/structure-snapshot.d.ts +12 -0
  70. package/dist/structure-snapshot.js +439 -85
  71. package/dist/token-edit.js +219 -64
  72. package/dist/token-projection.d.ts +56 -0
  73. package/dist/token-projection.js +286 -0
  74. package/dist/tokens-diff.js +125 -32
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +1 -1
  78. package/skills/penpair-design/SKILL.md +3 -0
  79. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  80. package/skills/penpair-design/references/project-facts.md +2 -0
  81. package/skills/penpair-handoff/SKILL.md +2 -0
  82. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  83. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  84. package/skills/penpair-recipes/SKILL.md +5 -0
  85. package/skills/penpair-setup/SKILL.md +3 -1
  86. package/dist/cli.d.ts +0 -2
  87. package/dist/cli.js +0 -3
  88. package/dist/docker/lifecycle.d.ts +0 -96
  89. package/dist/docker/lifecycle.js +0 -273
  90. package/dist/docker/secrets.d.ts +0 -11
  91. package/dist/docker/secrets.js +0 -38
  92. package/dist/docker/template.d.ts +0 -8
  93. package/dist/docker/template.js +0 -158
  94. package/dist/docker-profile.d.ts +0 -114
  95. package/dist/docker-profile.js +0 -18
  96. package/dist/instance/lifecycle.d.ts +0 -129
  97. package/dist/instance/lifecycle.js +0 -336
  98. package/dist/instance/secrets.d.ts +0 -11
  99. package/dist/instance/secrets.js +0 -38
  100. package/dist/instance/template.d.ts +0 -8
  101. package/dist/instance/template.js +0 -158
package/dist/cli-main.js CHANGED
@@ -2,23 +2,23 @@ import { auditDesignRules } from "./audit-rules.js";
2
2
  import { projectStatus, projectFileUrl, openProject } from "./project-status.js";
3
3
  import { artifactSchemas, migrateProjectConfig } from "./formats.js";
4
4
  import { importDtcg, exportDtcg } from "./portable-tokens.js";
5
- import { captureStructuralSnapshot, writeStructuralSnapshot, readStructuralSnapshot, diffStructuralSnapshots, renderStructuralDiff } from "./structure-snapshot.js";
5
+ import { captureStructuralSnapshot, writeStructuralSnapshot, readStructuralSnapshot, diffStructuralSnapshots, renderStructuralDiff, } from "./structure-snapshot.js";
6
6
  import { recordApproval, approvalFingerprint, artifactSha } from "./approvals.js";
7
7
  import { AsyncLocalStorage } from "node:async_hooks";
8
- import { buildDesignSpec, renderDesignSpec, checkDesignSpec, readDesignJson, validateComponentMap, emptyComponentMap, writeDesignSpec } from "./design-spec.js";
8
+ import { buildDesignSpec, renderDesignSpec, checkDesignSpec, readDesignJson, validateComponentMap, emptyComponentMap, writeDesignSpec, } from "./design-spec.js";
9
9
  import { readBaselineCoverage } from "./baseline-coverage.js";
10
10
  import { runKitSmoke } from "./kit-smoke.js";
11
11
  import { checkKits, loadKits } from "./kit.js";
12
12
  import { prepareFileScript, runFileScript } from "./run-file.js";
13
13
  import { tokenEditOperation, validateTokenEdit } from "./token-edit.js";
14
- import { withCommandSignal, commandSignal, commandOutput as output, commandFailure, commandRedactor, collectCommand, CommandInterruptedError } from "./command-result.js";
14
+ import { withCommandSignal, commandSignal, commandOutput as output, commandFailure, commandRedactor, collectCommand, CommandInterruptedError, } from "./command-result.js";
15
15
  import { readFile } from "node:fs/promises";
16
16
  import path from "node:path";
17
17
  import { CommandArgumentError, parseCommandArgs } from "./cli-arguments.js";
18
18
  import { connect, executeCode } from "./client.js";
19
19
  import { endpointRedactor, redactEndpoint, resolveEndpoint } from "./endpoint.js";
20
20
  import { PenpairError } from "./errors.js";
21
- import { captureInspection, inspectChunk, validateCaptureBudget, validateInspectionQuery } from "./inspection.js";
21
+ import { captureInspection, inspectChunk, validateCaptureBudget, validateInspectionQuery, } from "./inspection.js";
22
22
  import { resolveProjectOutput } from "./project-input.js";
23
23
  import { mkdir } from "node:fs/promises";
24
24
  import { captureShots, exportShapeByName, findShapesByName } from "./shots.js";
@@ -33,15 +33,15 @@ import { builtInRegistry, coerceParams, RegistryError } from "./registry.js";
33
33
  import { runSnippet } from "./snippet.js";
34
34
  import { audit, pages, tree } from "./snippets/read.js";
35
35
  import { diffEffective, effectiveValues, parseTokenDump } from "./tokens-diff.js";
36
- import { ConfigError, isExternal, loadConfig, loadOptionalConfig, requireLocal } from "./instance/config.js";
36
+ import { ConfigError, isExternal, loadConfig, loadOptionalConfig, requireLocal, } from "./instance/config.js";
37
37
  import { projectConfigSchema } from "./instance/config-schema.js";
38
38
  import { init } from "./instance/init.js";
39
39
  import { ConnectError, connectProject, keyFileCheck } from "./instance/connect.js";
40
40
  import { hostedOrigin, setup } from "./instance/setup.js";
41
41
  import { askHidden, askLine, openInBrowser } from "./instance/tty.js";
42
- import { loadAdapters, readRawConfig, reservedCommands } from "./adapters.js";
42
+ import { loadAdapters, readRawConfig, reservedCommands, } from "./adapters.js";
43
43
  import { requireLocalProfile } from "./local-profile.js";
44
- import { assertEndpointIsInstance, doctorExternal, LifecycleError } from "./instance/connection.js";
44
+ import { assertEndpointIsInstance, doctorExternal, LifecycleError, } from "./instance/connection.js";
45
45
  import { nodeRunner } from "./instance/runner.js";
46
46
  import { version } from "./version.js";
47
47
  import { guardFile } from "./file-guard.js";
@@ -59,44 +59,58 @@ Start here (guided, plain language; safe to run again):
59
59
  (a pipe works too: pbpaste | penpair connect)
60
60
  penpair connect --pin-file pin the connected design file ID in project configuration
61
61
  (--any-file explicitly overrides a pinned file; instance guarding still applies)
62
- Read-only:
63
- penpair config check [--json] validate project configuration offline, without loading extensions
64
- penpair config schema print its JSON Schema (also works while fixing invalid config)
65
- (without a project config, built-in inspection requires --standalone; writes require a project)
66
- penpair probe [--integrity] APIs and limits; optionally validate file integrity when supported
67
- penpair probe --deep --write also tries layout, tokens, components, swap, clone and export on a temporary
68
- board it removes again (it touches the open file, so --write is required)
69
- (in a project with an instance every command below acts on that instance only; --any-instance overrides)
70
- penpair pages the file, its revision, pages and their sizes
71
- penpair inspect <shape-id> --page <name> [--all] [--fields geometry,text,tokens] [--cursor <cursor>]
72
- penpair tree --page <name> [--depth 3] [--prefix <text>]
73
- penpair audit --page <name> --prefix <frame name start> [--steps 0,2,4,8] [--ignore <name starts>]
74
- penpair baselines coverage [--json] offline case matrix: approved intent, missing, unsupported and unknown
75
- penpair kit check / recipes [--include @kit] offline compilation and recipe metadata
76
- penpair run-file <script.js> [--include helpers.js] trusted plugin body; read declaration or reviewed inspect/apply/readback
77
- penpair tokens dump typed token sets and themes as JSON
78
- penpair tokens add-set|set|remove|promote inspect token edits; --apply writes once
79
- penpair tokens diff <before.json> <after.json> --theme <name>
80
- penpair export <shapeId> <out.png> [--page <name>] [--scale 1]
81
- penpair export --name <exact-name> --page <name> <out.png>
82
- penpair shots --page <name> [--prefix <prefix>] [--out design/shots]
83
- penpair backup file <new.penpot> try a bounded full-file export, without replacing an existing archive
62
+ Connect / diagnose:
63
+ penpair config check [--json] validate configuration offline
64
+ penpair config schema print its JSON Schema
65
+ penpair doctor [--json] [--timeout-ms 5000] origin, transport, fresh editor read and file pin (1000–30000 ms)
66
+ penpair status [--adapters] observed project health and optional companion readiness
67
+ penpair open [--print] open the pinned public file URL, or print it
68
+ penpair probe [--integrity] observed APIs and limits
69
+ penpair probe --deep --write [--kit] temporary scratch checks; requires an authorized disposable file
70
+ penpair onboard [--json] personal-key colleague setup checklist
71
+ Build:
72
+ penpair kit check [--include @kit] compile helpers offline
73
+ penpair recipes [--include @kit] list available recipes without building
74
+ penpair run-file <script.js> --include @kit --inspect inspect.js --readback readback.js --page PAGE [--apply]
75
+ penpair run <name> [--param key=value] [--apply] configured snippet; inspect first
76
+ penpair snippets / snippets check list or validate configured trusted scripts
77
+ penpair tokens add-set|set|remove|promote inspect token edits; --apply writes once
78
+ penpair archive --page PAGE --name Archive --keep PREFIX [--apply]
79
+ (run-file/export/shots accept --layout auto|native|geometry; default auto)
80
+ (journaled writes accept --resume RUN_ID --apply, --checkpoint and --trace)
81
+ Inspect / spec:
82
+ penpair pages file identity, revision and pages
83
+ penpair tree --page PAGE [--depth 3] [--prefix TEXT] bounded preview
84
+ penpair inspect ID --page PAGE --all --out design/inspection.json
85
+ penpair spec --from design/inspection.json --out design/spec.md
86
+ penpair spec check --from design/inspection.json --out design/spec.md
87
+ penpair spec --page PAGE --frame ID [--map mapping.json]
88
+ penpair audit --page PAGE --prefix PREFIX [--rules] advisory findings and unknowns
89
+ penpair tokens dump complete typed token catalog
90
+ Snapshot / diff:
91
+ penpair snapshot [--write] [--out design/snapshots/current]
92
+ penpair snapshot diff design/snapshots/before --from design/snapshots/current
93
+ penpair tokens diff before.json after.json --theme NAME
94
+ penpair runs [RUN_ID] private recovery evidence, offline
95
+ Capture / approve:
96
+ penpair export ID out.png [--page PAGE] [--scale 1]
97
+ penpair export --name EXACT_NAME --page PAGE out.png
98
+ penpair shots --page PAGE [--prefix PREFIX] [--out design/shots]
99
+ penpair approve --page PAGE --frame ID [--case CASE_ID] preview exact-artifact evidence
100
+ penpair approve --page PAGE --frame ID --write --note "OWNER WORDS" --by OWNER
101
+ penpair baselines [--apply] [--update] capture configured references; approval requires exact owner evidence
102
+ penpair baselines coverage [--json] approved intent, missing cases and unknowns
103
+ penpair backup file new.penpot bounded full-file archive; restore proof remains separate
104
+ Compare:
105
+ penpair reflection plan --map cases.json requires the configured comparison companion
106
+ penpair reflection check --map cases.json --provider provider.mjs
107
+ (install/register the companion, then supply real browser setup/readback; scores require owner review)
108
+ Workflow and limitations: docs/design-workflow.md, docs/adapters.md, docs/recovery.md
84
109
  @@INSTANCE@@Skills (the rules an AI agent follows, shipped with penpair):
85
110
  penpair skills list them
86
- penpair skills install [--to <folder>] [--only <name>] [--force] [--agents AGENTS.md] [--dry-run] copy them into the project and make them discoverable
87
- Snippets (the built-in ones and the project's own, listed under "snippets" in penpair.config.json):
88
- penpair snippets what can be run, with its parameters
89
- penpair snippets check validate the project's snippets without a connection
90
- penpair run <name> [--param key=value ...] [--apply] run one; a snippet that changes things only looks unless --apply
91
- Hand a page's design to a check (Reflection or any image comparison):
92
- penpair baselines [--apply] [--update] export the frames listed under "baselines" in penpair.config.json as approved images,
93
- a manifest and ready-made Reflection cases. Looks first; --apply writes new images;
94
- --update also replaces images whose design changed (approving the new design)
95
- Changes in a Penpot file (inspect first; nothing is written without --apply):
96
- penpair onboard [--json] colleague setup checklist, offline
97
- penpair runs [run-id] list recovery runs or inspect one, offline
98
- (write commands support --resume <run-id> --apply and optional --checkpoint)
99
- penpair archive --page <name> --name <archive board> --keep <prefixes> --parts <prefixes> [--x 9000] [--apply]
111
+ penpair skills install [--to <folder>] [--only <name>] [--force] [--agents AGENTS.md] [--dry-run]
112
+ (built-in help lists configured extensions without importing their trusted code)
113
+ (config-free inspection requires --standalone; writes require a project)
100
114
  penpair --version`;
101
115
  const usageLocalInstance = `An instance on this computer (one Penpot per project, configured in penpair.config.json):
102
116
  penpair init [--instance <name>] [--port 9001] [--gitignore] set up the config, the compose file and the secrets
@@ -111,125 +125,597 @@ const usageConnection = `Connecting to a Penpot (one project, configured in penp
111
125
  penpair init --external <origin> use a Penpot someone else runs (a team server or https://design.penpot.app)
112
126
  penpair doctor [--json] what is up, what is not, and what to do about it (--json for scripts and agents)
113
127
  `;
114
- const usage = (hasLocal, adapters = [], configured = []) => usageHead.replace("@@INSTANCE@@", usageConnection + (hasLocal ? usageLocalInstance : usageNoLocalInstance)).replace("@@SETUP@@", hasLocal ? " penpair setup --docker Penpot on this computer in Docker\n" : "") + adapterUsage(adapters) + (configured.length ? `\nConfigured executable extensions (not loaded for built-in help):\n${configured.map((name) => ` ${JSON.stringify(name)}`).join("\n")}\nUse the extension command to load its trusted project code.\n` : "");
128
+ const usage = (hasLocal, adapters = [], configured = []) => usageHead
129
+ .replace("@@INSTANCE@@", usageConnection + (hasLocal ? usageLocalInstance : usageNoLocalInstance))
130
+ .replace("@@SETUP@@", hasLocal
131
+ ? " penpair setup --docker Penpot on this computer in Docker\n"
132
+ : "") +
133
+ adapterUsage(adapters) +
134
+ (configured.length
135
+ ? `\nConfigured executable extensions (not loaded for built-in help):\n${configured.map((name) => ` ${JSON.stringify(name)}`).join("\n")}\nUse the extension command to load its trusted project code.\n`
136
+ : "");
115
137
  /** The commands that adapters from penpair.config.json add, after the core's own. */
116
- const adapterUsage = (adapters) => adapters.length === 0 ? "" : `\nAdapters (listed under "adapters" in penpair.config.json):\n${adapters.flatMap((a) => a.commands.flatMap((c) => c.usage.map((line) => ` ${line}`))).join("\n")}\n`;
117
- const list = (value) => (value ?? "").split(",").map((v) => v.trim()).filter(Boolean);
138
+ const adapterUsage = (adapters) => adapters.length === 0
139
+ ? ""
140
+ : `\nAdapters (listed under "adapters" in penpair.config.json):\n${adapters.flatMap((a) => a.commands.flatMap((c) => c.usage.map((line) => ` ${line}`))).join("\n")}\n`;
141
+ const list = (value) => (value ?? "")
142
+ .split(",")
143
+ .map((v) => v.trim())
144
+ .filter(Boolean);
118
145
  const commandProjects = new AsyncLocalStorage();
119
- const targetSignature = (config) => JSON.stringify({ instance: config.instance, file: config.file, profile: "profile" in config ? config.profile : null, ports: "ports" in config ? config.ports : null });
146
+ const targetSignature = (config) => JSON.stringify({
147
+ instance: config.instance,
148
+ file: config.file,
149
+ profile: "profile" in config ? config.profile : null,
150
+ ports: "ports" in config ? config.ports : null,
151
+ });
120
152
  async function loadCommandConfig(root) {
121
- const config = await loadConfig(root), pinned = commandProjects.getStore()?.pinnedConfig;
122
- if (pinned && targetSignature(config) !== targetSignature(pinned))
153
+ const config = await loadConfig(root);
154
+ const pinned = commandProjects.getStore()?.pinnedConfig;
155
+ if (pinned && targetSignature(config) !== targetSignature(pinned)) {
123
156
  throw new Error("Pinned project target changed; restart and review the server configuration");
157
+ }
124
158
  return config;
125
159
  }
126
160
  const projectRoot = () => commandProjects.getStore()?.root ?? process.cwd();
127
161
  const projectEndpoint = async (root) => {
128
162
  const config = await loadOptionalConfig(root);
129
- return resolveEndpoint({ envFiles: [path.join(root, ".env.local")], credentialRoot: root, credential: config?.connection?.credential, globalConfig: !commandProjects.getStore()?.projectEndpointOnly });
163
+ return resolveEndpoint({
164
+ envFiles: [
165
+ path.join(root, ".env.local"),
166
+ ],
167
+ credentialRoot: root,
168
+ credential: config?.connection?.credential,
169
+ globalConfig: !commandProjects.getStore()?.projectEndpointOnly,
170
+ });
130
171
  };
131
172
  async function main(argv, cli) {
132
173
  const { values, positionals } = parseCommandArgs({
133
174
  args: argv,
134
175
  allowPositionals: true,
135
176
  options: {
136
- include: { type: "string", multiple: true },
137
- rules: { type: "boolean" }, "token-source": { type: "string" },
138
- adapters: { type: "boolean" }, print: { type: "boolean" },
139
- note: { type: "string" }, by: { type: "string" }, "case": { type: "string" }, "force-reason": { type: "string" },
140
- from: { type: "string" }, map: { type: "string" }, frame: { type: "string" },
141
- inspect: { type: "string" }, readback: { type: "string" },
142
- page: { type: "string" },
143
- out: { type: "string" },
144
- fields: { type: "string" }, cursor: { type: "string" }, all: { type: "boolean" },
145
- "max-nodes": { type: "string" }, "response-bytes": { type: "string" }, "time-budget-ms": { type: "string" }, "document-bytes": { type: "string" },
146
- "max-calls": { type: "string" }, "max-total-bytes": { type: "string" }, "capture-timeout-ms": { type: "string" },
147
- depth: { type: "string" },
148
- limit: { type: "string" },
149
- prefix: { type: "string" },
150
- steps: { type: "string" },
151
- ignore: { type: "string" },
152
- theme: { type: "string" },
153
- type: { type: "string" }, value: { type: "string" }, description: { type: "string" },
154
- scale: { type: "string" },
155
- name: { type: "string" },
156
- keep: { type: "string" },
157
- parts: { type: "string" },
158
- x: { type: "string" },
159
- apply: { type: "boolean" },
160
- instance: { type: "string" },
161
- external: { type: "string" },
162
- port: { type: "string" },
163
- gitignore: { type: "boolean" },
164
- yes: { type: "boolean" },
165
- "any-instance": { type: "boolean" },
166
- "any-file": { type: "boolean" },
167
- "pin-file": { type: "boolean" },
168
- standalone: { type: "boolean" },
169
- "builtins-only": { type: "boolean" },
170
- agents: { type: "string" },
171
- "dry-run": { type: "boolean" },
172
- web: { type: "boolean" },
173
- docker: { type: "boolean" },
174
- replace: { type: "boolean" },
175
- update: { type: "boolean" },
176
- json: { type: "boolean" },
177
- deep: { type: "boolean" },
178
- integrity: { type: "boolean" },
179
- kit: { type: "boolean" },
180
- resume: { type: "string" },
181
- checkpoint: { type: "boolean" },
182
- "force-lock": { type: "boolean" },
183
- force: { type: "boolean" },
184
- to: { type: "string" },
185
- only: { type: "string", multiple: true },
186
- write: { type: "boolean" },
187
- param: { type: "string", multiple: true },
188
- version: { type: "boolean" },
189
- help: { type: "boolean" },
177
+ include: {
178
+ type: "string",
179
+ multiple: true,
180
+ },
181
+ rules: {
182
+ type: "boolean",
183
+ },
184
+ "token-source": {
185
+ type: "string",
186
+ },
187
+ adapters: {
188
+ type: "boolean",
189
+ },
190
+ print: {
191
+ type: "boolean",
192
+ },
193
+ note: {
194
+ type: "string",
195
+ },
196
+ by: {
197
+ type: "string",
198
+ },
199
+ case: {
200
+ type: "string",
201
+ },
202
+ "force-reason": {
203
+ type: "string",
204
+ },
205
+ from: {
206
+ type: "string",
207
+ },
208
+ map: {
209
+ type: "string",
210
+ },
211
+ frame: {
212
+ type: "string",
213
+ },
214
+ inspect: {
215
+ type: "string",
216
+ },
217
+ readback: {
218
+ type: "string",
219
+ },
220
+ "timeout-ms": {
221
+ type: "string",
222
+ },
223
+ layout: {
224
+ type: "string",
225
+ },
226
+ page: {
227
+ type: "string",
228
+ },
229
+ out: {
230
+ type: "string",
231
+ },
232
+ fields: {
233
+ type: "string",
234
+ },
235
+ cursor: {
236
+ type: "string",
237
+ },
238
+ all: {
239
+ type: "boolean",
240
+ },
241
+ "max-nodes": {
242
+ type: "string",
243
+ },
244
+ "response-bytes": {
245
+ type: "string",
246
+ },
247
+ "time-budget-ms": {
248
+ type: "string",
249
+ },
250
+ "document-bytes": {
251
+ type: "string",
252
+ },
253
+ "max-calls": {
254
+ type: "string",
255
+ },
256
+ "max-total-bytes": {
257
+ type: "string",
258
+ },
259
+ "capture-timeout-ms": {
260
+ type: "string",
261
+ },
262
+ depth: {
263
+ type: "string",
264
+ },
265
+ limit: {
266
+ type: "string",
267
+ },
268
+ prefix: {
269
+ type: "string",
270
+ },
271
+ steps: {
272
+ type: "string",
273
+ },
274
+ ignore: {
275
+ type: "string",
276
+ },
277
+ theme: {
278
+ type: "string",
279
+ },
280
+ type: {
281
+ type: "string",
282
+ },
283
+ value: {
284
+ type: "string",
285
+ },
286
+ description: {
287
+ type: "string",
288
+ },
289
+ scale: {
290
+ type: "string",
291
+ },
292
+ name: {
293
+ type: "string",
294
+ },
295
+ keep: {
296
+ type: "string",
297
+ },
298
+ parts: {
299
+ type: "string",
300
+ },
301
+ x: {
302
+ type: "string",
303
+ },
304
+ apply: {
305
+ type: "boolean",
306
+ },
307
+ instance: {
308
+ type: "string",
309
+ },
310
+ external: {
311
+ type: "string",
312
+ },
313
+ port: {
314
+ type: "string",
315
+ },
316
+ gitignore: {
317
+ type: "boolean",
318
+ },
319
+ yes: {
320
+ type: "boolean",
321
+ },
322
+ "any-instance": {
323
+ type: "boolean",
324
+ },
325
+ "any-file": {
326
+ type: "boolean",
327
+ },
328
+ "pin-file": {
329
+ type: "boolean",
330
+ },
331
+ standalone: {
332
+ type: "boolean",
333
+ },
334
+ "builtins-only": {
335
+ type: "boolean",
336
+ },
337
+ agents: {
338
+ type: "string",
339
+ },
340
+ "dry-run": {
341
+ type: "boolean",
342
+ },
343
+ web: {
344
+ type: "boolean",
345
+ },
346
+ docker: {
347
+ type: "boolean",
348
+ },
349
+ replace: {
350
+ type: "boolean",
351
+ },
352
+ update: {
353
+ type: "boolean",
354
+ },
355
+ json: {
356
+ type: "boolean",
357
+ },
358
+ deep: {
359
+ type: "boolean",
360
+ },
361
+ integrity: {
362
+ type: "boolean",
363
+ },
364
+ kit: {
365
+ type: "boolean",
366
+ },
367
+ resume: {
368
+ type: "string",
369
+ },
370
+ checkpoint: {
371
+ type: "boolean",
372
+ },
373
+ "force-lock": {
374
+ type: "boolean",
375
+ },
376
+ trace: {
377
+ type: "boolean",
378
+ },
379
+ force: {
380
+ type: "boolean",
381
+ },
382
+ to: {
383
+ type: "string",
384
+ },
385
+ only: {
386
+ type: "string",
387
+ multiple: true,
388
+ },
389
+ write: {
390
+ type: "boolean",
391
+ },
392
+ param: {
393
+ type: "string",
394
+ multiple: true,
395
+ },
396
+ version: {
397
+ type: "boolean",
398
+ },
399
+ help: {
400
+ type: "boolean",
401
+ },
190
402
  },
191
403
  });
192
404
  const [command, ...rest] = positionals;
193
- const inspectionFlags = ["cursor", "all", "out", "max-nodes", "response-bytes", "time-budget-ms", "document-bytes", "max-calls", "max-total-bytes", "capture-timeout-ms"];
405
+ const inspectionFlags = [
406
+ "cursor",
407
+ "all",
408
+ "out",
409
+ "max-nodes",
410
+ "response-bytes",
411
+ "time-budget-ms",
412
+ "document-bytes",
413
+ "max-calls",
414
+ "max-total-bytes",
415
+ "capture-timeout-ms",
416
+ ];
194
417
  const flags = {
195
- formats: ["out"],
196
- snapshot: ["write", "out", "from"],
197
- approve: ["page", "frame", "note", "by", "case", "write"],
198
- spec: ["from", "map", "out", "page", "frame"],
199
- pages: [], tree: ["page", "depth", "prefix", "limit"], audit: ["page", "prefix", "steps", "ignore", "rules", "from", "token-source"], inspect: ["page", "depth", "fields", ...inspectionFlags], tokens: rest[0] === "dump" ? inspectionFlags : rest[0] === "diff" ? ["theme"] : ["type", "value", "description", "apply", "resume", "checkpoint", "force-lock"], export: ["page", "scale", "name"], shots: ["page", "prefix", "out", "scale"],
200
- archive: ["page", "name", "keep", "parts", "x", "apply", "resume", "checkpoint", "force-lock"],
201
- kit: ["include"], recipes: ["include"],
202
- "run-file": ["include", "inspect", "readback", "page", "param", "apply", "resume", "checkpoint", "force-lock"],
203
- run: ["builtins-only", "param", "apply", "resume", "checkpoint", "force-lock"], baselines: ["apply", "update", "force-reason"], probe: ["deep", "write", "integrity", "kit"],
204
- skills: ["to", "only", "force", "agents", "dry-run"], snippets: ["builtins-only"], init: ["instance", "port", "gitignore", "external"], setup: ["agents", "web", "docker", "external", "instance", "port"],
205
- connect: ["replace", "pin-file", "any-file"], doctor: [], up: [], down: [], open: ["print"], status: ["adapters"], backup: [], restore: ["yes"],
418
+ formats: [
419
+ "out",
420
+ ],
421
+ snapshot: [
422
+ "write",
423
+ "out",
424
+ "from",
425
+ ],
426
+ approve: [
427
+ "page",
428
+ "frame",
429
+ "note",
430
+ "by",
431
+ "case",
432
+ "write",
433
+ ],
434
+ spec: [
435
+ "from",
436
+ "map",
437
+ "out",
438
+ "page",
439
+ "frame",
440
+ ],
441
+ pages: [],
442
+ tree: [
443
+ "page",
444
+ "depth",
445
+ "prefix",
446
+ "limit",
447
+ ],
448
+ audit: [
449
+ "page",
450
+ "prefix",
451
+ "steps",
452
+ "ignore",
453
+ "rules",
454
+ "from",
455
+ "token-source",
456
+ ],
457
+ inspect: [
458
+ "page",
459
+ "depth",
460
+ "fields",
461
+ ...inspectionFlags,
462
+ ],
463
+ tokens: rest[0] === "dump"
464
+ ? inspectionFlags
465
+ : rest[0] === "diff"
466
+ ? [
467
+ "theme",
468
+ ]
469
+ : [
470
+ "type",
471
+ "value",
472
+ "description",
473
+ "apply",
474
+ "resume",
475
+ "checkpoint",
476
+ "force-lock",
477
+ "trace",
478
+ ],
479
+ export: [
480
+ "page",
481
+ "scale",
482
+ "name",
483
+ "layout",
484
+ ],
485
+ shots: [
486
+ "page",
487
+ "prefix",
488
+ "out",
489
+ "scale",
490
+ "layout",
491
+ ],
492
+ archive: [
493
+ "page",
494
+ "name",
495
+ "keep",
496
+ "parts",
497
+ "x",
498
+ "apply",
499
+ "resume",
500
+ "checkpoint",
501
+ "force-lock",
502
+ "trace",
503
+ ],
504
+ kit: [
505
+ "include",
506
+ ],
507
+ recipes: [
508
+ "include",
509
+ ],
510
+ "run-file": [
511
+ "layout",
512
+ "include",
513
+ "inspect",
514
+ "readback",
515
+ "page",
516
+ "param",
517
+ "apply",
518
+ "resume",
519
+ "checkpoint",
520
+ "force-lock",
521
+ "trace",
522
+ ],
523
+ run: [
524
+ "builtins-only",
525
+ "param",
526
+ "apply",
527
+ "resume",
528
+ "checkpoint",
529
+ "force-lock",
530
+ "trace",
531
+ ],
532
+ baselines: [
533
+ "apply",
534
+ "update",
535
+ "force-reason",
536
+ ],
537
+ probe: [
538
+ "deep",
539
+ "write",
540
+ "integrity",
541
+ "kit",
542
+ ],
543
+ skills: [
544
+ "to",
545
+ "only",
546
+ "force",
547
+ "agents",
548
+ "dry-run",
549
+ ],
550
+ snippets: [
551
+ "builtins-only",
552
+ ],
553
+ init: [
554
+ "instance",
555
+ "port",
556
+ "gitignore",
557
+ "external",
558
+ ],
559
+ setup: [
560
+ "agents",
561
+ "web",
562
+ "docker",
563
+ "external",
564
+ "instance",
565
+ "port",
566
+ ],
567
+ connect: [
568
+ "replace",
569
+ "pin-file",
570
+ "any-file",
571
+ ],
572
+ doctor: [
573
+ "timeout-ms",
574
+ ],
575
+ up: [],
576
+ down: [],
577
+ open: [
578
+ "print",
579
+ ],
580
+ status: [
581
+ "adapters",
582
+ ],
583
+ backup: [],
584
+ restore: [
585
+ "yes",
586
+ ],
206
587
  };
207
- const network = ["snapshot", "approve", "spec", "inspect", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "run", "run-file", "baselines", "probe", "backup", "doctor"];
208
- const allowed = new Set(["help", "version", "json", ...(flags[command ?? ""] ?? []), ...(network.includes(command ?? "") ? ["any-instance", "any-file", "standalone"] : [])]);
209
- for (const key of Object.keys(values))
210
- if (!allowed.has(key))
588
+ const network = [
589
+ "snapshot",
590
+ "approve",
591
+ "spec",
592
+ "inspect",
593
+ "pages",
594
+ "tree",
595
+ "audit",
596
+ "tokens",
597
+ "export",
598
+ "shots",
599
+ "archive",
600
+ "run",
601
+ "run-file",
602
+ "baselines",
603
+ "probe",
604
+ "backup",
605
+ "doctor",
606
+ ];
607
+ const allowed = new Set([
608
+ "help",
609
+ "version",
610
+ "json",
611
+ ...(flags[command ?? ""] ?? []),
612
+ ...(network.includes(command ?? "")
613
+ ? [
614
+ "any-instance",
615
+ "any-file",
616
+ "standalone",
617
+ ]
618
+ : []),
619
+ ]);
620
+ for (const key of Object.keys(values)) {
621
+ if (!allowed.has(key)) {
211
622
  throw new Error(`--${key} is not supported by ${command || "this command"}`);
212
- for (const [key, min, max, integer] of [["depth", 0, command === "inspect" ? 10000 : 100, true], ["limit", 1, 1000, true], ["scale", 0.01, 8, false], ["x", -10000000, 10000000, false], ["port", 1024, 65535, true]]) {
623
+ }
624
+ }
625
+ for (const [key, min, max, integer] of [
626
+ [
627
+ "depth",
628
+ 0,
629
+ command === "inspect" ? 10000 : 100,
630
+ true,
631
+ ],
632
+ [
633
+ "limit",
634
+ 1,
635
+ 1000,
636
+ true,
637
+ ],
638
+ [
639
+ "scale",
640
+ 0.01,
641
+ 8,
642
+ false,
643
+ ],
644
+ [
645
+ "x",
646
+ -10000000,
647
+ 10000000,
648
+ false,
649
+ ],
650
+ [
651
+ "port",
652
+ 1024,
653
+ 65535,
654
+ true,
655
+ ],
656
+ ]) {
213
657
  const raw = values[key];
214
- if (raw === undefined)
658
+ if (raw === undefined) {
215
659
  continue;
660
+ }
216
661
  const number = Number(raw);
217
- if (!raw.trim() || !Number.isFinite(number) || number < min || number > max || integer && !Number.isInteger(number))
662
+ if (!raw.trim() ||
663
+ !Number.isFinite(number) ||
664
+ number < min ||
665
+ number > max ||
666
+ (integer && !Number.isInteger(number))) {
218
667
  throw new Error(`--${key} must be ${integer ? "an integer" : "a number"} from ${min} to ${max}`);
668
+ }
219
669
  }
220
- if (command === "export" && (rest.length !== (values.name ? 1 : 2) || values.name && !values.page))
670
+ if (command === "export" &&
671
+ (rest.length !== (values.name ? 1 : 2) || (values.name && !values.page))) {
221
672
  throw new CommandArgumentError("Export requires an ID and output path, or --name with an explicit --page and one output path");
222
- if (command === "shots" && (!values.page || rest.length))
673
+ }
674
+ if (command === "shots" && (!values.page || rest.length)) {
223
675
  throw new CommandArgumentError("Shots requires --page and supports optional --prefix/--out");
676
+ }
677
+ if (values.layout !== undefined &&
678
+ ![
679
+ "auto",
680
+ "native",
681
+ "geometry",
682
+ ].includes(values.layout)) {
683
+ throw new CommandArgumentError("--layout must be auto, native or geometry");
684
+ }
685
+ if (command === "doctor" &&
686
+ values["timeout-ms"] !== undefined &&
687
+ (!/^\d+$/.test(values["timeout-ms"]) ||
688
+ Number(values["timeout-ms"]) < 1000 ||
689
+ Number(values["timeout-ms"]) > 30000)) {
690
+ throw new CommandArgumentError("doctor --timeout-ms must be an integer from 1000 to 30000");
691
+ }
224
692
  let tokenEdit;
225
- if (command === "tokens" && !["dump", "diff"].includes(rest[0] ?? "")) {
693
+ if (command === "tokens" &&
694
+ ![
695
+ "dump",
696
+ "diff",
697
+ ].includes(rest[0] ?? "")) {
226
698
  const [kind, set, name] = rest;
227
- if (kind === "add-set" && rest.length === 2)
228
- tokenEdit = { kind, set: set };
229
- else if (kind === "remove" && rest.length === 3)
230
- tokenEdit = { kind, set: set, name: name };
231
- else if (kind === "promote" && rest.length === 3)
232
- tokenEdit = { kind, set: set, to: name };
699
+ if (kind === "add-set" && rest.length === 2) {
700
+ tokenEdit = {
701
+ kind,
702
+ set: set,
703
+ };
704
+ }
705
+ else if (kind === "remove" && rest.length === 3) {
706
+ tokenEdit = {
707
+ kind,
708
+ set: set,
709
+ name: name,
710
+ };
711
+ }
712
+ else if (kind === "promote" && rest.length === 3) {
713
+ tokenEdit = {
714
+ kind,
715
+ set: set,
716
+ to: name,
717
+ };
718
+ }
233
719
  else if (kind === "set" && rest.length === 3 && values.type && values.value) {
234
720
  let value;
235
721
  try {
@@ -238,36 +724,101 @@ async function main(argv, cli) {
238
724
  catch {
239
725
  throw new CommandArgumentError("--value must be JSON: quote strings or supply a composite object");
240
726
  }
241
- tokenEdit = { kind, set: set, token: { name: name, type: values.type, value: value, description: values.description ?? "" } };
727
+ tokenEdit = {
728
+ kind,
729
+ set: set,
730
+ token: {
731
+ name: name,
732
+ type: values.type,
733
+ value: value,
734
+ description: values.description ?? "",
735
+ },
736
+ };
242
737
  }
243
- else
738
+ else {
244
739
  throw new CommandArgumentError("Usage: tokens add-set SET; tokens set SET NAME --type TYPE --value JSON; tokens remove SET NAME; tokens promote SOURCE DESTINATION");
245
- if (kind !== "set" && [values.type, values.value, values.description].some(v => v !== undefined))
740
+ }
741
+ if (kind !== "set" &&
742
+ [
743
+ values.type,
744
+ values.value,
745
+ values.description,
746
+ ].some((v) => v !== undefined)) {
246
747
  throw new CommandArgumentError("Token value options apply only to tokens set");
748
+ }
247
749
  tokenEdit = validateTokenEdit(tokenEdit);
248
750
  }
249
751
  let inspectionRequest;
250
- if (command === "inspect" || command === "tokens" && rest[0] === "dump") {
251
- if (rest.length !== 1 || command === "inspect" && !values.page)
752
+ if (command === "inspect" || (command === "tokens" && rest[0] === "dump")) {
753
+ if (rest.length !== 1 || (command === "inspect" && !values.page)) {
252
754
  throw new CommandArgumentError("Inspect requires one shape ID and --page; tokens dump accepts no extra positionals");
253
- if (values.cursor && values.all || values.cursor && values.out || command === "inspect" && values.out && !values.all)
755
+ }
756
+ if ((values.cursor && values.all) ||
757
+ (values.cursor && values.out) ||
758
+ (command === "inspect" && values.out && !values.all)) {
254
759
  throw new CommandArgumentError("Use --all for a complete output file; --cursor selects one continuation chunk");
255
- if ((values.cursor || command === "inspect" && !values.all) && [values["max-calls"], values["max-total-bytes"], values["capture-timeout-ms"]].some(value => value !== undefined))
760
+ }
761
+ if ((values.cursor || (command === "inspect" && !values.all)) &&
762
+ [
763
+ values["max-calls"],
764
+ values["max-total-bytes"],
765
+ values["capture-timeout-ms"],
766
+ ].some((value) => value !== undefined)) {
256
767
  throw new CommandArgumentError("Collection budgets require a full capture rather than one cursor chunk");
768
+ }
257
769
  const numeric = (key) => values[key] === undefined ? undefined : Number(values[key]);
258
- const query = { kind: command === "inspect" ? "shape" : "tokens", shapeId: command === "inspect" ? rest[0] : undefined, page: values.page,
259
- fields: values.fields?.split(",").map(field => field.trim()), depth: numeric("depth"), maxNodes: numeric("max-nodes"), responseBytes: numeric("response-bytes"), timeBudgetMs: numeric("time-budget-ms"), documentBytes: numeric("document-bytes"), cursor: values.cursor };
770
+ const query = {
771
+ kind: command === "inspect" ? "shape" : "tokens",
772
+ shapeId: command === "inspect" ? rest[0] : undefined,
773
+ page: values.page,
774
+ fields: values.fields?.split(",").map((field) => field.trim()),
775
+ depth: numeric("depth"),
776
+ maxNodes: numeric("max-nodes"),
777
+ responseBytes: numeric("response-bytes"),
778
+ timeBudgetMs: numeric("time-budget-ms"),
779
+ documentBytes: numeric("document-bytes"),
780
+ cursor: values.cursor,
781
+ };
260
782
  validateInspectionQuery(query);
261
- inspectionRequest = { query, budget: validateCaptureBudget({ maxCalls: numeric("max-calls"), maxTotalBytes: numeric("max-total-bytes"), timeoutMs: numeric("capture-timeout-ms") }) };
783
+ inspectionRequest = {
784
+ query,
785
+ budget: validateCaptureBudget({
786
+ maxCalls: numeric("max-calls"),
787
+ maxTotalBytes: numeric("max-total-bytes"),
788
+ timeoutMs: numeric("capture-timeout-ms"),
789
+ }),
790
+ };
262
791
  }
263
- if (values.kit && (!values.deep || !values.write))
792
+ if (values.kit && (!values.deep || !values.write)) {
264
793
  throw new CommandArgumentError("--kit requires the explicit scratch probe --deep --write");
265
- if (values.steps && values.steps.split(",").some((step) => !step.trim() || !Number.isFinite(Number(step)) || Number(step) < 0))
794
+ }
795
+ if (values.steps &&
796
+ values.steps
797
+ .split(",")
798
+ .some((step) => !step.trim() || !Number.isFinite(Number(step)) || Number(step) < 0)) {
266
799
  throw new Error("--steps requires finite nonnegative numbers");
267
- if (command === "skills" && rest[0] !== "install" && [values.agents, values["dry-run"], values.to, values.only, values.force].some((value) => value !== undefined))
800
+ }
801
+ if (command === "skills" &&
802
+ rest[0] !== "install" &&
803
+ [
804
+ values.agents,
805
+ values["dry-run"],
806
+ values.to,
807
+ values.only,
808
+ values.force,
809
+ ].some((value) => value !== undefined)) {
268
810
  throw new Error("Installation flags require skills install");
269
- if ((values.resume || values.checkpoint || values["force-lock"]) && (!values.apply || !(["archive", "run", "run-file"].includes(command ?? "") || command === "tokens" && tokenEdit)))
811
+ }
812
+ if ((values.resume || values.checkpoint || values["force-lock"] || values.trace) &&
813
+ (!values.apply ||
814
+ !([
815
+ "archive",
816
+ "run",
817
+ "run-file",
818
+ ].includes(command ?? "") ||
819
+ (command === "tokens" && tokenEdit)))) {
270
820
  throw new Error("--resume and --checkpoint require an operation command (archive/run/run-file/tokens) with --apply");
821
+ }
271
822
  if (values.version) {
272
823
  output.log(version);
273
824
  return 0;
@@ -277,12 +828,23 @@ async function main(argv, cli) {
277
828
  return 0;
278
829
  }
279
830
  if (command === "audit" && values.from) {
280
- if (rest.length || values.page || values.prefix || values.steps || values.ignore || values["any-file"] || values["any-instance"] || values.standalone)
831
+ if (rest.length ||
832
+ values.page ||
833
+ values.prefix ||
834
+ values.steps ||
835
+ values.ignore ||
836
+ values["any-file"] ||
837
+ values["any-instance"] ||
838
+ values.standalone) {
281
839
  throw new CommandArgumentError("audit --from INSPECTION [--token-source TOKENS] uses configured advisory rules offline");
840
+ }
282
841
  const config = await loadCommandConfig(projectRoot());
283
- if (!config.audit)
842
+ if (!config.audit) {
284
843
  throw new Error("Configure explicit audit rules first");
285
- const report = auditDesignRules(await readDesignJson(projectRoot(), values.from), config.audit, values["token-source"] ? await readDesignJson(projectRoot(), values["token-source"]) : undefined);
844
+ }
845
+ const report = auditDesignRules((await readDesignJson(projectRoot(), values.from)), config.audit, values["token-source"]
846
+ ? (await readDesignJson(projectRoot(), values["token-source"]))
847
+ : undefined);
286
848
  output.result(report);
287
849
  output.log(JSON.stringify(report, null, 2));
288
850
  return report.complete ? 0 : 3;
@@ -294,13 +856,25 @@ async function main(argv, cli) {
294
856
  output.log(JSON.stringify(artifactSchemas[input], null, 2));
295
857
  return 0;
296
858
  }
297
- if (!["import-tokens", "export-tokens", "migrate-config"].includes(action ?? "") || rest.length !== 2)
859
+ if (![
860
+ "import-tokens",
861
+ "export-tokens",
862
+ "migrate-config",
863
+ ].includes(action ?? "") ||
864
+ rest.length !== 2) {
298
865
  throw new CommandArgumentError("formats schema NAME | import-tokens FILE | export-tokens FILE | migrate-config FILE [--out NEW_FILE]");
866
+ }
299
867
  const raw = await readDesignJson(projectRoot(), input, 8 * 1024 * 1024);
300
- const result = action === "import-tokens" ? importDtcg(raw) : action === "export-tokens" ? exportDtcg(raw) : migrateProjectConfig(raw);
868
+ const result = action === "import-tokens"
869
+ ? importDtcg(raw)
870
+ : action === "export-tokens"
871
+ ? exportDtcg(raw)
872
+ : migrateProjectConfig(raw);
301
873
  if (values.out) {
302
874
  const target = await resolveProjectOutput(projectRoot(), values.out);
303
- await mkdir(path.dirname(target), { recursive: true });
875
+ await mkdir(path.dirname(target), {
876
+ recursive: true,
877
+ });
304
878
  await atomicWriteFile(target, JSON.stringify(result, null, 2) + "\n");
305
879
  }
306
880
  output.result(result);
@@ -308,78 +882,140 @@ async function main(argv, cli) {
308
882
  return 0;
309
883
  }
310
884
  if (command === "snapshot" && rest[0] === "diff") {
311
- if (rest.length !== 2 || !values.from || values.write || values["any-file"] || values["any-instance"] || values.standalone)
885
+ if (rest.length !== 2 ||
886
+ !values.from ||
887
+ values.write ||
888
+ values["any-file"] ||
889
+ values["any-instance"] ||
890
+ values.standalone) {
312
891
  throw new CommandArgumentError("snapshot diff BEFORE_FOLDER --from AFTER_FOLDER [--out FILE]");
892
+ }
313
893
  const diff = diffStructuralSnapshots(await readStructuralSnapshot(projectRoot(), rest[1]), await readStructuralSnapshot(projectRoot(), values.from));
314
- if (values.out)
894
+ if (values.out) {
315
895
  await writeDesignSpec(projectRoot(), values.out, renderStructuralDiff(diff));
896
+ }
316
897
  output.result(diff);
317
- if (!values.json)
898
+ if (!values.json) {
318
899
  output.log(renderStructuralDiff(diff));
900
+ }
319
901
  return !diff.complete ? 3 : diff.totalChanges ? 2 : 0;
320
902
  }
321
- if (command === "snapshot" && (rest.length || values.from))
903
+ if (command === "snapshot" && (rest.length || values.from)) {
322
904
  throw new CommandArgumentError("snapshot [--write] [--out FOLDER], or snapshot diff BEFORE --from AFTER");
905
+ }
323
906
  if (command === "spec" && values.from) {
324
- if (rest.length > 1 || rest.length === 1 && rest[0] !== "check" || values.page || values.frame || values.standalone || values["any-file"] || values["any-instance"])
907
+ if (rest.length > 1 ||
908
+ (rest.length === 1 && rest[0] !== "check") ||
909
+ values.page ||
910
+ values.frame ||
911
+ values.standalone ||
912
+ values["any-file"] ||
913
+ values["any-instance"]) {
325
914
  throw new CommandArgumentError("Offline spec requires --from INSPECTION [--map MAPPING] [--out FILE]; spec check also requires --out");
326
- const inspection = await readDesignJson(projectRoot(), values.from);
327
- const mapping = values.map ? validateComponentMap(await readDesignJson(projectRoot(), values.map, 1024 * 1024)) : emptyComponentMap;
915
+ }
916
+ const inspection = (await readDesignJson(projectRoot(), values.from));
917
+ const mapping = values.map
918
+ ? validateComponentMap(await readDesignJson(projectRoot(), values.map, 1024 * 1024))
919
+ : emptyComponentMap;
328
920
  if (rest[0] === "check") {
329
- if (!values.out)
921
+ if (!values.out) {
330
922
  throw new CommandArgumentError("spec check requires --out and --from");
331
- const file = await import("./project-input.js").then(m => m.resolveProjectInput(projectRoot(), values.out));
923
+ }
924
+ const file = await import("./project-input.js").then((m) => m.resolveProjectInput(projectRoot(), values.out));
332
925
  const report = checkDesignSpec(await readFile(file, "utf8"), inspection, mapping);
333
926
  output.result(report);
334
927
  output.log(JSON.stringify(report));
335
928
  return report.current ? 0 : 2;
336
929
  }
337
- const spec = buildDesignSpec(inspection, mapping), markdown = renderDesignSpec(spec);
338
- if (values.out)
930
+ const spec = buildDesignSpec(inspection, mapping);
931
+ const markdown = renderDesignSpec(spec);
932
+ if (values.out) {
339
933
  await writeDesignSpec(projectRoot(), values.out, markdown);
934
+ }
340
935
  output.result(spec);
341
- if (!values.json)
936
+ if (!values.json) {
342
937
  output.log(markdown);
938
+ }
343
939
  return 0;
344
940
  }
345
- if (command === "spec" && (rest.length || !values.page || !values.frame))
941
+ if (command === "spec" && (rest.length || !values.page || !values.frame)) {
346
942
  throw new CommandArgumentError("Live spec requires --page and --frame ID (or --from for offline generation)");
943
+ }
347
944
  if (command === "baselines" && rest[0] === "coverage") {
348
- if (rest.length !== 1 || values.apply || values.update || values["any-file"] || values["any-instance"] || values.standalone)
945
+ if (rest.length !== 1 ||
946
+ values.apply ||
947
+ values.update ||
948
+ values["any-file"] ||
949
+ values["any-instance"] ||
950
+ values.standalone) {
349
951
  throw new CommandArgumentError("baselines coverage is an offline report with optional --json");
952
+ }
350
953
  const config = await loadCommandConfig(projectRoot());
351
- if (!config.baselines)
954
+ if (!config.baselines) {
352
955
  throw new Error("Configure baselines before reporting coverage");
956
+ }
353
957
  const report = await readBaselineCoverage(projectRoot(), config.baselines, config.file?.id);
354
958
  output.result(report);
355
- if (!values.json)
959
+ if (!values.json) {
356
960
  output.log(JSON.stringify(report, null, 2));
961
+ }
357
962
  return report.designCasesComplete ? 0 : 3;
358
963
  }
359
964
  if (command === "kit" || command === "recipes") {
360
- if (command === "kit" ? rest.length !== 1 || rest[0] !== "check" : rest.length)
965
+ if (command === "kit" ? rest.length !== 1 || rest[0] !== "check" : rest.length) {
361
966
  throw new CommandArgumentError("Use kit check, or recipes, with optional ordered --include aliases");
362
- const kits = await loadKits(projectRoot(), values.include ?? ["@kit"]);
363
- output.result(command === "kit" ? checkKits(kits) : { recipes: kits.flatMap(value => value.recipes.map(recipe => ({ ...recipe, include: value.include }))), includes: kits.map(value => value.include) });
364
- if (!values.json)
365
- output.log(command === "kit" ? JSON.stringify(checkKits(kits), null, 2) : kits.flatMap(value => value.recipes.map(recipe => `${value.include} ${recipe.name}: ${recipe.description}`)).join("\n"));
967
+ }
968
+ const kits = await loadKits(projectRoot(), values.include ?? [
969
+ "@kit",
970
+ ]);
971
+ output.result(command === "kit"
972
+ ? checkKits(kits)
973
+ : {
974
+ recipes: kits.flatMap((value) => value.recipes.map((recipe) => ({
975
+ ...recipe,
976
+ include: value.include,
977
+ }))),
978
+ includes: kits.map((value) => value.include),
979
+ });
980
+ if (!values.json) {
981
+ output.log(command === "kit"
982
+ ? JSON.stringify(checkKits(kits), null, 2)
983
+ : kits
984
+ .flatMap((value) => value.recipes.map((recipe) => `${value.include} ${recipe.name}: ${recipe.description}`))
985
+ .join("\n"));
986
+ }
366
987
  return 0;
367
988
  }
368
989
  if (command === "tokens" && rest[0] === "diff") {
369
990
  const [, beforeFile, afterFile] = rest;
370
- if (!beforeFile || !afterFile || !values.theme)
991
+ if (!beforeFile || !afterFile || !values.theme) {
371
992
  throw new Error("usage: penpair tokens diff <before.json> <after.json> --theme <name>");
993
+ }
372
994
  const load = async (file) => parseTokenDump(JSON.parse(await readFile(file, "utf8")));
373
995
  const diff = diffEffective(effectiveValues(await load(beforeFile), values.theme), effectiveValues(await load(afterFile), values.theme));
374
996
  output.log(JSON.stringify(diff, null, 2));
375
- return !diff.complete ? 3 : diff.added.length + diff.removed.length + diff.changed.length === 0 ? 0 : 2;
997
+ return !diff.complete
998
+ ? 3
999
+ : diff.added.length + diff.removed.length + diff.changed.length === 0
1000
+ ? 0
1001
+ : 2;
376
1002
  }
377
1003
  const root = projectRoot();
378
1004
  const log = (message) => output.error(`… ${message}`);
379
1005
  if (values.standalone) {
380
- const inspection = ["inspect", "pages", "tree", "audit", "export", "shots"].includes(command) || (command === "probe" && !values.deep) || (command === "tokens" && rest[0] === "dump");
381
- if (await loadOptionalConfig(root) || !inspection || values.apply || values.write)
1006
+ const inspection = [
1007
+ "inspect",
1008
+ "pages",
1009
+ "tree",
1010
+ "audit",
1011
+ "export",
1012
+ "shots",
1013
+ ].includes(command) ||
1014
+ (command === "probe" && !values.deep) ||
1015
+ (command === "tokens" && rest[0] === "dump");
1016
+ if ((await loadOptionalConfig(root)) || !inspection || values.apply || values.write) {
382
1017
  throw new ConfigError("--standalone is only for built-in inspection commands in a folder without penpair.config.json. It cannot bypass a project or run writes/snippets.");
1018
+ }
383
1019
  }
384
1020
  /** What `doctor` needs to look for the editor and plugin, or nothing when no endpoint is set yet. */
385
1021
  async function mcpProbeFor() {
@@ -388,12 +1024,31 @@ async function main(argv, cli) {
388
1024
  commandRedactor(endpointRedactor(endpoint));
389
1025
  return {
390
1026
  origin: endpoint.description.origin,
391
- connect: async () => {
392
- const client = await connect(endpoint);
1027
+ connect: async (request) => {
1028
+ if (!request) {
1029
+ throw new Error("Doctor requires a fresh challenge");
1030
+ }
1031
+ const started = Date.now();
1032
+ const client = await connect(endpoint, {
1033
+ timeoutMs: request.timeoutMs,
1034
+ signal: request.signal,
1035
+ });
393
1036
  try {
394
- const { result } = await executeCode(client, "return { file: penpot.currentFile.name, fileId: penpot.currentFile.id };", { retries: 0, effect: "read" });
395
- const actual = result;
396
- return { file: String(actual.file ?? "?"), fileId: actual.fileId };
1037
+ request.signal.throwIfAborted();
1038
+ request.onTransport();
1039
+ const remaining = request.timeoutMs - (Date.now() - started);
1040
+ if (remaining < 1) {
1041
+ throw new Error("Doctor connection exhausted execution budget");
1042
+ }
1043
+ const { result } = await executeCode(client, `return { challenge: ${JSON.stringify(request.challenge)}, file: penpot.currentFile.name, fileId: penpot.currentFile.id, pageId: penpot.currentPage.id, apiVersion: typeof penpot.version === "string" ? penpot.version : undefined };`, {
1044
+ retries: 0,
1045
+ effect: "read",
1046
+ timeoutMs: remaining,
1047
+ });
1048
+ if (!result || typeof result !== "object") {
1049
+ throw new Error("No fresh editor execution evidence");
1050
+ }
1051
+ return result;
397
1052
  }
398
1053
  finally {
399
1054
  await client.close();
@@ -407,26 +1062,49 @@ async function main(argv, cli) {
407
1062
  }
408
1063
  async function registryFor(requested) {
409
1064
  const registry = builtInRegistry();
410
- if (values["builtins-only"] || requested && registry.list().some((entry) => entry.name === requested))
411
- return { registry, problems: [], files: [] };
1065
+ if (values["builtins-only"] ||
1066
+ (requested && registry.list().some((entry) => entry.name === requested))) {
1067
+ return {
1068
+ registry,
1069
+ problems: [],
1070
+ files: [],
1071
+ };
1072
+ }
412
1073
  const snippets = (await loadOptionalConfig(root))?.snippets;
413
1074
  const report = await loadProjectSnippets(root, snippets, registry);
414
- return { registry, problems: report.problems, files: report.files };
1075
+ return {
1076
+ registry,
1077
+ problems: report.problems,
1078
+ files: report.files,
1079
+ };
415
1080
  }
416
1081
  const connectIo = () => ({
417
1082
  say: (line) => output.log(line),
418
1083
  askHidden,
419
1084
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
420
- confirmFile: async (file) => values["pin-file"] === true || /^(y|yes)$/i.test(await askLine(`Pin this project's file to "${file.name}" (${file.id})? [y/N] `, "n")),
1085
+ confirmFile: async (file) => values["pin-file"] === true ||
1086
+ /^(y|yes)$/i.test(await askLine(`Pin this project's file to "${file.name}" (${file.id})? [y/N] `, "n")),
421
1087
  openFile: async (url) => {
422
- const endpoint = await resolveEndpoint({ environment: { PENPAIR_MCP_URL: url }, envFiles: [] });
1088
+ const endpoint = await resolveEndpoint({
1089
+ environment: {
1090
+ PENPAIR_MCP_URL: url,
1091
+ },
1092
+ envFiles: [],
1093
+ });
423
1094
  commandRedactor(endpointRedactor(endpoint));
424
1095
  try {
425
1096
  const client = await connect(endpoint);
426
1097
  try {
427
- const { result } = await executeCode(client, "return { file: penpot.currentFile.name, fileId: penpot.currentFile.id, pages: penpot.currentFile.pages.map((p) => p.name) };", { retries: 0, effect: "read" });
1098
+ const { result } = await executeCode(client, "return { file: penpot.currentFile.name, fileId: penpot.currentFile.id, pages: penpot.currentFile.pages.map((p) => p.name) };", {
1099
+ retries: 0,
1100
+ effect: "read",
1101
+ });
428
1102
  const read = result;
429
- return { file: String(read.file ?? "?"), fileId: read.fileId, pages: read.pages ?? [] };
1103
+ return {
1104
+ file: String(read.file ?? "?"),
1105
+ fileId: read.fileId,
1106
+ pages: read.pages ?? [],
1107
+ };
430
1108
  }
431
1109
  finally {
432
1110
  await client.close();
@@ -440,16 +1118,30 @@ async function main(argv, cli) {
440
1118
  if (command === "setup" || command === "connect") {
441
1119
  if (command === "connect") {
442
1120
  const project = await loadCommandConfig(root);
443
- await connectProject(root, project, connectIo(), { replace: values.replace === true, anyFile: values["any-file"] === true });
1121
+ await connectProject(root, project, connectIo(), {
1122
+ replace: values.replace === true,
1123
+ anyFile: values["any-file"] === true,
1124
+ });
444
1125
  output.log("Next: `penpair doctor`, then `penpair skills install` if you have not yet.");
445
1126
  return 0;
446
1127
  }
447
1128
  let where;
448
- if (values.docker)
449
- where = { type: "docker" };
450
- else if (values.web || values.external)
451
- where = { type: "web", origin: values.external ?? hostedOrigin };
452
- await setup(root, { ...connectIo(), ask: (question, fallback) => askLine(question, fallback), openBrowser: openInBrowser }, {
1129
+ if (values.docker) {
1130
+ where = {
1131
+ type: "docker",
1132
+ };
1133
+ }
1134
+ else if (values.web || values.external) {
1135
+ where = {
1136
+ type: "web",
1137
+ origin: values.external ?? hostedOrigin,
1138
+ };
1139
+ }
1140
+ await setup(root, {
1141
+ ...connectIo(),
1142
+ ask: (question, fallback) => askLine(question, fallback),
1143
+ openBrowser: openInBrowser,
1144
+ }, {
453
1145
  where,
454
1146
  instance: values.instance,
455
1147
  port: values.port ? Number(values.port) : undefined,
@@ -457,78 +1149,137 @@ async function main(argv, cli) {
457
1149
  log,
458
1150
  local: cli.local,
459
1151
  installSkills: async () => {
460
- const report = await installSkills({ root, agents: values.agents });
1152
+ const report = await installSkills({
1153
+ root,
1154
+ agents: values.agents,
1155
+ });
461
1156
  output.log(`Installed ${report.installed.length} rule file(s) in ${report.to}/ (${report.unchanged.length} already there).`);
462
- if (report.instructions?.path)
1157
+ if (report.instructions?.path) {
463
1158
  output.log(`Instructions: ${report.instructions.path} (${report.instructions.status})`);
464
- else
1159
+ }
1160
+ else {
465
1161
  output.log(`Choose an instruction destination with skills install --agents <path>. Rows:\n${report.agentsRows.join("\n")}`);
1162
+ }
466
1163
  },
467
1164
  });
468
1165
  return 0;
469
1166
  }
470
1167
  if (command === "skills") {
471
1168
  if (rest[0] === "install") {
472
- const report = await installSkills({ root, to: values.to, only: values.only, force: values.force, agents: values.agents, dryRun: values["dry-run"] });
1169
+ const report = await installSkills({
1170
+ root,
1171
+ to: values.to,
1172
+ only: values.only,
1173
+ force: values.force,
1174
+ agents: values.agents,
1175
+ dryRun: values["dry-run"],
1176
+ });
473
1177
  output.result(report);
474
- if (report.instructions?.path)
1178
+ if (report.instructions?.path) {
475
1179
  output.log(`instructions: ${report.instructions.path} (${report.instructions.status})`);
1180
+ }
476
1181
  if (report.dryRun) {
477
- for (const file of report.wouldInstall ?? [])
1182
+ for (const file of report.wouldInstall ?? []) {
478
1183
  output.log(`would write ${file}`);
479
- for (const file of report.wouldLink ?? [])
1184
+ }
1185
+ for (const file of report.wouldLink ?? []) {
480
1186
  output.log(`would link ${file}`);
481
- if (report.instructions?.diff)
1187
+ }
1188
+ if (report.instructions?.diff) {
482
1189
  output.log(report.instructions.diff);
1190
+ }
483
1191
  }
484
- if (report.instructions?.status === "choice-required")
1192
+ if (report.instructions?.status === "choice-required") {
485
1193
  output.error("Both AGENTS.md and CLAUDE.md exist; choose --agents <path> explicitly. Instruction files were not changed.");
486
- for (const file of report.installed)
1194
+ }
1195
+ for (const file of report.installed) {
487
1196
  output.log(`wrote ${file}`);
488
- for (const file of report.kept)
1197
+ }
1198
+ for (const file of report.kept) {
489
1199
  output.log(`kept ${file} (differs from the shipped one; --force replaces it, except the project-owned files)`);
490
- for (const link of report.linked)
1200
+ }
1201
+ for (const link of report.linked) {
491
1202
  output.log(`linked ${link}`);
1203
+ }
492
1204
  output.log(`${report.unchanged.length} file(s) already up to date`);
493
- if (!report.instructions?.path)
1205
+ if (!report.instructions?.path) {
494
1206
  output.log("\nChoose --agents <path> to integrate these rows:\n" + report.agentsRows.join("\n"));
495
- for (const old of report.legacy)
1207
+ }
1208
+ for (const old of report.legacy) {
496
1209
  output.log(`\nnote ${old.folder} is the old name of ${old.renamedTo}. ${old.carriedFacts ? "Your project-facts.md was copied to the new folder. " : ""}Delete the old folder and its links (.agents/skills/${path.posix.basename(old.folder)}, .claude/skills/${path.posix.basename(old.folder)}) and update its row in your AGENTS.md.`);
1210
+ }
497
1211
  output.log("\nThen fill in references/project-facts.md in penpair-design: it is yours and is never replaced.");
498
1212
  return report.instructions?.status === "choice-required" ? 3 : 0;
499
1213
  }
500
1214
  const bundled = await listBundledSkills();
501
- output.result({ skills: bundled });
502
- for (const name of bundled)
1215
+ output.result({
1216
+ skills: bundled,
1217
+ });
1218
+ for (const name of bundled) {
503
1219
  output.log(name);
1220
+ }
504
1221
  return 0;
505
1222
  }
506
1223
  if (command === "snippets") {
507
1224
  const { registry, problems, files } = await registryFor();
508
1225
  if (rest[0] === "check") {
509
- const found = [...problems, ...checkProjectSnippets(registry)];
510
- output.result({ files, problems: found, valid: found.length === 0 });
1226
+ const found = [
1227
+ ...problems,
1228
+ ...checkProjectSnippets(registry),
1229
+ ];
1230
+ output.result({
1231
+ files,
1232
+ problems: found,
1233
+ valid: found.length === 0,
1234
+ });
511
1235
  const project = registry.list().filter((entry) => entry.source === "project");
512
1236
  output.log(`${project.length} project snippet(s) from ${files.length} file(s)`);
513
- for (const problem of found)
1237
+ for (const problem of found) {
514
1238
  output.log(`FAIL ${problem}`);
515
- if (found.length === 0)
1239
+ }
1240
+ if (found.length === 0) {
516
1241
  output.log("ok every snippet is valid plugin code and small enough");
1242
+ }
517
1243
  return found.length === 0 ? 0 : 1;
518
1244
  }
519
- output.result({ snippets: registry.list().map(({ name, description, kind, params, source }) => ({ name, description, kind, params, source })), problems, complete: problems.length === 0 });
1245
+ output.result({
1246
+ snippets: registry.list().map(({ name, description, kind, params, source }) => ({
1247
+ name,
1248
+ description,
1249
+ kind,
1250
+ params,
1251
+ source,
1252
+ })),
1253
+ problems,
1254
+ complete: problems.length === 0,
1255
+ });
520
1256
  for (const entry of registry.list()) {
521
1257
  output.log(`${entry.name.padEnd(14)} ${entry.kind === "write" ? "changes " : "reads "} ${entry.source === "project" ? "[project] " : ""}${entry.description}`);
522
- for (const p of entry.params)
1258
+ for (const p of entry.params) {
523
1259
  output.log(`${"".padEnd(14)} --param ${p.name}=<${p.type}>${p.required ? " (required)" : p.default !== undefined ? ` (default ${JSON.stringify(p.default)})` : ""}${p.description ? ` ${p.description}` : ""}`);
1260
+ }
524
1261
  }
525
- for (const problem of problems)
1262
+ for (const problem of problems) {
526
1263
  output.error(`warning: ${problem}`);
1264
+ }
527
1265
  return 0;
528
1266
  }
529
1267
  if (command === "init") {
530
- const result = await init(root, { instance: values.instance, port: values.port ? Number(values.port) : undefined, gitignore: values.gitignore, external: values.external }, { local: cli.local });
531
- output.result({ createdConfig: result.createdConfig, createdSecrets: result.createdSecrets, instance: result.config.instance, files: result.files, gitignoreAdded: result.gitignoreAdded });
1268
+ const result = await init(root, {
1269
+ instance: values.instance,
1270
+ port: values.port ? Number(values.port) : undefined,
1271
+ gitignore: values.gitignore,
1272
+ external: values.external,
1273
+ }, {
1274
+ local: cli.local,
1275
+ });
1276
+ output.result({
1277
+ createdConfig: result.createdConfig,
1278
+ createdSecrets: result.createdSecrets,
1279
+ instance: result.config.instance,
1280
+ files: result.files,
1281
+ gitignoreAdded: result.gitignoreAdded,
1282
+ });
532
1283
  if (isExternal(result.config)) {
533
1284
  output.log(`${result.createdConfig ? "created" : "kept"} penpair.config.json (external Penpot at ${result.config.profile.origin})`);
534
1285
  output.log("next: in Penpot switch on Account, Integrations, MCP Server and copy the address; open a design file and choose File, MCP Server, Connect; then run penpair connect (takes the address without showing it)");
@@ -536,8 +1287,9 @@ async function main(argv, cli) {
536
1287
  }
537
1288
  output.log(`${result.createdConfig ? "created" : "kept"} penpair.config.json (instance ${result.config.instance}, Penpot ${result.config.penpotVersion}, port ${result.config.ports.frontend})`);
538
1289
  output.log(`wrote ${result.config.directory}/docker-compose.yaml; secrets ${result.createdSecrets ? "generated" : "kept"} in ${result.config.directory}/.env`);
539
- if (!result.gitignoreAdded)
1290
+ if (!result.gitignoreAdded) {
540
1291
  output.log(`keep ${result.config.directory}/ out of git (add it to .gitignore, or run init with --gitignore)`);
1292
+ }
541
1293
  output.log("next: penpair up, then penpair connect");
542
1294
  return 0;
543
1295
  }
@@ -545,30 +1297,66 @@ async function main(argv, cli) {
545
1297
  async function report(checks, mcp) {
546
1298
  const keyCheck = await keyFileCheck(root, nodeRunner);
547
1299
  const backupCheck = await backupNoteCheck(root, await loadCommandConfig(root));
548
- const all = [...checks, ...(!mcp ? [{ check: "editor and plugin", ok: false, detail: "No resolved project credential/connection; editor identity was not observed", remedy: "Configure the private credential source, connect the intended editor, then rerun doctor." }] : []), ...(keyCheck ? [keyCheck] : []), ...(backupCheck ? [backupCheck] : [])];
1300
+ const all = [
1301
+ ...checks,
1302
+ ...(!mcp
1303
+ ? [
1304
+ {
1305
+ check: "editor and plugin",
1306
+ ok: false,
1307
+ detail: "No resolved project credential/connection; editor identity was not observed",
1308
+ remedy: "Configure the private credential source, connect the intended editor, then rerun doctor.",
1309
+ },
1310
+ ]
1311
+ : []),
1312
+ ...(keyCheck
1313
+ ? [
1314
+ keyCheck,
1315
+ ]
1316
+ : []),
1317
+ ...(backupCheck
1318
+ ? [
1319
+ backupCheck,
1320
+ ]
1321
+ : []),
1322
+ ];
549
1323
  const ok = all.every((check) => check.ok);
550
1324
  if (values.json) {
551
- output.log(JSON.stringify({ ok, mcpConfigured: Boolean(mcp), file: all.find((check) => check.file)?.file, checks: all }, null, 2));
1325
+ output.log(JSON.stringify({
1326
+ ok,
1327
+ mcpConfigured: Boolean(mcp),
1328
+ file: all.find((check) => check.file)?.file,
1329
+ checks: all,
1330
+ }, null, 2));
552
1331
  return ok ? 0 : 1;
553
1332
  }
554
- for (const check of all)
1333
+ for (const check of all) {
555
1334
  output.log(`${check.warning ? "WARN" : check.ok ? "ok " : "FAIL"} ${check.check}: ${check.detail}${"remedy" in check && check.remedy ? `\n → ${check.remedy}` : ""}`);
556
- if (!mcp)
1335
+ }
1336
+ if (!mcp) {
557
1337
  output.log("skip editor and plugin: no key yet. Run `penpair connect`.");
1338
+ }
558
1339
  return ok ? 0 : 1;
559
1340
  }
560
1341
  if (command === "open") {
561
- if (rest.length)
1342
+ if (rest.length) {
562
1343
  throw new CommandArgumentError("open accepts only --print for a URL preview");
563
- const config = await loadCommandConfig(root), url = values.print ? projectFileUrl(config) : await openProject(config, openInBrowser);
564
- output.result({ url, opened: !values.print });
565
- if (!values.json)
1344
+ }
1345
+ const config = await loadCommandConfig(root);
1346
+ const url = values.print ? projectFileUrl(config) : await openProject(config, openInBrowser);
1347
+ output.result({
1348
+ url,
1349
+ opened: !values.print,
1350
+ });
1351
+ if (!values.json) {
566
1352
  output.log(url);
1353
+ }
567
1354
  return 0;
568
1355
  }
569
1356
  if (command === "status") {
570
- if (rest.length)
1357
+ if (rest.length) {
571
1358
  throw new CommandArgumentError("status accepts optional --adapters");
1359
+ }
572
1360
  const config = await loadCommandConfig(root);
573
1361
  let endpoint;
574
1362
  try {
@@ -577,51 +1365,112 @@ async function main(argv, cli) {
577
1365
  assertEndpointIsInstance(endpoint.description, config);
578
1366
  }
579
1367
  catch (error) {
580
- if (endpoint)
1368
+ if (endpoint) {
581
1369
  throw error;
1370
+ }
582
1371
  }
583
1372
  const report = await projectStatus(root, config, {
584
- ...(endpoint ? { connection: async () => guardFile(await connect(endpoint), config.file, { mode: "strict" }), redact: endpointRedactor(endpoint) } : {}),
585
- ...(values.adapters ? { adapterStatus: async () => {
586
- const loaded = await loadAdapters(root, config.adapters ?? []);
587
- if (loaded.problems.length)
588
- throw new Error(loaded.problems.join("; "));
589
- const context = await adapterContext(root, []);
590
- return (await Promise.all(loaded.adapters.map(async (a) => a.status ? a.status(context) : [{ id: a.name, label: a.name, state: "unknown", detail: "No status hook" }]))).flat();
591
- } } : {}),
1373
+ ...(endpoint
1374
+ ? {
1375
+ connection: async () => guardFile(await connect(endpoint), config.file, {
1376
+ mode: "strict",
1377
+ }),
1378
+ redact: endpointRedactor(endpoint),
1379
+ }
1380
+ : {}),
1381
+ ...(values.adapters
1382
+ ? {
1383
+ adapterStatus: async () => {
1384
+ const loaded = await loadAdapters(root, config.adapters ?? []);
1385
+ if (loaded.problems.length) {
1386
+ throw new Error(loaded.problems.join("; "));
1387
+ }
1388
+ const context = await adapterContext(root, []);
1389
+ return (await Promise.all(loaded.adapters.map(async (a) => a.status
1390
+ ? a.status(context)
1391
+ : [
1392
+ {
1393
+ id: a.name,
1394
+ label: a.name,
1395
+ state: "unknown",
1396
+ detail: "No status hook",
1397
+ },
1398
+ ]))).flat();
1399
+ },
1400
+ }
1401
+ : {}),
592
1402
  });
593
1403
  let localState = null;
594
1404
  if (!isExternal(config)) {
595
1405
  if (cli.local) {
596
- const state = await cli.local.status({ root, config, runner: nodeRunner, log });
1406
+ const state = await cli.local.status({
1407
+ root,
1408
+ config,
1409
+ runner: nodeRunner,
1410
+ log,
1411
+ });
597
1412
  localState = state;
598
- report.lines.push({ id: "local-runtime", label: "Local containers", state: state.frontendReachable && state.backendReady ? "ok" : "failed", detail: `${state.url}; backend ${state.backendReady ? "ready" : "not ready"}` });
1413
+ report.lines.push({
1414
+ id: "local-runtime",
1415
+ label: "Local containers",
1416
+ state: state.frontendReachable && state.backendReady ? "ok" : "failed",
1417
+ detail: `${state.url}; backend ${state.backendReady ? "ready" : "not ready"}`,
1418
+ });
599
1419
  }
600
- else
601
- report.lines.push({ id: "local-runtime", label: "Local containers", state: "unknown", detail: "Install the local-instance command package for container diagnostics" });
602
- report.complete = report.lines.every(line => line.state !== "unknown");
603
- report.ok = report.complete && report.lines.every(line => line.state === "ok");
1420
+ else {
1421
+ report.lines.push({
1422
+ id: "local-runtime",
1423
+ label: "Local containers",
1424
+ state: "unknown",
1425
+ detail: "Install the local-instance command package for container diagnostics",
1426
+ });
1427
+ }
1428
+ report.complete = report.lines.every((line) => line.state !== "unknown");
1429
+ report.ok = report.complete && report.lines.every((line) => line.state === "ok");
604
1430
  }
605
- output.result({ ...report, localState });
606
- if (!values.json)
607
- for (const line of report.lines)
1431
+ output.result({
1432
+ ...report,
1433
+ localState,
1434
+ });
1435
+ if (!values.json) {
1436
+ for (const line of report.lines) {
608
1437
  output.log(`${line.state.padEnd(7)} ${line.label}: ${line.detail}`);
609
- return report.lines.some(l => l.state === "failed") ? 1 : report.ok ? 0 : 3;
1438
+ }
1439
+ }
1440
+ return report.lines.some((l) => l.state === "failed") ? 1 : report.ok ? 0 : 3;
610
1441
  }
611
- const lifecycle = new Set(["up", "down", "status", "doctor", "backup", "restore"]);
1442
+ const lifecycle = new Set([
1443
+ "up",
1444
+ "down",
1445
+ "status",
1446
+ "doctor",
1447
+ "backup",
1448
+ "restore",
1449
+ ]);
612
1450
  if (lifecycle.has(command) && !(command === "backup" && rest[0] === "file")) {
613
1451
  const project = await loadCommandConfig(root);
614
- if (isExternal(project) && command !== "doctor")
1452
+ if (isExternal(project) && command !== "doctor") {
615
1453
  requireLocal(project);
1454
+ }
616
1455
  if (isExternal(project)) {
617
1456
  const mcp = await mcpProbeFor();
618
- return report(await doctorExternal(project, { mcp }), mcp);
1457
+ return report(await doctorExternal(project, {
1458
+ mcp,
1459
+ timeoutMs: Number(values["timeout-ms"] ?? 5000),
1460
+ }), mcp);
619
1461
  }
620
1462
  const local = requireLocalProfile(cli.local);
621
- const c = { root, config: requireLocal(project), runner: nodeRunner, log };
1463
+ const c = {
1464
+ root,
1465
+ config: requireLocal(project),
1466
+ runner: nodeRunner,
1467
+ log,
1468
+ };
622
1469
  if (command === "up") {
623
1470
  const result = await local.up(c);
624
- output.log(result.ready ? `Penpot is up at ${result.url}. Create an account and open a file, then run \`penpair connect\`.` : `The containers started but ${result.url} does not answer yet. Run \`penpair status\`.`);
1471
+ output.log(result.ready
1472
+ ? `Penpot is up at ${result.url}. Create an account and open a file, then run \`penpair connect\`.`
1473
+ : `The containers started but ${result.url} does not answer yet. Run \`penpair status\`.`);
625
1474
  return result.ready ? 0 : 1;
626
1475
  }
627
1476
  if (command === "down") {
@@ -631,22 +1480,28 @@ async function main(argv, cli) {
631
1480
  }
632
1481
  if (command === "status") {
633
1482
  const state = await local.status(c);
634
- for (const s of state.services)
1483
+ for (const s of state.services) {
635
1484
  output.log(`${s.state.padEnd(10)} ${s.service}${s.health ? ` (${s.health})` : ""}`);
1485
+ }
636
1486
  output.log(`${state.frontendReachable ? "answers" : "does not answer"} at ${state.url}; backend ${state.backendReady ? "ready" : "not ready"}`);
637
1487
  return state.frontendReachable && state.backendReady ? 0 : 1;
638
1488
  }
639
1489
  if (command === "doctor") {
640
1490
  const mcp = await mcpProbeFor();
641
- return report(await local.doctor(c, { mcp }), mcp);
1491
+ return report(await local.doctor(c, {
1492
+ mcp,
1493
+ timeoutMs: Number(values["timeout-ms"] ?? 5000),
1494
+ }), mcp);
642
1495
  }
643
1496
  if (command === "backup") {
644
1497
  if (rest[0] === "verify") {
645
- if (!rest[1])
1498
+ if (!rest[1]) {
646
1499
  throw new Error("usage: penpair backup verify <backup directory>");
1500
+ }
647
1501
  const result = await local.verifyBackup(c, rest[1]);
648
- for (const check of result.checks)
1502
+ for (const check of result.checks) {
649
1503
  output.log(`${check.ok ? "ok " : "FAIL"} ${check.check}: ${check.detail}`);
1504
+ }
650
1505
  return result.ok ? 0 : 1;
651
1506
  }
652
1507
  const result = await local.backup(c);
@@ -654,62 +1509,114 @@ async function main(argv, cli) {
654
1509
  return 0;
655
1510
  }
656
1511
  if (command === "restore") {
657
- if (!rest[0])
1512
+ if (!rest[0]) {
658
1513
  throw new Error("usage: penpair restore <backup directory> --yes");
659
- const result = await local.restore(c, rest[0], { yes: values.yes === true });
660
- for (const check of result.checks)
1514
+ }
1515
+ const result = await local.restore(c, rest[0], {
1516
+ yes: values.yes === true,
1517
+ });
1518
+ for (const check of result.checks) {
661
1519
  output.log(`${check.ok ? "ok " : "FAIL"} ${check.check}: ${check.detail}`);
662
- if (result.safetyBackup)
1520
+ }
1521
+ if (result.safetyBackup) {
663
1522
  output.log(`safety backup of the previous data: ${result.safetyBackup}`);
1523
+ }
664
1524
  return result.ok ? 0 : 1;
665
1525
  }
666
1526
  }
667
- if (command === "run-file" && rest.length !== 1)
1527
+ if (command === "run-file" && rest.length !== 1) {
668
1528
  throw new CommandArgumentError("run-file requires exactly one plugin body path");
669
- const projectConfig = values.standalone ? await loadOptionalConfig(root) : await loadCommandConfig(root);
670
- if (values.standalone && projectConfig)
1529
+ }
1530
+ const projectConfig = values.standalone
1531
+ ? await loadOptionalConfig(root)
1532
+ : await loadCommandConfig(root);
1533
+ if (values.standalone && projectConfig) {
671
1534
  throw new ConfigError("--standalone cannot bypass a project configuration.");
1535
+ }
672
1536
  let preparedFile;
673
1537
  if (command === "run-file") {
674
1538
  const params = Object.create(null);
675
1539
  for (const pair of values.param ?? []) {
676
1540
  const at = pair.indexOf("=");
677
- if (at < 1)
1541
+ if (at < 1) {
678
1542
  throw new CommandArgumentError("--param needs key=value");
1543
+ }
679
1544
  const key = pair.slice(0, at);
680
- if (Object.hasOwn(params, key))
1545
+ if (Object.hasOwn(params, key)) {
681
1546
  throw new CommandArgumentError("Duplicate script parameter: " + key);
1547
+ }
682
1548
  params[key] = pair.slice(at + 1);
683
1549
  }
684
- const aliases = (values.include ?? []).filter(include => include.startsWith("@"));
1550
+ const aliases = (values.include ?? []).filter((include) => include.startsWith("@"));
685
1551
  const kits = aliases.length ? await loadKits(root, aliases) : [];
686
- const virtualIncludes = Object.fromEntries(kits.map(value => [value.include, value.source]));
687
- preparedFile = await prepareFileScript(root, rest[0], { virtualIncludes, include: values.include, inspect: values.inspect, readback: values.readback, page: values.page, params });
688
- if (preparedFile.kind === "read" && values.apply)
1552
+ const virtualIncludes = Object.fromEntries(kits.map((value) => [
1553
+ value.include,
1554
+ value.source,
1555
+ ]));
1556
+ preparedFile = await prepareFileScript(root, rest[0], {
1557
+ virtualIncludes,
1558
+ include: values.include,
1559
+ inspect: values.inspect,
1560
+ readback: values.readback,
1561
+ page: values.page,
1562
+ params,
1563
+ });
1564
+ if (preparedFile.kind === "read" && values.apply) {
689
1565
  throw new CommandArgumentError("Read files do not require or accept --apply");
1566
+ }
690
1567
  }
691
1568
  const endpoint = await projectEndpoint(root);
692
1569
  commandRedactor(endpointRedactor(endpoint));
693
- if (projectConfig && !values["any-instance"])
1570
+ if (projectConfig && !values["any-instance"]) {
694
1571
  assertEndpointIsInstance(endpoint.description, projectConfig);
1572
+ }
695
1573
  const guarded = async () => guardFile(await connect(endpoint), projectConfig?.file, {
696
1574
  anyFile: values["any-file"] === true,
697
- mode: !commandProjects.getStore()?.strictFileGuard && (["pages", "tree", "audit", "tokens"].includes(command) || command === "probe" && !values.deep && !values.integrity) ? "warn" : "strict",
1575
+ mode: !commandProjects.getStore()?.strictFileGuard &&
1576
+ ([
1577
+ "pages",
1578
+ "tree",
1579
+ "audit",
1580
+ "tokens",
1581
+ ].includes(command) ||
1582
+ (command === "probe" && !values.deep && !values.integrity))
1583
+ ? "warn"
1584
+ : "strict",
698
1585
  warn: log,
699
1586
  });
700
1587
  if (command === "probe") {
701
- if (values.deep && !values.write)
1588
+ if (values.deep && !values.write) {
702
1589
  throw new Error("probe --deep creates a temporary board, a token set and two components in the open file and removes them again. Run it on a scratch file, with --write to confirm.");
703
- const capabilityReport = await probe(endpoint, guarded, { integrity: values.integrity === true });
1590
+ }
1591
+ const capabilityReport = await probe(endpoint, guarded, {
1592
+ integrity: values.integrity === true,
1593
+ });
704
1594
  output.log(JSON.stringify(capabilityReport, null, 2));
705
- if (capabilityReport.integrity?.supported && capabilityReport.integrity.valid === false)
1595
+ if (capabilityReport.integrity?.supported && capabilityReport.integrity.valid === false) {
706
1596
  return 1;
1597
+ }
707
1598
  if (values.deep) {
708
1599
  const client = await guarded();
709
1600
  try {
710
- const report = await runDeepCheck(client, { onWait: (m) => output.error(`… ${m}`) });
711
- const kitReport = values.kit && report.ok ? await runKitSmoke(client, root, { apply: true, onStep: log, redact: endpointRedactor(endpoint) }) : undefined;
712
- output.result({ ...report, ...(kitReport ? { kit: kitReport } : {}), capabilities: capabilityReport });
1601
+ const report = await runDeepCheck(client, {
1602
+ onWait: (m) => output.error(`… ${m}`),
1603
+ });
1604
+ const kitReport = values.kit && report.ok
1605
+ ? await runKitSmoke(client, root, {
1606
+ apply: true,
1607
+ onStep: log,
1608
+ redact: endpointRedactor(endpoint),
1609
+ })
1610
+ : undefined;
1611
+ output.result({
1612
+ ...report,
1613
+ ...(kitReport
1614
+ ? {
1615
+ kit: kitReport,
1616
+ }
1617
+ : {}),
1618
+ capabilities: capabilityReport,
1619
+ });
713
1620
  output.log(`\n${formatDeepReport(report)}`);
714
1621
  return report.ok && (!kitReport || kitReport.verified) ? 0 : 1;
715
1622
  }
@@ -721,91 +1628,184 @@ async function main(argv, cli) {
721
1628
  }
722
1629
  const client = await guarded();
723
1630
  commandRedactor(endpointRedactor(endpoint));
724
- const options = { signal: commandSignal(), redact: endpointRedactor(endpoint), onWait: (m) => output.error(`… ${m}`), onStep: (m) => output.error(`… ${m}`) };
1631
+ const options = {
1632
+ ...(values.layout
1633
+ ? {
1634
+ layout: {
1635
+ strategy: values.layout,
1636
+ },
1637
+ }
1638
+ : {}),
1639
+ signal: commandSignal(),
1640
+ redact: endpointRedactor(endpoint),
1641
+ onWait: (m) => output.error(`… ${m}`),
1642
+ onStep: (m) => output.error(`… ${m}`),
1643
+ };
725
1644
  const writeOptions = (operation) => ({
726
- ...options, apply: values.apply === true, redact: (text) => redactEndpoint(text, endpoint),
727
- ...(values.apply ? { journal: {
728
- root, forceLock: values["force-lock"] === true, runId: values.resume, resume: Boolean(values.resume), requireCheckpoint: values.checkpoint === true,
729
- ...(values.checkpoint ? { checkpoint: (runId) => createCheckpoint(client, { label: operation, runId }, { apply: true }) } : {}),
730
- } } : {}),
1645
+ ...options,
1646
+ apply: values.apply === true,
1647
+ redact: (text) => redactEndpoint(text, endpoint),
1648
+ ...(values.apply
1649
+ ? {
1650
+ journal: {
1651
+ root,
1652
+ trace: values.trace === true,
1653
+ forceLock: values["force-lock"] === true,
1654
+ runId: values.resume,
1655
+ resume: Boolean(values.resume),
1656
+ requireCheckpoint: values.checkpoint === true,
1657
+ ...(values.checkpoint
1658
+ ? {
1659
+ checkpoint: (runId) => createCheckpoint(client, {
1660
+ label: operation,
1661
+ runId,
1662
+ }, {
1663
+ apply: true,
1664
+ }),
1665
+ }
1666
+ : {}),
1667
+ },
1668
+ }
1669
+ : {}),
731
1670
  });
732
1671
  try {
733
1672
  if (command === "backup" && rest[0] === "file") {
734
1673
  const out = rest[1];
735
- if (!out || !out.endsWith(".penpot"))
1674
+ if (!out || !out.endsWith(".penpot")) {
736
1675
  throw new Error("usage: penpair backup file <new.penpot>");
1676
+ }
737
1677
  const archive = await exportFileArchive(client);
738
1678
  if (archive.status !== "exported") {
739
1679
  output.error(redactEndpoint(archive.message, endpoint));
740
1680
  return 1;
741
1681
  }
742
1682
  await atomicWriteFile(out, archive.bytes);
743
- output.log(JSON.stringify({ file: out, fileId: archive.fileId, revn: archive.revn, bytes: archive.bytes.length, sha256: archive.sha256, restoreVerified: false }, null, 2));
1683
+ output.log(JSON.stringify({
1684
+ file: out,
1685
+ fileId: archive.fileId,
1686
+ revn: archive.revn,
1687
+ bytes: archive.bytes.length,
1688
+ sha256: archive.sha256,
1689
+ restoreVerified: false,
1690
+ }, null, 2));
744
1691
  output.error("Archive bytes saved. Restore is not verified: import into a scratch project and compare content and renders.");
745
1692
  return 0;
746
1693
  }
747
1694
  if (command === "run-file") {
748
1695
  const report = await runFileScript(client, preparedFile, writeOptions("run-file"));
749
1696
  output.log(JSON.stringify(report, null, 2));
750
- if (report.phase === "inspected")
1697
+ if (report.phase === "inspected") {
751
1698
  output.error("Nothing was written. Review the inspection, then use --apply.");
1699
+ }
752
1700
  return report.complete === false ? 3 : report.phase === "applied" && !report.verified ? 1 : 0;
753
1701
  }
754
1702
  if (command === "run") {
755
1703
  const [name] = rest;
756
- if (!name)
1704
+ if (!name) {
757
1705
  throw new Error("usage: penpair run <snippet> [--param key=value ...] [--apply]");
1706
+ }
758
1707
  const { registry, problems } = await registryFor(name);
759
- for (const problem of problems)
1708
+ for (const problem of problems) {
760
1709
  output.error(`warning: ${problem}`);
1710
+ }
761
1711
  const raw = {};
762
1712
  for (const pair of values.param ?? []) {
763
1713
  const at = pair.indexOf("=");
764
- if (at < 1)
1714
+ if (at < 1) {
765
1715
  throw new RegistryError(`--param needs key=value, got ${JSON.stringify(pair)}`);
1716
+ }
766
1717
  const key = pair.slice(0, at);
767
1718
  const value = pair.slice(at + 1);
768
1719
  const earlier = raw[key];
769
- raw[key] = earlier === undefined ? value : [...(Array.isArray(earlier) ? earlier : [earlier]), value];
1720
+ raw[key] =
1721
+ earlier === undefined
1722
+ ? value
1723
+ : [
1724
+ ...(Array.isArray(earlier)
1725
+ ? earlier
1726
+ : [
1727
+ earlier,
1728
+ ]),
1729
+ value,
1730
+ ];
770
1731
  }
771
1732
  const entry = registry.get(name);
772
- if (entry.kind === "read" && (values.resume || values.checkpoint))
1733
+ if (entry.kind === "read" && (values.resume || values.checkpoint)) {
773
1734
  throw new Error("Read snippets have no write run to resume or checkpoint");
1735
+ }
774
1736
  const result = await registry.run(client, name, coerceParams(entry.params, raw), writeOptions(name));
775
1737
  output.log(JSON.stringify(result, null, 2));
776
- if (entry.kind === "write" && !values.apply)
1738
+ if (entry.kind === "write" && !values.apply) {
777
1739
  output.error("Nothing was written. Run again with --apply to do it.");
778
- return result.phase === "applied" && !result.verified ? 1 : 0;
1740
+ }
1741
+ return result.phase === "applied" &&
1742
+ !result.verified
1743
+ ? 1
1744
+ : 0;
779
1745
  }
780
1746
  if (command === "baselines") {
781
1747
  const config = (await loadCommandConfig(root)).baselines;
782
- if (!config)
1748
+ if (!config) {
783
1749
  throw new Error('penpair.config.json has no "baselines" block. Add {"page": "<page>", "root": "tests/baselines", "frames": [{"frame": "<frame name>", "id": "<file name>", "route": "<route>"}]}; see docs/guide.md.');
784
- const report = await exportBaselines(client, root, config, { ...options, apply: values.apply === true, update: values.update === true, forceReason: values["force-reason"] });
1750
+ }
1751
+ const report = await exportBaselines(client, root, config, {
1752
+ ...options,
1753
+ apply: values.apply === true,
1754
+ update: values.update === true,
1755
+ forceReason: values["force-reason"],
1756
+ });
785
1757
  output.result(report);
786
- for (const r of report.results)
1758
+ for (const r of report.results) {
787
1759
  output.log(`${r.status.padEnd(9)} ${r.id.padEnd(28)} ${r.frame}${r.imageWidth ? ` (${r.imageWidth}x${r.imageHeight})` : ""}${r.written ? " written" : ""}`);
788
- for (const file of report.wrote)
1760
+ }
1761
+ for (const file of report.wrote) {
789
1762
  output.log(`wrote ${file}`);
1763
+ }
790
1764
  const changed = report.results.filter((r) => r.status === "changed" && !r.written);
791
- if (!values.apply)
1765
+ if (!values.apply) {
792
1766
  output.error("Nothing was written. Run again with --apply to write new images.");
793
- if (changed.length > 0)
1767
+ }
1768
+ if (changed.length > 0) {
794
1769
  output.error(`${changed.length} design(s) differ from the image in the folder. That is a change of the approved design: review it in Penpot, then run again with --apply --update to approve it.`);
795
- for (const row of report.coverage.rows)
796
- if (row.status !== "approved-design-intent")
1770
+ }
1771
+ for (const row of report.coverage.rows) {
1772
+ if (row.status !== "approved-design-intent") {
797
1773
  output.error(`coverage ${row.status}: ${row.requirement.route} ${row.requirement.theme}/${row.requirement.state} ${row.requirement.viewport.width}x${row.requirement.viewport.height}${row.reason ? ": " + row.reason : ""}`);
798
- if (!report.complete || report.coverage.configured && !report.coverage.designCasesComplete)
1774
+ }
1775
+ }
1776
+ if (!report.complete ||
1777
+ (report.coverage.configured && !report.coverage.designCasesComplete)) {
799
1778
  return 3;
1779
+ }
800
1780
  return changed.length > 0 ? 2 : 0;
801
1781
  }
802
1782
  if (command === "audit" && (values.rules || projectConfig?.audit)) {
803
- if (!values.page || !projectConfig?.audit || rest.length || values.prefix || values.steps || values.ignore || values["token-source"])
1783
+ if (!values.page ||
1784
+ !projectConfig?.audit ||
1785
+ rest.length ||
1786
+ values.prefix ||
1787
+ values.steps ||
1788
+ values.ignore ||
1789
+ values["token-source"]) {
804
1790
  throw new CommandArgumentError("Configured audit requires --page and explicit config rules, without legacy filters");
805
- const shapes = await captureInspection(client, { kind: "shape", shapeId: "@page-root", page: values.page, depth: 10000, maxNodes: 50000 });
806
- const tokens = projectConfig.audit.contrast ? await captureInspection(client, { kind: "tokens" }) : undefined;
807
- if (!shapes.complete || tokens && !tokens.complete) {
808
- output.result({ complete: false, reason: "Audit inspection incomplete" });
1791
+ }
1792
+ const shapes = await captureInspection(client, {
1793
+ kind: "shape",
1794
+ shapeId: "@page-root",
1795
+ page: values.page,
1796
+ depth: 10000,
1797
+ maxNodes: 50000,
1798
+ });
1799
+ const tokens = projectConfig.audit.contrast
1800
+ ? await captureInspection(client, {
1801
+ kind: "tokens",
1802
+ })
1803
+ : undefined;
1804
+ if (!shapes.complete || (tokens && !tokens.complete)) {
1805
+ output.result({
1806
+ complete: false,
1807
+ reason: "Audit inspection incomplete",
1808
+ });
809
1809
  return 3;
810
1810
  }
811
1811
  const report = auditDesignRules(shapes, projectConfig.audit, tokens);
@@ -813,29 +1813,45 @@ async function main(argv, cli) {
813
1813
  output.log(JSON.stringify(report, null, 2));
814
1814
  return report.complete ? 0 : 3;
815
1815
  }
816
- if (command === "pages")
1816
+ if (command === "pages") {
817
1817
  output.log(JSON.stringify(await runSnippet(client, pages(), options), null, 2));
1818
+ }
818
1819
  else if (command === "tree") {
819
- if (!values.page)
1820
+ if (!values.page) {
820
1821
  throw new Error("--page is required");
821
- const record = await runOnPage(client, tree({ page: values.page, depth: values.depth ? Number(values.depth) : 3, limit: values.limit ? Number(values.limit) : 400, namePrefix: values.prefix }), values.page, options);
822
- if (values.json)
1822
+ }
1823
+ const record = await runOnPage(client, tree({
1824
+ page: values.page,
1825
+ depth: values.depth ? Number(values.depth) : 3,
1826
+ limit: values.limit ? Number(values.limit) : 400,
1827
+ namePrefix: values.prefix,
1828
+ }), values.page, options);
1829
+ if (values.json) {
823
1830
  output.log(JSON.stringify(record));
824
- else
1831
+ }
1832
+ else {
825
1833
  output.log((record.lines ?? []).join("\n"));
1834
+ }
826
1835
  if (record.truncated === true) {
827
1836
  output.error("Tree is a limited preview; use inspect --all for full text or narrow the tree scope.");
828
1837
  return 3;
829
1838
  }
830
1839
  }
831
1840
  else if (command === "audit") {
832
- if (!values.page || !values.prefix)
1841
+ if (!values.page || !values.prefix) {
833
1842
  throw new Error("--page and --prefix are required");
1843
+ }
834
1844
  const steps = values.steps ? values.steps.split(",").map(Number) : undefined;
835
- const record = await runOnPage(client, audit({ page: values.page, framePrefix: values.prefix, spacingSteps: steps, ignoreUnbound: list(values.ignore) }), values.page, options);
1845
+ const record = await runOnPage(client, audit({
1846
+ page: values.page,
1847
+ framePrefix: values.prefix,
1848
+ spacingSteps: steps,
1849
+ ignoreUnbound: list(values.ignore),
1850
+ }), values.page, options);
836
1851
  output.log(JSON.stringify(record, null, 2));
837
- if (record.truncated === true)
1852
+ if (record.truncated === true) {
838
1853
  return 3;
1854
+ }
839
1855
  }
840
1856
  else if (command === "snapshot") {
841
1857
  const snapshot = await captureStructuralSnapshot(client, options);
@@ -851,48 +1867,91 @@ async function main(argv, cli) {
851
1867
  return 0;
852
1868
  }
853
1869
  else if (command === "approve") {
854
- if (rest.length || !values.page || !values.frame || values.write && (!values.note?.trim() || !values.by?.trim()) || !values.write && (values.note || values.by))
1870
+ if (rest.length ||
1871
+ !values.page ||
1872
+ !values.frame ||
1873
+ (values.write && (!values.note?.trim() || !values.by?.trim())) ||
1874
+ (!values.write && (values.note || values.by))) {
855
1875
  throw new CommandArgumentError("approve --page PAGE --frame ID [--case CASE_ID] previews evidence; --write requires --note exact-owner-words and --by identity");
1876
+ }
856
1877
  let intent;
857
1878
  if (values.case) {
858
- const baseline = projectConfig?.baselines, wanted = baseline?.frames.find(f => f.id === values.case);
859
- if (!wanted?.case || baseline?.page !== values.page)
1879
+ const baseline = projectConfig?.baselines;
1880
+ const wanted = baseline?.frames.find((f) => f.id === values.case);
1881
+ if (!wanted?.case || baseline?.page !== values.page) {
860
1882
  throw new Error("Approval case must reference an explicitly configured frame on this page");
1883
+ }
861
1884
  const found = await findShapesByName(client, wanted.frame, values.page);
862
- if (found.matches.length !== 1 || found.matches[0].id !== values.frame)
1885
+ if (found.matches.length !== 1 || found.matches[0].id !== values.frame) {
863
1886
  throw new Error("Approval frame ID does not match the configured case");
1887
+ }
864
1888
  intent = wanted.case;
865
1889
  }
866
- const captured = await exportShapePng(client, values.frame, { ...options, page: values.page, stable: true });
867
- if (!captured.stable)
1890
+ const captured = await exportShapePng(client, values.frame, {
1891
+ ...options,
1892
+ page: values.page,
1893
+ stable: true,
1894
+ });
1895
+ if (!captured.stable) {
868
1896
  throw new Error("Approval export did not stabilize");
1897
+ }
869
1898
  const sha256 = artifactSha(captured.png);
870
- const report = values.write ? await recordApproval(root, captured, { note: values.note, by: values.by }, intent, endpointRedactor(endpoint)) : { schemaVersion: 1, kind: "approval-candidate", capture: captured.capture, sha256, fingerprint: approvalFingerprint(captured.capture, sha256, intent), recorded: false };
1899
+ const report = values.write
1900
+ ? await recordApproval(root, captured, {
1901
+ note: values.note,
1902
+ by: values.by,
1903
+ }, intent, endpointRedactor(endpoint))
1904
+ : {
1905
+ schemaVersion: 1,
1906
+ kind: "approval-candidate",
1907
+ capture: captured.capture,
1908
+ sha256,
1909
+ fingerprint: approvalFingerprint(captured.capture, sha256, intent),
1910
+ recorded: false,
1911
+ };
871
1912
  output.result(report);
872
1913
  output.log(JSON.stringify(report, null, 2));
873
1914
  return 0;
874
1915
  }
875
1916
  else if (command === "spec") {
876
- const capture = await captureInspection(client, { shapeId: values.frame, page: values.page, kind: "shape" });
1917
+ const capture = await captureInspection(client, {
1918
+ shapeId: values.frame,
1919
+ page: values.page,
1920
+ kind: "shape",
1921
+ });
877
1922
  if (!capture.complete || capture.kind !== "shape-inspection") {
878
1923
  output.result(capture);
879
1924
  return 3;
880
1925
  }
881
- const mapping = values.map ? validateComponentMap(await readDesignJson(root, values.map, 1024 * 1024)) : emptyComponentMap;
882
- const spec = buildDesignSpec(capture, mapping), markdown = renderDesignSpec(spec);
883
- if (values.out)
1926
+ const mapping = values.map
1927
+ ? validateComponentMap(await readDesignJson(root, values.map, 1024 * 1024))
1928
+ : emptyComponentMap;
1929
+ const spec = buildDesignSpec(capture, mapping);
1930
+ const markdown = renderDesignSpec(spec);
1931
+ if (values.out) {
884
1932
  await writeDesignSpec(root, values.out, markdown, endpointRedactor(endpoint));
1933
+ }
885
1934
  output.result(spec);
886
- if (!values.json)
1935
+ if (!values.json) {
887
1936
  output.log(markdown);
1937
+ }
888
1938
  return 0;
889
1939
  }
890
- else if (command === "inspect" || command === "tokens" && rest[0] === "dump") {
1940
+ else if (command === "inspect" || (command === "tokens" && rest[0] === "dump")) {
891
1941
  const { query, budget } = inspectionRequest;
892
- const result = values.cursor || command === "inspect" && !values.all ? await inspectChunk(client, query) : await captureInspection(client, query, budget);
893
- const redact = endpointRedactor(endpoint), fragments = endpointRedactor(endpoint, true);
1942
+ const result = values.cursor || (command === "inspect" && !values.all)
1943
+ ? await inspectChunk(client, query)
1944
+ : await captureInspection(client, query, budget);
1945
+ const redact = endpointRedactor(endpoint);
1946
+ const fragments = endpointRedactor(endpoint, true);
894
1947
  let altered = redact(JSON.stringify(result)) !== JSON.stringify(result);
895
- const chunks = result.kind === "inspection-chunk" ? [result] : result.kind === "inspection-partial" ? result.chunks : [];
1948
+ const chunks = result.kind === "inspection-chunk"
1949
+ ? [
1950
+ result,
1951
+ ]
1952
+ : result.kind === "inspection-partial"
1953
+ ? result.chunks
1954
+ : [];
896
1955
  for (const chunk of chunks) {
897
1956
  const safe = fragments(chunk.chunk);
898
1957
  if (safe !== chunk.chunk) {
@@ -902,53 +1961,101 @@ async function main(argv, cli) {
902
1961
  }
903
1962
  if (altered) {
904
1963
  result.complete = false;
905
- Object.assign(result, { redacted: true, reason: "Output required credential redaction; source evidence is incomplete" });
906
- if ("nextCursor" in result)
1964
+ Object.assign(result, {
1965
+ redacted: true,
1966
+ reason: "Output required credential redaction; source evidence is incomplete",
1967
+ });
1968
+ if ("nextCursor" in result) {
907
1969
  result.nextCursor = null;
1970
+ }
908
1971
  }
909
1972
  output.log(JSON.stringify(result, null, 2));
910
- if (!result.complete)
1973
+ if (!result.complete) {
911
1974
  return 3;
1975
+ }
912
1976
  if (values.out) {
913
1977
  const serialized = JSON.stringify(result, null, 2) + "\n";
914
1978
  const target = await resolveProjectOutput(root, values.out);
915
- await mkdir(path.dirname(target), { recursive: true });
916
- await atomicWriteFile(target, serialized, { replace: true });
1979
+ await mkdir(path.dirname(target), {
1980
+ recursive: true,
1981
+ });
1982
+ await atomicWriteFile(target, serialized, {
1983
+ replace: true,
1984
+ });
917
1985
  }
918
1986
  return 0;
919
1987
  }
920
1988
  else if (command === "export") {
921
- const [shapeId, out] = values.name ? [undefined, rest[0]] : rest;
922
- if (!out || rest.length !== (values.name ? 1 : 2) || values.name && !values.page)
1989
+ const [shapeId, out] = values.name
1990
+ ? [
1991
+ undefined,
1992
+ rest[0],
1993
+ ]
1994
+ : rest;
1995
+ if (!out || rest.length !== (values.name ? 1 : 2) || (values.name && !values.page)) {
923
1996
  throw new Error("usage: penpair export <shapeId> <out.png>, or export --name <exact-name> --page <name> <out.png>");
924
- const exportOptions = { ...options, page: values.page, scale: values.scale ? Number(values.scale) : 1 };
925
- const result = values.name ? await exportShapeByName(client, values.name, values.page, exportOptions) : await exportShapePng(client, shapeId, exportOptions);
926
- if (!result.stable)
1997
+ }
1998
+ const exportOptions = {
1999
+ ...options,
2000
+ page: values.page,
2001
+ scale: values.scale ? Number(values.scale) : 1,
2002
+ };
2003
+ const result = values.name
2004
+ ? await exportShapeByName(client, values.name, values.page, exportOptions)
2005
+ : await exportShapePng(client, shapeId, exportOptions);
2006
+ if (!result.stable) {
927
2007
  throw new PenpairError("incomplete-output", "Export did not stabilize; previous output was preserved");
928
- await atomicWriteFile(out, result.png, { replace: true });
929
- output.log(JSON.stringify({ file: out, bytes: result.png.length, stable: result.stable, exports: result.exports, layout: result.layout, capture: result.capture }, null, 2));
2008
+ }
2009
+ await atomicWriteFile(out, result.png, {
2010
+ replace: true,
2011
+ });
2012
+ output.log(JSON.stringify({
2013
+ file: out,
2014
+ bytes: result.png.length,
2015
+ stable: result.stable,
2016
+ exports: result.exports,
2017
+ layout: result.layout,
2018
+ capture: result.capture,
2019
+ }, null, 2));
930
2020
  }
931
2021
  else if (command === "shots") {
932
- if (!values.page || rest.length)
2022
+ if (!values.page || rest.length) {
933
2023
  throw new Error("usage: penpair shots --page <name> [--prefix <prefix>] [--out design/shots]");
934
- const report = await captureShots(client, root, { page: values.page, prefix: values.prefix, out: values.out }, { ...options, scale: values.scale ? Number(values.scale) : 1 });
2024
+ }
2025
+ const report = await captureShots(client, root, {
2026
+ page: values.page,
2027
+ prefix: values.prefix,
2028
+ out: values.out,
2029
+ }, {
2030
+ ...options,
2031
+ scale: values.scale ? Number(values.scale) : 1,
2032
+ });
935
2033
  output.log(JSON.stringify(report, null, 2));
936
2034
  return report.complete ? 0 : 3;
937
2035
  }
938
2036
  else if (command === "tokens" && tokenEdit) {
939
2037
  const report = await runOperation(client, tokenEditOperation, tokenEdit, writeOptions("tokens-edit"));
940
2038
  output.log(JSON.stringify(report, null, 2));
941
- if (!values.apply)
2039
+ if (!values.apply) {
942
2040
  output.error("Nothing was written. Run again with --apply to execute this token edit.");
2041
+ }
943
2042
  return report.phase === "applied" && !report.verified ? 1 : 0;
944
2043
  }
945
2044
  else if (command === "archive") {
946
- if (!values.page || !values.name)
2045
+ if (!values.page || !values.name) {
947
2046
  throw new Error("--page and --name are required");
948
- const report = await runOperation(client, archiveOperation, { page: values.page, archiveName: values.name, keepPrefixes: list(values.keep), partPrefixes: list(values.parts), x: values.x ? Number(values.x) : 9000 }, writeOptions("archive"));
2047
+ }
2048
+ const report = await runOperation(client, archiveOperation, {
2049
+ page: values.page,
2050
+ archiveName: values.name,
2051
+ keepPrefixes: list(values.keep),
2052
+ partPrefixes: list(values.parts),
2053
+ x: values.x ? Number(values.x) : 9000,
2054
+ }, writeOptions("archive"));
949
2055
  output.log(JSON.stringify(report, null, 2));
950
- if (report.phase === "inspected")
2056
+ if (report.phase === "inspected") {
951
2057
  output.error("Nothing was written. Run again with --apply to do it.");
2058
+ }
952
2059
  return report.phase === "applied" && !report.verified ? 1 : 0;
953
2060
  }
954
2061
  else {
@@ -978,7 +2085,10 @@ async function adapterContext(root, args) {
978
2085
  redact = (text) => redactEndpoint(text, endpoint);
979
2086
  commandRedactor(endpointRedactor(endpoint));
980
2087
  assertEndpointIsInstance(endpoint.description, config);
981
- const client = guardFile(await connect(endpoint), config.file, { anyFile: args.includes("--any-file"), warn: context.log });
2088
+ const client = guardFile(await connect(endpoint), config.file, {
2089
+ anyFile: args.includes("--any-file"),
2090
+ warn: context.log,
2091
+ });
982
2092
  try {
983
2093
  return await work(client);
984
2094
  }
@@ -999,24 +2109,31 @@ async function runCliInternal(argv, options = {}) {
999
2109
  try {
1000
2110
  const root = projectRoot();
1001
2111
  if (argv[0] === "onboard") {
1002
- if (argv.slice(1).some((arg) => arg !== "--json"))
2112
+ if (argv.slice(1).some((arg) => arg !== "--json")) {
1003
2113
  throw new Error("usage: penpair onboard [--json]");
2114
+ }
1004
2115
  const report = onboardingChecklist(await loadCommandConfig(root));
1005
- if (argv.includes("--json"))
2116
+ if (argv.includes("--json")) {
1006
2117
  output.log(JSON.stringify(report, null, 2));
2118
+ }
1007
2119
  else {
1008
2120
  output.log(`Join ${report.project}: ${report.origin}`);
1009
- if (report.fileUrl)
2121
+ if (report.fileUrl) {
1010
2122
  output.log(`File: ${report.fileUrl}`);
2123
+ }
1011
2124
  report.steps.forEach((step, index) => output.log(`${index + 1}. ${step}`));
1012
2125
  report.warnings.forEach((warning) => output.log(`Note: ${warning}`));
1013
2126
  }
1014
2127
  return 0;
1015
2128
  }
1016
2129
  if (argv[0] === "runs") {
1017
- if (argv.filter((arg) => arg !== "--json").length > 2 || argv[1]?.startsWith("--") && argv[1] !== "--json")
2130
+ if (argv.filter((arg) => arg !== "--json").length > 2 ||
2131
+ (argv[1]?.startsWith("--") && argv[1] !== "--json")) {
1018
2132
  throw new Error("usage: penpair runs [run-id]");
1019
- output.log(JSON.stringify(argv[1] && argv[1] !== "--json" ? await readJournal(root, argv[1]) : await listJournals(root), null, 2));
2133
+ }
2134
+ output.log(JSON.stringify(argv[1] && argv[1] !== "--json"
2135
+ ? await readJournal(root, argv[1])
2136
+ : await listJournals(root), null, 2));
1020
2137
  return 0;
1021
2138
  }
1022
2139
  if (argv[0] === "config") {
@@ -1024,59 +2141,141 @@ async function runCliInternal(argv, options = {}) {
1024
2141
  output.log(JSON.stringify(projectConfigSchema, null, 2));
1025
2142
  return 0;
1026
2143
  }
1027
- if (argv[1] !== "check" || argv.slice(2).some((arg) => arg !== "--json"))
2144
+ if (argv[1] !== "check" || argv.slice(2).some((arg) => arg !== "--json")) {
1028
2145
  throw new ConfigError("usage: penpair config check [--json] | penpair config schema");
2146
+ }
1029
2147
  try {
1030
2148
  await loadCommandConfig(root);
1031
2149
  }
1032
2150
  catch (error) {
1033
- if (!argv.includes("--json") || !(error instanceof ConfigError))
2151
+ if (!argv.includes("--json") || !(error instanceof ConfigError)) {
1034
2152
  throw error;
2153
+ }
1035
2154
  commandFailure(error);
1036
- output.log(JSON.stringify({ valid: false, file: "penpair.config.json", error: { code: error.code, message: error.message } }));
2155
+ output.log(JSON.stringify({
2156
+ valid: false,
2157
+ file: "penpair.config.json",
2158
+ error: {
2159
+ code: error.code,
2160
+ message: error.message,
2161
+ },
2162
+ }));
1037
2163
  return 1;
1038
2164
  }
1039
- output.log(argv.includes("--json") ? JSON.stringify({ valid: true, file: "penpair.config.json" }) : "ok penpair.config.json is valid (extension settings are validated by their adapters)");
2165
+ output.log(argv.includes("--json")
2166
+ ? JSON.stringify({
2167
+ valid: true,
2168
+ file: "penpair.config.json",
2169
+ })
2170
+ : "ok penpair.config.json is valid (extension settings are validated by their adapters)");
1040
2171
  return 0;
1041
2172
  }
1042
2173
  const raw = await readRawConfig(root);
1043
- const specifiers = Array.isArray(raw.adapters) ? raw.adapters.filter((entry) => typeof entry === "string") : [];
2174
+ const specifiers = Array.isArray(raw.adapters)
2175
+ ? raw.adapters.filter((entry) => typeof entry === "string")
2176
+ : [];
1044
2177
  const [first] = argv;
1045
- const builtIn = first === undefined || first.startsWith("--") || reservedCommands.includes(first);
1046
- if (builtIn)
1047
- return await main(argv, { ...options, configuredAdapters: specifiers });
2178
+ const builtIn = first === undefined ||
2179
+ first.startsWith("--") ||
2180
+ reservedCommands.includes(first);
2181
+ if (builtIn) {
2182
+ return await main(argv, {
2183
+ ...options,
2184
+ configuredAdapters: specifiers,
2185
+ });
2186
+ }
1048
2187
  // Dynamic discovery is necessary only for a requested extension command; it executes trusted project modules.
1049
- const loaded = options.adapters ? { adapters: options.adapters, problems: [] } : await loadAdapters(root, specifiers);
1050
- const adapterCommand = loaded.adapters.flatMap((adapter) => adapter.commands).find((command) => command.name === first);
1051
- if (adapterCommand)
2188
+ const loaded = options.adapters
2189
+ ? {
2190
+ adapters: options.adapters,
2191
+ problems: [],
2192
+ }
2193
+ : await loadAdapters(root, specifiers);
2194
+ const adapterCommand = loaded.adapters
2195
+ .flatMap((adapter) => adapter.commands)
2196
+ .find((command) => command.name === first);
2197
+ if (adapterCommand) {
1052
2198
  return await runAdapterCommand(adapterCommand, root, argv.slice(1));
1053
- for (const problem of loaded.problems)
2199
+ }
2200
+ for (const problem of loaded.problems) {
1054
2201
  output.error(`warning: ${problem}`);
1055
- return await main(argv, { ...options, adapters: loaded.adapters, configuredAdapters: specifiers });
2202
+ }
2203
+ return await main(argv, {
2204
+ ...options,
2205
+ adapters: loaded.adapters,
2206
+ configuredAdapters: specifiers,
2207
+ });
1056
2208
  }
1057
2209
  catch (error) {
1058
2210
  commandFailure(error);
1059
- output.error(error instanceof PenpairError ? error.message : error instanceof ConfigError || error instanceof ConnectError || error instanceof LifecycleError || error instanceof RegistryError ? `penpair: ${error.message}` : `penpair: ${String(error?.message ?? error)}`);
2211
+ output.error(error instanceof PenpairError
2212
+ ? error.message
2213
+ : error instanceof ConfigError ||
2214
+ error instanceof ConnectError ||
2215
+ error instanceof LifecycleError ||
2216
+ error instanceof RegistryError
2217
+ ? `penpair: ${error.message}`
2218
+ : `penpair: ${String(error?.message ?? error)}`);
1060
2219
  const value = error;
1061
- return value.code === "write-uncertain" || value.outcome === "uncertain" ? 4 : value.code === "incomplete-output" ? 3 : value.code === "cancelled" ? 130 : value.code === "deadline" ? 124 : 1;
2220
+ return value.code === "write-uncertain" || value.outcome === "uncertain"
2221
+ ? 4
2222
+ : value.code === "incomplete-output"
2223
+ ? 3
2224
+ : value.code === "cancelled"
2225
+ ? 130
2226
+ : value.code === "deadline"
2227
+ ? 124
2228
+ : 1;
1062
2229
  }
1063
2230
  }
1064
2231
  export async function runCommand(argv, options = {}) {
1065
- const args = [...argv.filter((arg) => arg !== "--json"), "--json"];
2232
+ const args = [
2233
+ ...argv.filter((arg) => arg !== "--json"),
2234
+ "--json",
2235
+ ];
1066
2236
  return collectCommand(args[0] ?? "help", async () => {
1067
- if (["setup", "connect"].includes(argv[0] ?? ""))
2237
+ if ([
2238
+ "setup",
2239
+ "connect",
2240
+ ].includes(argv[0] ?? "")) {
1068
2241
  throw new Error("Interactive setup/connect require a terminal; run without --json and use doctor --json afterwards");
1069
- return commandProjects.run({ root: path.resolve(options.root ?? projectRoot()), projectEndpointOnly: options.projectEndpointOnly, pinnedConfig: options.pinnedConfig, strictFileGuard: options.strictFileGuard }, () => runCliInternal(args, options));
1070
- }, { signal: options.signal });
2242
+ }
2243
+ return commandProjects.run({
2244
+ root: path.resolve(options.root ?? projectRoot()),
2245
+ projectEndpointOnly: options.projectEndpointOnly,
2246
+ pinnedConfig: options.pinnedConfig,
2247
+ strictFileGuard: options.strictFileGuard,
2248
+ }, () => runCliInternal(args, options));
2249
+ }, {
2250
+ signal: options.signal,
2251
+ });
1071
2252
  }
1072
2253
  async function runCliBound(argv, options = {}) {
1073
2254
  if (argv[0] === "mcp") {
1074
2255
  try {
1075
- const parsed = parseCommandArgs({ args: argv.slice(1), allowPositionals: true, options: { root: { type: "string" }, "allow-writes": { type: "boolean" } } });
1076
- if (!parsed.values.root || !path.isAbsolute(parsed.values.root) || parsed.positionals.length)
2256
+ const parsed = parseCommandArgs({
2257
+ args: argv.slice(1),
2258
+ allowPositionals: true,
2259
+ options: {
2260
+ root: {
2261
+ type: "string",
2262
+ },
2263
+ "allow-writes": {
2264
+ type: "boolean",
2265
+ },
2266
+ },
2267
+ });
2268
+ if (!parsed.values.root ||
2269
+ !path.isAbsolute(parsed.values.root) ||
2270
+ parsed.positionals.length) {
1077
2271
  throw new Error("mcp requires an explicit absolute --root PROJECT; optionally --allow-writes");
2272
+ }
1078
2273
  const { serveMcp } = await import("./mcp-server.js");
1079
- await serveMcp({ root: parsed.values.root, allowWrites: parsed.values["allow-writes"] === true, cli: options });
2274
+ await serveMcp({
2275
+ root: parsed.values.root,
2276
+ allowWrites: parsed.values["allow-writes"] === true,
2277
+ cli: options,
2278
+ });
1080
2279
  return 0;
1081
2280
  }
1082
2281
  catch (error) {
@@ -1087,20 +2286,34 @@ async function runCliBound(argv, options = {}) {
1087
2286
  const controller = new AbortController();
1088
2287
  const interrupt = () => controller.abort(new CommandInterruptedError("cancelled"));
1089
2288
  let timer;
1090
- const args = [...argv];
2289
+ const args = [
2290
+ ...argv,
2291
+ ];
1091
2292
  const timeoutAt = args.indexOf("--timeout-ms");
1092
2293
  let invalidTimeout = false;
1093
2294
  if (timeoutAt !== -1) {
1094
2295
  const milliseconds = Number(args[timeoutAt + 1]);
1095
- invalidTimeout = !Number.isInteger(milliseconds) || milliseconds < 1 || milliseconds > 1_800_000 || args.indexOf("--timeout-ms", timeoutAt + 1) !== -1;
1096
- args.splice(timeoutAt, 2);
1097
- if (!invalidTimeout)
2296
+ invalidTimeout =
2297
+ !Number.isInteger(milliseconds) ||
2298
+ milliseconds < (args[0] === "doctor" ? 1000 : 1) ||
2299
+ milliseconds > (args[0] === "doctor" ? 30000 : 1_800_000) ||
2300
+ args.indexOf("--timeout-ms", timeoutAt + 1) !== -1;
2301
+ if (args[0] !== "doctor") {
2302
+ args.splice(timeoutAt, 2);
2303
+ }
2304
+ if (!invalidTimeout) {
1098
2305
  timer = setTimeout(() => controller.abort(new CommandInterruptedError("deadline")), milliseconds);
2306
+ }
1099
2307
  }
1100
2308
  const json = args.includes("--json");
1101
- if (!json && ["setup", "connect"].includes(args[0] ?? "")) {
1102
- if (timer)
2309
+ if (!json &&
2310
+ [
2311
+ "setup",
2312
+ "connect",
2313
+ ].includes(args[0] ?? "")) {
2314
+ if (timer) {
1103
2315
  clearTimeout(timer);
2316
+ }
1104
2317
  if (timeoutAt !== -1) {
1105
2318
  console.error("--timeout-ms is not supported for interactive setup/connect");
1106
2319
  return 1;
@@ -1111,28 +2324,44 @@ async function runCliBound(argv, options = {}) {
1111
2324
  process.on("SIGTERM", interrupt);
1112
2325
  try {
1113
2326
  if (invalidTimeout) {
1114
- const result = await collectCommand(args[0] ?? "help", async () => { throw new Error("--timeout-ms must be one integer from 1 to 1800000"); });
1115
- if (json)
2327
+ const result = await collectCommand(args[0] ?? "help", async () => {
2328
+ throw new Error(args[0] === "doctor"
2329
+ ? "doctor --timeout-ms must be one integer from 1000 to 30000"
2330
+ : "--timeout-ms must be one integer from 1 to 1800000");
2331
+ });
2332
+ if (json) {
1116
2333
  console.log(JSON.stringify(result, null, 2));
1117
- else
2334
+ }
2335
+ else {
1118
2336
  console.error(result.errors[0]?.message);
2337
+ }
1119
2338
  return result.exitCode;
1120
2339
  }
1121
2340
  return await withCommandSignal(controller.signal, async () => {
1122
- if (!json)
2341
+ if (!json) {
1123
2342
  return runCliInternal(args, options);
1124
- const result = await runCommand(args, { ...options, signal: controller.signal });
2343
+ }
2344
+ const result = await runCommand(args, {
2345
+ ...options,
2346
+ signal: controller.signal,
2347
+ });
1125
2348
  console.log(JSON.stringify(result, null, 2));
1126
2349
  return result.exitCode;
1127
2350
  });
1128
2351
  }
1129
2352
  finally {
1130
- if (timer)
2353
+ if (timer) {
1131
2354
  clearTimeout(timer);
2355
+ }
1132
2356
  process.removeListener("SIGINT", interrupt);
1133
2357
  process.removeListener("SIGTERM", interrupt);
1134
2358
  }
1135
2359
  }
1136
2360
  export async function runCli(argv, options = {}) {
1137
- return commandProjects.run({ root: path.resolve(options.root ?? projectRoot()), projectEndpointOnly: options.projectEndpointOnly, pinnedConfig: options.pinnedConfig, strictFileGuard: options.strictFileGuard }, () => runCliBound(argv, options));
2361
+ return commandProjects.run({
2362
+ root: path.resolve(options.root ?? projectRoot()),
2363
+ projectEndpointOnly: options.projectEndpointOnly,
2364
+ pinnedConfig: options.pinnedConfig,
2365
+ strictFileGuard: options.strictFileGuard,
2366
+ }, () => runCliBound(argv, options));
1138
2367
  }