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