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