@penpair/core 0.2.0 → 0.3.1

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