@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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { onboardingChecklist } from "./onboard.js";
|
|
2
|
+
import { snippet, runSnippet } from "./snippet.js";
|
|
3
|
+
import { installSkills } from "./skills.js";
|
|
4
|
+
import { readBaselineCoverage } from "./baseline-coverage.js";
|
|
5
|
+
import { redactEndpoint } from "./endpoint.js";
|
|
6
|
+
export const projectFileUrl = (config) => {
|
|
7
|
+
const url = onboardingChecklist(config).fileUrl;
|
|
8
|
+
if (!url) {
|
|
9
|
+
throw new Error("Pin the owner-confirmed file before opening it");
|
|
10
|
+
}
|
|
11
|
+
return url;
|
|
12
|
+
};
|
|
13
|
+
export async function openProject(config, open) {
|
|
14
|
+
const url = projectFileUrl(config);
|
|
15
|
+
await open(url);
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
export async function projectStatus(root, config, options = {}) {
|
|
19
|
+
const origin = onboardingChecklist(config).origin;
|
|
20
|
+
const lines = [];
|
|
21
|
+
const safe = (error) => (options.redact ?? redactEndpoint)(String(error.message));
|
|
22
|
+
let reachable = false;
|
|
23
|
+
try {
|
|
24
|
+
reachable = await (options.reachable ??
|
|
25
|
+
(async (origin) => (await fetch(origin, {
|
|
26
|
+
redirect: "manual",
|
|
27
|
+
signal: AbortSignal.timeout(5000),
|
|
28
|
+
})).status < 500))(origin);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
lines.push({
|
|
32
|
+
id: "deployment",
|
|
33
|
+
label: "Penpot",
|
|
34
|
+
state: reachable ? "ok" : "failed",
|
|
35
|
+
detail: reachable ? `${origin} answers` : `${origin} is unavailable`,
|
|
36
|
+
});
|
|
37
|
+
lines.push({
|
|
38
|
+
id: "file-pin",
|
|
39
|
+
label: "File pin",
|
|
40
|
+
state: config.file ? "ok" : "unknown",
|
|
41
|
+
detail: config.file ? (config.file.name ?? config.file.id) : "No owner-confirmed file pin",
|
|
42
|
+
});
|
|
43
|
+
let connection = null;
|
|
44
|
+
if (options.connection) {
|
|
45
|
+
let client;
|
|
46
|
+
try {
|
|
47
|
+
client = await options.connection();
|
|
48
|
+
const observation = await runSnippet(client, snippet("project-status", `return {ok:true,fileId:penpot.currentFile.id,file:penpot.currentFile.name,revision:penpot.currentFile.revn,pages:penpot.currentFile.pages.map(p=>({id:p.id,name:p.name})),apiVersion:typeof penpot.version==="string"?penpot.version:null,capabilities:{tokens:!!penpot.library?.local?.tokens,fonts:!!penpot.fonts,nativeLayout:typeof penpot.currentPage.root.waitForLayoutUpdate==="function"}};`, {}, "read"));
|
|
49
|
+
if (!config.file ||
|
|
50
|
+
observation.fileId !== config.file.id ||
|
|
51
|
+
!Array.isArray(observation.pages) ||
|
|
52
|
+
!Number.isInteger(observation.revision)) {
|
|
53
|
+
throw new Error("Status did not observe the configured file identity/revision");
|
|
54
|
+
}
|
|
55
|
+
const { execution: _execution, ...data } = observation;
|
|
56
|
+
connection = data;
|
|
57
|
+
lines.push({
|
|
58
|
+
id: "connection",
|
|
59
|
+
label: "Editor and plugin",
|
|
60
|
+
state: "ok",
|
|
61
|
+
detail: `Pinned file connected; ${observation.pages.length} pages`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
lines.push({
|
|
66
|
+
id: "connection",
|
|
67
|
+
label: "Editor and plugin",
|
|
68
|
+
state: "failed",
|
|
69
|
+
detail: safe(error),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await client?.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push({
|
|
78
|
+
id: "connection",
|
|
79
|
+
label: "Editor and plugin",
|
|
80
|
+
state: "unknown",
|
|
81
|
+
detail: "No resolved project connection",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const skills = await installSkills({
|
|
86
|
+
root,
|
|
87
|
+
dryRun: true,
|
|
88
|
+
});
|
|
89
|
+
lines.push({
|
|
90
|
+
id: "skills",
|
|
91
|
+
label: "Skills",
|
|
92
|
+
state: skills.wouldInstall?.length || skills.wouldLink?.length || skills.kept.length
|
|
93
|
+
? "warning"
|
|
94
|
+
: "ok",
|
|
95
|
+
detail: `${skills.wouldInstall?.length ?? 0} missing/update candidates; ${skills.kept.length} locally edited files preserved; discovery is not verified`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
lines.push({
|
|
100
|
+
id: "skills",
|
|
101
|
+
label: "Skills",
|
|
102
|
+
state: "unknown",
|
|
103
|
+
detail: safe(error),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
let coverage = null;
|
|
107
|
+
if (config.baselines) {
|
|
108
|
+
try {
|
|
109
|
+
const found = await readBaselineCoverage(root, config.baselines, config.file?.id);
|
|
110
|
+
coverage = found;
|
|
111
|
+
lines.push({
|
|
112
|
+
id: "baselines",
|
|
113
|
+
label: "Baselines",
|
|
114
|
+
state: found.designCasesComplete ? "ok" : "warning",
|
|
115
|
+
detail: found.configured
|
|
116
|
+
? "See explicit case coverage"
|
|
117
|
+
: "No complete rich coverage matrix declared",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
lines.push({
|
|
122
|
+
id: "baselines",
|
|
123
|
+
label: "Baselines",
|
|
124
|
+
state: "failed",
|
|
125
|
+
detail: safe(error),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
lines.push({
|
|
131
|
+
id: "baselines",
|
|
132
|
+
label: "Baselines",
|
|
133
|
+
state: "unknown",
|
|
134
|
+
detail: "No baseline configuration",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (options.adapterStatus) {
|
|
138
|
+
try {
|
|
139
|
+
const adapterLines = await options.adapterStatus();
|
|
140
|
+
if (!Array.isArray(adapterLines) ||
|
|
141
|
+
adapterLines.length > 100 ||
|
|
142
|
+
adapterLines.some((l) => !l ||
|
|
143
|
+
typeof l.id !== "string" ||
|
|
144
|
+
typeof l.label !== "string" ||
|
|
145
|
+
typeof l.detail !== "string" ||
|
|
146
|
+
![
|
|
147
|
+
"ok",
|
|
148
|
+
"warning",
|
|
149
|
+
"failed",
|
|
150
|
+
"unknown",
|
|
151
|
+
].includes(l.state))) {
|
|
152
|
+
throw new Error("Invalid adapter status lines");
|
|
153
|
+
}
|
|
154
|
+
lines.push(...adapterLines);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
lines.push({
|
|
158
|
+
id: "adapters",
|
|
159
|
+
label: "Adapters",
|
|
160
|
+
state: "failed",
|
|
161
|
+
detail: safe(error),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (config.adapters?.length) {
|
|
166
|
+
lines.push({
|
|
167
|
+
id: "adapters",
|
|
168
|
+
label: "Adapters",
|
|
169
|
+
state: "unknown",
|
|
170
|
+
detail: "Not loaded; --adapters explicitly executes trusted status hooks",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const complete = lines.every((l) => l.state !== "unknown");
|
|
174
|
+
const ok = complete && lines.every((l) => l.state === "ok");
|
|
175
|
+
return {
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
kind: "project-status",
|
|
178
|
+
project: config.instance,
|
|
179
|
+
origin,
|
|
180
|
+
fileUrl: config.file ? projectFileUrl(config) : null,
|
|
181
|
+
connection,
|
|
182
|
+
coverage,
|
|
183
|
+
lines,
|
|
184
|
+
complete,
|
|
185
|
+
ok,
|
|
186
|
+
visualApproval: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface ReadDefinition extends DefinitionBase {
|
|
|
27
27
|
/**
|
|
28
28
|
* A change done the safe way. `inspect` only looks and says what would change; `apply` changes once and must be safe to repeat;
|
|
29
29
|
* `readback` looks again from scratch. `verify` compares them and returns the problems; by default the readback's
|
|
30
|
-
* `
|
|
30
|
+
* `verification.expected` and `verification.actual` must contain matching nonempty observation objects.
|
|
31
31
|
*/
|
|
32
32
|
export interface WriteDefinition extends DefinitionBase {
|
|
33
33
|
kind: "write";
|
|
@@ -35,6 +35,7 @@ export interface WriteDefinition extends DefinitionBase {
|
|
|
35
35
|
apply: Body;
|
|
36
36
|
readback: Body;
|
|
37
37
|
verify?: Operation<Params>["verify"];
|
|
38
|
+
reconcile?: Operation<Params>["reconcile"];
|
|
38
39
|
}
|
|
39
40
|
export type SnippetDefinition = ReadDefinition | WriteDefinition;
|
|
40
41
|
/** Declares a project snippet. Returns the definition unchanged, so a module can `export default defineSnippet({...})` with types checked. */
|
package/dist/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runOperation, runOnPage } from "./operation.js";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { compiles, requirePage, snippet } from "./snippet.js";
|
|
3
4
|
import { archiveOperation } from "./operation.js";
|
|
4
5
|
import { audit, pages, tokensDump, tree } from "./snippets/read.js";
|
|
@@ -7,44 +8,77 @@ export const defineSnippet = (definition) => definition;
|
|
|
7
8
|
export class RegistryError extends Error {
|
|
8
9
|
}
|
|
9
10
|
const namePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
10
|
-
const paramTypes = [
|
|
11
|
-
|
|
11
|
+
const paramTypes = [
|
|
12
|
+
"string",
|
|
13
|
+
"number",
|
|
14
|
+
"boolean",
|
|
15
|
+
"string[]",
|
|
16
|
+
"number[]",
|
|
17
|
+
];
|
|
18
|
+
const bodyOf = (body, params) => typeof body === "function" ? body(params) : body;
|
|
12
19
|
const pageOf = (page) => page === undefined ? undefined : typeof page === "function" ? page : () => page;
|
|
13
20
|
/** Whatever page the definition names is handed to the snippet as CFG.page, with the guard in front of the body. */
|
|
14
21
|
function pageAware(def, params) {
|
|
15
22
|
const page = pageOf(def.page)?.(params);
|
|
16
|
-
return page === undefined
|
|
23
|
+
return page === undefined
|
|
24
|
+
? {
|
|
25
|
+
cfg: params,
|
|
26
|
+
guard: "",
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
cfg: {
|
|
30
|
+
...params,
|
|
31
|
+
page,
|
|
32
|
+
},
|
|
33
|
+
guard: `${requirePage}\n`,
|
|
34
|
+
};
|
|
17
35
|
}
|
|
18
36
|
/** Turns a definition into what the registry runs. Throws a RegistryError that says what is wrong with the definition. */
|
|
19
37
|
export function normalize(def, source) {
|
|
20
38
|
const fail = (message) => {
|
|
21
39
|
throw new RegistryError(`snippet ${JSON.stringify(def.name)}: ${message}`);
|
|
22
40
|
};
|
|
23
|
-
if (typeof def !== "object" || def === null)
|
|
41
|
+
if (typeof def !== "object" || def === null) {
|
|
24
42
|
return fail("a snippet must be an object");
|
|
25
|
-
|
|
43
|
+
}
|
|
44
|
+
if (typeof def.name !== "string" || !namePattern.test(def.name)) {
|
|
26
45
|
fail("name must be lowercase words joined by hyphens");
|
|
27
|
-
|
|
46
|
+
}
|
|
47
|
+
if (typeof def.description !== "string" || def.description.trim() === "") {
|
|
28
48
|
fail("a description is required");
|
|
49
|
+
}
|
|
29
50
|
const params = def.params ?? [];
|
|
30
51
|
const seen = new Set();
|
|
31
52
|
for (const spec of params) {
|
|
32
|
-
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(spec.name))
|
|
53
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(spec.name)) {
|
|
33
54
|
fail(`parameter name ${JSON.stringify(spec.name)} is not valid`);
|
|
34
|
-
|
|
55
|
+
}
|
|
56
|
+
if (!paramTypes.includes(spec.type)) {
|
|
35
57
|
fail(`parameter ${spec.name} has an unknown type ${JSON.stringify(spec.type)}`);
|
|
36
|
-
|
|
58
|
+
}
|
|
59
|
+
if (seen.has(spec.name)) {
|
|
37
60
|
fail(`parameter ${spec.name} is declared twice`);
|
|
61
|
+
}
|
|
38
62
|
seen.add(spec.name);
|
|
39
63
|
}
|
|
40
|
-
const common = {
|
|
64
|
+
const common = {
|
|
65
|
+
name: def.name,
|
|
66
|
+
description: def.description,
|
|
67
|
+
params,
|
|
68
|
+
source,
|
|
69
|
+
};
|
|
41
70
|
if (def.kind === "read") {
|
|
42
|
-
if (typeof def.body !== "string" && typeof def.body !== "function")
|
|
71
|
+
if (typeof def.body !== "string" && typeof def.body !== "function") {
|
|
43
72
|
fail("a read snippet needs a body");
|
|
73
|
+
}
|
|
44
74
|
return {
|
|
45
75
|
kind: "read",
|
|
46
76
|
...common,
|
|
47
|
-
...(def.page === undefined
|
|
77
|
+
...(def.page === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: {
|
|
80
|
+
page: pageOf(def.page),
|
|
81
|
+
}),
|
|
48
82
|
build(p) {
|
|
49
83
|
const { cfg, guard } = pageAware(def, p);
|
|
50
84
|
return snippet(def.name, `${guard}${bodyOf(def.body, p)}`, cfg);
|
|
@@ -52,9 +86,15 @@ export function normalize(def, source) {
|
|
|
52
86
|
};
|
|
53
87
|
}
|
|
54
88
|
if (def.kind === "write") {
|
|
55
|
-
for (const phase of [
|
|
56
|
-
|
|
89
|
+
for (const phase of [
|
|
90
|
+
"inspect",
|
|
91
|
+
"apply",
|
|
92
|
+
"readback",
|
|
93
|
+
]) {
|
|
94
|
+
if (typeof def[phase] !== "string" && typeof def[phase] !== "function") {
|
|
57
95
|
fail(`a write snippet needs ${phase}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
58
98
|
const phaseSnippet = (phase) => (p) => {
|
|
59
99
|
const { cfg, guard } = pageAware(def, p);
|
|
60
100
|
return snippet(`${def.name}-${phase}`, `${guard}${bodyOf(def[phase], p)}`, cfg);
|
|
@@ -68,8 +108,12 @@ export function normalize(def, source) {
|
|
|
68
108
|
inspect: phaseSnippet("inspect"),
|
|
69
109
|
apply: phaseSnippet("apply"),
|
|
70
110
|
readback: phaseSnippet("readback"),
|
|
71
|
-
verify: def.verify ??
|
|
72
|
-
|
|
111
|
+
verify: def.verify ?? (({ readback }) => verifyEvidence(readback)),
|
|
112
|
+
...(def.reconcile
|
|
113
|
+
? {
|
|
114
|
+
reconcile: def.reconcile,
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
73
117
|
},
|
|
74
118
|
};
|
|
75
119
|
}
|
|
@@ -78,27 +122,33 @@ export function normalize(def, source) {
|
|
|
78
122
|
/** Checks that every phase of a snippet is valid plugin code and small enough, using only the declared defaults. No connection needed. */
|
|
79
123
|
export function checkEntry(entry) {
|
|
80
124
|
const sample = {};
|
|
81
|
-
for (const spec of entry.params)
|
|
82
|
-
if (spec.default !== undefined)
|
|
125
|
+
for (const spec of entry.params) {
|
|
126
|
+
if (spec.default !== undefined) {
|
|
83
127
|
sample[spec.name] = spec.default;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
84
130
|
const problems = [];
|
|
85
131
|
const snippets = [];
|
|
86
132
|
try {
|
|
87
|
-
if (entry.kind === "read")
|
|
133
|
+
if (entry.kind === "read") {
|
|
88
134
|
snippets.push(entry.build(sample));
|
|
89
|
-
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
90
137
|
snippets.push(entry.operation.inspect(sample), entry.operation.apply(sample), entry.operation.readback(sample));
|
|
138
|
+
}
|
|
91
139
|
}
|
|
92
140
|
catch (error) {
|
|
93
141
|
// A builder may need a real parameter to build at all; then only the rest of the checks apply.
|
|
94
142
|
const message = String(error.message);
|
|
95
|
-
if (/bytes/.test(message))
|
|
143
|
+
if (/bytes/.test(message)) {
|
|
96
144
|
problems.push(message);
|
|
145
|
+
}
|
|
97
146
|
}
|
|
98
147
|
for (const s of snippets) {
|
|
99
148
|
const error = compiles(s.code);
|
|
100
|
-
if (error)
|
|
149
|
+
if (error) {
|
|
101
150
|
problems.push(`${s.name}: ${error}`);
|
|
151
|
+
}
|
|
102
152
|
}
|
|
103
153
|
return problems;
|
|
104
154
|
}
|
|
@@ -106,16 +156,22 @@ export function checkEntry(entry) {
|
|
|
106
156
|
export function coerceParams(specs, raw) {
|
|
107
157
|
const out = {};
|
|
108
158
|
const known = new Set(specs.map((s) => s.name));
|
|
109
|
-
for (const key of Object.keys(raw))
|
|
110
|
-
if (!known.has(key))
|
|
111
|
-
throw new RegistryError(`unknown parameter ${key}; known: ${[
|
|
159
|
+
for (const key of Object.keys(raw)) {
|
|
160
|
+
if (!known.has(key)) {
|
|
161
|
+
throw new RegistryError(`unknown parameter ${key}; known: ${[
|
|
162
|
+
...known,
|
|
163
|
+
].join(", ") || "none"}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
112
166
|
for (const spec of specs) {
|
|
113
167
|
const value = raw[spec.name];
|
|
114
168
|
if (value === undefined) {
|
|
115
|
-
if (spec.default !== undefined)
|
|
169
|
+
if (spec.default !== undefined) {
|
|
116
170
|
out[spec.name] = spec.default;
|
|
117
|
-
|
|
171
|
+
}
|
|
172
|
+
else if (spec.required) {
|
|
118
173
|
throw new RegistryError(`parameter ${spec.name} is required`);
|
|
174
|
+
}
|
|
119
175
|
continue;
|
|
120
176
|
}
|
|
121
177
|
const text = Array.isArray(value) ? value.join(",") : value;
|
|
@@ -125,23 +181,32 @@ export function coerceParams(specs, raw) {
|
|
|
125
181
|
break;
|
|
126
182
|
case "number": {
|
|
127
183
|
const n = Number(text);
|
|
128
|
-
if (!Number.isFinite(n))
|
|
184
|
+
if (!Number.isFinite(n)) {
|
|
129
185
|
throw new RegistryError(`parameter ${spec.name} must be a number`);
|
|
186
|
+
}
|
|
130
187
|
out[spec.name] = n;
|
|
131
188
|
break;
|
|
132
189
|
}
|
|
133
190
|
case "boolean":
|
|
134
|
-
if (![
|
|
191
|
+
if (![
|
|
192
|
+
"true",
|
|
193
|
+
"false",
|
|
194
|
+
].includes(text)) {
|
|
135
195
|
throw new RegistryError(`parameter ${spec.name} must be true or false`);
|
|
196
|
+
}
|
|
136
197
|
out[spec.name] = text === "true";
|
|
137
198
|
break;
|
|
138
199
|
case "string[]":
|
|
139
|
-
out[spec.name] = text
|
|
200
|
+
out[spec.name] = text
|
|
201
|
+
.split(",")
|
|
202
|
+
.map((v) => v.trim())
|
|
203
|
+
.filter(Boolean);
|
|
140
204
|
break;
|
|
141
205
|
case "number[]": {
|
|
142
206
|
const list = text.split(",").map((v) => Number(v.trim()));
|
|
143
|
-
if (list.some((n) => !Number.isFinite(n)))
|
|
207
|
+
if (list.some((n) => !Number.isFinite(n))) {
|
|
144
208
|
throw new RegistryError(`parameter ${spec.name} must be numbers separated by commas`);
|
|
209
|
+
}
|
|
145
210
|
out[spec.name] = list;
|
|
146
211
|
break;
|
|
147
212
|
}
|
|
@@ -149,30 +214,72 @@ export function coerceParams(specs, raw) {
|
|
|
149
214
|
}
|
|
150
215
|
return out;
|
|
151
216
|
}
|
|
217
|
+
/** An absent error list is not evidence that the intended state exists. */
|
|
218
|
+
function verifyEvidence(readback) {
|
|
219
|
+
if (readback.problems !== undefined &&
|
|
220
|
+
(!Array.isArray(readback.problems) ||
|
|
221
|
+
readback.problems.some((problem) => typeof problem !== "string"))) {
|
|
222
|
+
return [
|
|
223
|
+
"Readback problems must be a list of strings",
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(readback.problems) && readback.problems.length > 0) {
|
|
227
|
+
return readback.problems;
|
|
228
|
+
}
|
|
229
|
+
const evidence = readback.verification;
|
|
230
|
+
const observation = (value) => typeof value === "object" &&
|
|
231
|
+
value !== null &&
|
|
232
|
+
!Array.isArray(value) &&
|
|
233
|
+
Object.keys(value).length > 0;
|
|
234
|
+
if (!evidence || !observation(evidence.expected) || !observation(evidence.actual)) {
|
|
235
|
+
return [
|
|
236
|
+
"Readback needs nonempty verification.expected and verification.actual observations, or an explicit verifier",
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
return isDeepStrictEqual(evidence.expected, evidence.actual)
|
|
240
|
+
? []
|
|
241
|
+
: [
|
|
242
|
+
"Readback observations do not match the expected state",
|
|
243
|
+
];
|
|
244
|
+
}
|
|
152
245
|
export class SnippetRegistry {
|
|
153
246
|
entries = new Map();
|
|
154
247
|
register(def, source = "project") {
|
|
155
|
-
const entry = "build" in def || "operation" in def
|
|
156
|
-
|
|
248
|
+
const entry = "build" in def || "operation" in def
|
|
249
|
+
? def
|
|
250
|
+
: normalize(def, source);
|
|
251
|
+
if (this.entries.has(entry.name)) {
|
|
157
252
|
throw new RegistryError(`snippet ${entry.name} is already registered (${this.entries.get(entry.name).source})`);
|
|
253
|
+
}
|
|
158
254
|
this.entries.set(entry.name, entry);
|
|
159
255
|
return entry;
|
|
160
256
|
}
|
|
161
257
|
get(name) {
|
|
162
258
|
const entry = this.entries.get(name);
|
|
163
|
-
if (!entry)
|
|
164
|
-
throw new RegistryError(`no snippet named ${name}. Known: ${[
|
|
259
|
+
if (!entry) {
|
|
260
|
+
throw new RegistryError(`no snippet named ${name}. Known: ${[
|
|
261
|
+
...this.entries.keys(),
|
|
262
|
+
]
|
|
263
|
+
.sort()
|
|
264
|
+
.join(", ")}`);
|
|
265
|
+
}
|
|
165
266
|
return entry;
|
|
166
267
|
}
|
|
167
268
|
list() {
|
|
168
|
-
return [
|
|
269
|
+
return [
|
|
270
|
+
...this.entries.values(),
|
|
271
|
+
].sort((a, b) => a.name.localeCompare(b.name));
|
|
169
272
|
}
|
|
170
273
|
/** Runs a snippet by name. A write snippet only looks unless `apply` is set. */
|
|
171
274
|
async run(client, name, params, options = {}) {
|
|
172
275
|
const entry = this.get(name);
|
|
173
|
-
if (entry.kind === "write")
|
|
276
|
+
if (entry.kind === "write") {
|
|
174
277
|
return runOperation(client, entry.operation, params, options);
|
|
175
|
-
|
|
278
|
+
}
|
|
279
|
+
const record = await runOnPage(client, entry.build(params), entry.page?.(params), {
|
|
280
|
+
...options,
|
|
281
|
+
effect: "read",
|
|
282
|
+
});
|
|
176
283
|
return record;
|
|
177
284
|
}
|
|
178
285
|
}
|
|
@@ -182,7 +289,11 @@ const readBuiltIn = (name, description, params, build) => ({
|
|
|
182
289
|
description,
|
|
183
290
|
params,
|
|
184
291
|
source: "built-in",
|
|
185
|
-
...(params.some((p) => p.name === "page")
|
|
292
|
+
...(params.some((p) => p.name === "page")
|
|
293
|
+
? {
|
|
294
|
+
page: (p) => p.page,
|
|
295
|
+
}
|
|
296
|
+
: {}),
|
|
186
297
|
build,
|
|
187
298
|
});
|
|
188
299
|
/** The registry with everything penpair ships. Project snippets are added to it. */
|
|
@@ -190,28 +301,88 @@ export function builtInRegistry() {
|
|
|
190
301
|
const registry = new SnippetRegistry();
|
|
191
302
|
registry.register(readBuiltIn("pages", "The file, its revision, its pages and how many top-level shapes each has.", [], () => pages()), "built-in");
|
|
192
303
|
registry.register(readBuiltIn("tree", "One line per shape: type, name, size, position, layout and sizing, and the text it carries.", [
|
|
193
|
-
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
304
|
+
{
|
|
305
|
+
name: "page",
|
|
306
|
+
type: "string",
|
|
307
|
+
required: true,
|
|
308
|
+
description: "Page to read",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "depth",
|
|
312
|
+
type: "number",
|
|
313
|
+
default: 3,
|
|
314
|
+
description: "How many levels down",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "namePrefix",
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "Only top-level shapes whose name starts with this",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "limit",
|
|
323
|
+
type: "number",
|
|
324
|
+
default: 400,
|
|
325
|
+
description: "Stop after this many lines",
|
|
326
|
+
},
|
|
197
327
|
], (p) => tree(p)), "built-in");
|
|
198
328
|
registry.register(readBuiltIn("audit", "Blank text, colours and strokes not bound to a token, children sticking out of their parent, gaps off the allowed steps.", [
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
329
|
+
{
|
|
330
|
+
name: "page",
|
|
331
|
+
type: "string",
|
|
332
|
+
required: true,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "framePrefix",
|
|
336
|
+
type: "string",
|
|
337
|
+
required: true,
|
|
338
|
+
description: "Audit top-level frames whose names start with this",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "spacingSteps",
|
|
342
|
+
type: "number[]",
|
|
343
|
+
description: "Allowed gap and padding values; leave out to skip that check",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "ignoreUnbound",
|
|
347
|
+
type: "string[]",
|
|
348
|
+
description: "Names (starts) not reported as unbound",
|
|
349
|
+
},
|
|
203
350
|
], (p) => audit(p)), "built-in");
|
|
204
|
-
registry.register(readBuiltIn("tokens-dump", "
|
|
351
|
+
registry.register(readBuiltIn("tokens-dump", "Bounded typed token preview; tokens dump captures complete chunked evidence.", [], () => tokensDump()), "built-in");
|
|
205
352
|
registry.register({
|
|
206
353
|
kind: "write",
|
|
207
354
|
name: "archive",
|
|
208
355
|
description: "Nest the frames that are not part of the live work into one archive board. Frames keep names, ids and component links.",
|
|
209
356
|
params: [
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
357
|
+
{
|
|
358
|
+
name: "page",
|
|
359
|
+
type: "string",
|
|
360
|
+
required: true,
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "archiveName",
|
|
364
|
+
type: "string",
|
|
365
|
+
required: true,
|
|
366
|
+
description: "Name of the archive board",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "keepPrefixes",
|
|
370
|
+
type: "string[]",
|
|
371
|
+
default: [],
|
|
372
|
+
description: "Boards whose names start with one of these stay live",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "partPrefixes",
|
|
376
|
+
type: "string[]",
|
|
377
|
+
default: [],
|
|
378
|
+
description: "Library parts, never archived",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "x",
|
|
382
|
+
type: "number",
|
|
383
|
+
default: 9000,
|
|
384
|
+
description: "Where the archive board is placed",
|
|
385
|
+
},
|
|
215
386
|
],
|
|
216
387
|
source: "built-in",
|
|
217
388
|
operation: archiveOperation,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PenpotToolClient } from "./client.js";
|
|
2
|
+
import { type Snippet } from "./snippet.js";
|
|
3
|
+
import { type Params, type WriteEntry } from "./registry.js";
|
|
4
|
+
import { type OperationOptions } from "./operation.js";
|
|
5
|
+
export interface FileScriptOptions {
|
|
6
|
+
include?: string[];
|
|
7
|
+
page?: string;
|
|
8
|
+
params?: Params;
|
|
9
|
+
inspect?: string;
|
|
10
|
+
readback?: string;
|
|
11
|
+
virtualIncludes?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface PreparedFileScript {
|
|
14
|
+
kind: "read" | "write";
|
|
15
|
+
source: {
|
|
16
|
+
file: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
}[];
|
|
19
|
+
params: Params;
|
|
20
|
+
page?: string;
|
|
21
|
+
read?: Snippet;
|
|
22
|
+
operation?: WriteEntry["operation"];
|
|
23
|
+
}
|
|
24
|
+
/** Reads and compiles explicitly selected files without importing project modules on the host. */
|
|
25
|
+
export declare function prepareFileScript(root: string, file: string, options?: FileScriptOptions): Promise<PreparedFileScript>;
|
|
26
|
+
export declare function runFileScript(client: PenpotToolClient, prepared: PreparedFileScript, options?: OperationOptions): Promise<Record<string, unknown>>;
|