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