@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,279 @@
|
|
|
1
|
+
import { layoutPolicySource } from "./layout-policy.js";
|
|
2
|
+
/** Plain plugin-sandbox JavaScript. Inclusion defines helpers without touching the document. */
|
|
3
|
+
export const coreKitSource = String.raw `// penpair: read
|
|
4
|
+
const pp = (() => {
|
|
5
|
+
const bindings = [], styles = [], recipes = new Map();
|
|
6
|
+
let flushing = false, queuedContext = null;
|
|
7
|
+
const numeric = (value, label) => { if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
8
|
+
throw new Error(label + " must be finite and nonnegative"); return value; };
|
|
9
|
+
const activeSets = () => { const sets = penpot.library.local.tokens.sets; if (!Array.isArray(sets))
|
|
10
|
+
throw new Error("Token API unavailable"); return sets.filter(set => set.active); };
|
|
11
|
+
const context = () => JSON.stringify(activeSets().map(set => [set.id || null, set.name, set.tokens.map(token => [token.id || null, token.name, token.type, token.value])]));
|
|
12
|
+
const tok = name => { if (typeof name !== "string" || !name)
|
|
13
|
+
throw new Error("Token name required"); let found = null, count = 0; for (const set of activeSets()) {
|
|
14
|
+
const matches = [];
|
|
15
|
+
for (const token of set.tokens) {
|
|
16
|
+
if (++count > 10000)
|
|
17
|
+
throw new Error("Active token budget exceeded");
|
|
18
|
+
if (token.name === name)
|
|
19
|
+
matches.push(token);
|
|
20
|
+
}
|
|
21
|
+
if (matches.length > 1)
|
|
22
|
+
throw new Error("Ambiguous token in set: " + name);
|
|
23
|
+
if (matches.length)
|
|
24
|
+
found = matches[0];
|
|
25
|
+
} if (!found)
|
|
26
|
+
throw new Error("No active token: " + name); return found; };
|
|
27
|
+
const numericToken = (value, label) => { if (value === undefined || value === 0)
|
|
28
|
+
return { value: 0, token: null }; if (typeof value !== "string")
|
|
29
|
+
throw new Error(label + " needs a token name (only unbound zero is allowed)"); const token = tok(value); if (token.type !== "spacing")
|
|
30
|
+
throw new Error(label + " needs a spacing token"); const resolved = token.resolvedValue === undefined ? Number(token.value) : Number(token.resolvedValue); return { value: numeric(resolved, label), token: value }; };
|
|
31
|
+
const reserve = () => { const actual = context(); if (queuedContext === null)
|
|
32
|
+
queuedContext = actual;
|
|
33
|
+
else if (queuedContext !== actual)
|
|
34
|
+
throw new Error("Active token context changed while building"); };
|
|
35
|
+
const bind = (shape, name, properties) => { if (!shape || !shape.id || typeof shape.applyToken !== "function")
|
|
36
|
+
throw new Error("Bindable shape required"); if (!Array.isArray(properties) || !properties.length || properties.some(key => typeof key !== "string" || !key))
|
|
37
|
+
throw new Error("Explicit binding properties required"); tok(name); reserve(); if (bindings.length >= 10000)
|
|
38
|
+
throw new Error("Binding queue exceeds budget"); bindings.push({ shape, name, properties: [...properties] }); return shape; };
|
|
39
|
+
const checkStyle = options => { for (const key of ["bg", "stroke", "radius"])
|
|
40
|
+
if (options[key] !== undefined && (typeof options[key] !== "string" || !options[key]))
|
|
41
|
+
throw new Error(key + " needs a token name"); for (const key of ["bg", "stroke", "radius"])
|
|
42
|
+
if (options[key] !== undefined) {
|
|
43
|
+
const token = tok(options[key]);
|
|
44
|
+
if (token.type !== (key === "radius" ? "borderRadius" : "color"))
|
|
45
|
+
throw new Error("Wrong token type for " + key);
|
|
46
|
+
} numericToken(options.gap, "gap"); const pad = options.pad === undefined ? [0, 0, 0, 0] : options.pad; if (!Array.isArray(pad) || pad.length !== 4)
|
|
47
|
+
throw new Error("Padding requires four token names or zeros"); pad.forEach(value => numericToken(value, "padding")); };
|
|
48
|
+
const style = (shape, options = {}) => {
|
|
49
|
+
checkStyle(options);
|
|
50
|
+
if ((options.gap !== undefined || options.pad !== undefined) && !shape.flex)
|
|
51
|
+
throw new Error("Spacing style requires a flex board");
|
|
52
|
+
for (const name of [options.bg, options.stroke, options.radius].filter(Boolean))
|
|
53
|
+
tok(name);
|
|
54
|
+
const gap = numericToken(options.gap, "gap"), pad = options.pad === undefined ? [0, 0, 0, 0] : options.pad;
|
|
55
|
+
if (!Array.isArray(pad) || pad.length !== 4)
|
|
56
|
+
throw new Error("Padding requires four token names or zeros");
|
|
57
|
+
const pads = pad.map(value => numericToken(value, "padding"));
|
|
58
|
+
reserve();
|
|
59
|
+
if (styles.length >= 5000)
|
|
60
|
+
throw new Error("Style queue exceeds budget");
|
|
61
|
+
styles.push(() => {
|
|
62
|
+
if (shape.flex) {
|
|
63
|
+
if (options.gap !== undefined) {
|
|
64
|
+
shape.flex.rowGap = gap.value;
|
|
65
|
+
shape.flex.columnGap = gap.value;
|
|
66
|
+
}
|
|
67
|
+
if (options.pad !== undefined) {
|
|
68
|
+
shape.flex.paddingType = "multiple";
|
|
69
|
+
["topPadding", "rightPadding", "bottomPadding", "leftPadding"].forEach((key, index) => { shape.flex[key] = pads[index].value; });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (options.stroke) {
|
|
73
|
+
shape.strokes = [{ strokeColor: "#000000", strokeWidth: options.strokeWidth === undefined ? 1 : numeric(options.strokeWidth, "stroke width"), strokeAlignment: "inner" }];
|
|
74
|
+
bind(shape, options.stroke, ["strokeColor"]);
|
|
75
|
+
}
|
|
76
|
+
if (options.bg)
|
|
77
|
+
bind(shape, options.bg, ["fill"]);
|
|
78
|
+
if (options.radius)
|
|
79
|
+
bind(shape, options.radius, ["borderRadiusTopLeft", "borderRadiusTopRight", "borderRadiusBottomRight", "borderRadiusBottomLeft"]);
|
|
80
|
+
if (gap.token)
|
|
81
|
+
bind(shape, gap.token, ["rowGap", "columnGap"]);
|
|
82
|
+
pads.forEach((entry, index) => { if (entry.token)
|
|
83
|
+
bind(shape, entry.token, [["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"][index]]); });
|
|
84
|
+
});
|
|
85
|
+
return shape;
|
|
86
|
+
};
|
|
87
|
+
const box = (parent, options = {}) => {
|
|
88
|
+
checkStyle(options);
|
|
89
|
+
const gap = numericToken(options.gap, "gap");
|
|
90
|
+
if (options.w !== undefined)
|
|
91
|
+
numeric(options.w, "width");
|
|
92
|
+
if (options.h !== undefined)
|
|
93
|
+
numeric(options.h, "height");
|
|
94
|
+
const board = penpot.createBoard();
|
|
95
|
+
if (parent)
|
|
96
|
+
parent.appendChild(board);
|
|
97
|
+
board.name = options.name || "Board";
|
|
98
|
+
board.fills = [];
|
|
99
|
+
board.clipContent = !!options.clip;
|
|
100
|
+
board.addFlexLayout();
|
|
101
|
+
board.flex.dir = options.dir || "column";
|
|
102
|
+
board.flex.wrap = options.wrap ? "wrap" : "nowrap";
|
|
103
|
+
board.flex.alignItems = options.align || "start";
|
|
104
|
+
board.flex.justifyContent = options.justify || "start";
|
|
105
|
+
board.flex.rowGap = gap.value;
|
|
106
|
+
board.flex.columnGap = gap.value;
|
|
107
|
+
if (options.w !== undefined || options.h !== undefined)
|
|
108
|
+
board.resize(options.w === undefined ? board.width : options.w, options.h === undefined ? board.height : options.h);
|
|
109
|
+
board.flex.horizontalSizing = options.w === undefined ? "auto" : "fix";
|
|
110
|
+
board.flex.verticalSizing = options.h === undefined ? "auto" : "fix";
|
|
111
|
+
return style(board, { ...options, gap: options.gap === undefined ? 0 : options.gap, pad: options.pad === undefined ? [0, 0, 0, 0] : options.pad });
|
|
112
|
+
};
|
|
113
|
+
const text = (parent, value, options = {}) => {
|
|
114
|
+
if (typeof value !== "string" || value.length === 0)
|
|
115
|
+
throw new Error("Text needs nonempty characters; use an explicit space for blank text");
|
|
116
|
+
const typography = tok(options.typo);
|
|
117
|
+
if (typography.type !== "typography" || tok(options.color).type !== "color")
|
|
118
|
+
throw new Error("Text needs typography and color token types");
|
|
119
|
+
let resolved = typography.resolvedValue;
|
|
120
|
+
if (Array.isArray(resolved) && resolved.length === 1)
|
|
121
|
+
resolved = resolved[0];
|
|
122
|
+
const data = resolved && typeof resolved === "object" && !Array.isArray(resolved) ? resolved : typography.value;
|
|
123
|
+
const families = options.font || (data && typeof data === "object" && (data.fontFamilies || data.fontFamily));
|
|
124
|
+
const family = Array.isArray(families) ? families[0] : families;
|
|
125
|
+
if (typeof family !== "string" || !family || family.includes("{"))
|
|
126
|
+
throw new Error("An explicit or resolved font family is required");
|
|
127
|
+
const font = penpot.fonts.findByName(family);
|
|
128
|
+
if (!font || !Array.isArray(font.variants) || typeof font.applyToText !== "function")
|
|
129
|
+
throw new Error("Font unavailable: " + family);
|
|
130
|
+
const weight = String(options.weight || (data && typeof data === "object" && (data.fontWeight || data.fontWeights)) || "400"), fontStyle = options.fontStyle || "normal";
|
|
131
|
+
const variants = font.variants.filter(variant => String(variant.fontWeight) === weight && (variant.fontStyle || "normal") === fontStyle);
|
|
132
|
+
if (variants.length !== 1)
|
|
133
|
+
throw new Error("Font weight/style is missing or ambiguous: " + family + " " + weight);
|
|
134
|
+
const shape = penpot.createText(value);
|
|
135
|
+
if (parent)
|
|
136
|
+
parent.appendChild(shape);
|
|
137
|
+
shape.name = options.name || value.slice(0, 80);
|
|
138
|
+
shape.growType = options.wrap ? "auto-height" : options.grow || "auto-width";
|
|
139
|
+
if (options.align)
|
|
140
|
+
shape.align = options.align;
|
|
141
|
+
font.applyToText(shape, variants[0]);
|
|
142
|
+
if (options.wrap || options.fill) {
|
|
143
|
+
if (!shape.layoutChild)
|
|
144
|
+
throw new Error("Text layout child unavailable");
|
|
145
|
+
shape.layoutChild.horizontalSizing = "fill";
|
|
146
|
+
}
|
|
147
|
+
bind(shape, options.typo, ["typography"]);
|
|
148
|
+
bind(shape, options.color, ["fill"]);
|
|
149
|
+
return shape;
|
|
150
|
+
};
|
|
151
|
+
const glyphs = { chevron: { path: "M9 6 L15 12 L9 18", bounds: [9, 6, 6, 12] }, check: { path: "M4 12 L10 18 L20 6", bounds: [4, 6, 16, 12] }, alert: { path: "M12 3 L22 20 H2 Z M12 10 V14", bounds: [2, 3, 20, 17] }, calendar: { path: "M4 6 H20 V20 H4 Z M4 10 H20 M8 3 V7 M16 3 V7", bounds: [4, 3, 16, 17] } };
|
|
152
|
+
const icon = (parent, kind, color, size = 20) => {
|
|
153
|
+
numeric(size, "icon size");
|
|
154
|
+
if (size === 0)
|
|
155
|
+
throw new Error("Icon size must be positive");
|
|
156
|
+
tok(color);
|
|
157
|
+
const glyph = typeof kind === "string" ? glyphs[kind] : kind;
|
|
158
|
+
if (!glyph || typeof glyph.path !== "string" || !Array.isArray(glyph.bounds) || glyph.bounds.length !== 4 || !glyph.bounds.every(Number.isFinite) || glyph.bounds[2] <= 0 || glyph.bounds[3] <= 0)
|
|
159
|
+
throw new Error("Icon requires known path and positive bounds");
|
|
160
|
+
const board = penpot.createBoard();
|
|
161
|
+
if (parent)
|
|
162
|
+
parent.appendChild(board);
|
|
163
|
+
board.name = "Icon";
|
|
164
|
+
board.fills = [];
|
|
165
|
+
board.clipContent = false;
|
|
166
|
+
board.resize(size, size);
|
|
167
|
+
const path = penpot.createPath();
|
|
168
|
+
board.appendChild(path);
|
|
169
|
+
path.name = "Glyph";
|
|
170
|
+
path.content = glyph.path;
|
|
171
|
+
path.fills = [];
|
|
172
|
+
path.strokes = [{ strokeColor: "#000000", strokeWidth: Math.max(1, 2 * size / 24), strokeCapStyle: "round", strokeJoinStyle: "round" }];
|
|
173
|
+
const [x, y, width, height] = glyph.bounds, scale = size / 24;
|
|
174
|
+
path.proportionLock = true;
|
|
175
|
+
path.resize(width * scale, height * scale);
|
|
176
|
+
path.x = board.x + x * scale;
|
|
177
|
+
path.y = board.y + y * scale;
|
|
178
|
+
bind(path, color, ["strokeColor"]);
|
|
179
|
+
return board;
|
|
180
|
+
};
|
|
181
|
+
const dot = (parent, color, size = 8) => { numeric(size, "dot size"); if (!size)
|
|
182
|
+
throw new Error("Positive dot size required"); tok(color); const shape = penpot.createEllipse(); if (parent)
|
|
183
|
+
parent.appendChild(shape); shape.name = "Dot"; shape.resize(size, size); bind(shape, color, ["fill"]); return shape; };
|
|
184
|
+
const rule = (parent, color) => { tok(color); const shape = penpot.createRectangle(); if (parent)
|
|
185
|
+
parent.appendChild(shape); shape.name = "Rule"; shape.resize(10, 1); if (!shape.layoutChild)
|
|
186
|
+
throw new Error("Rule layout child unavailable"); shape.layoutChild.horizontalSizing = "fill"; bind(shape, color, ["fill"]); return shape; };
|
|
187
|
+
const descendants = root => { const out = [], stack = [root], seen = new Set(); while (stack.length) {
|
|
188
|
+
if (out.length >= 20000)
|
|
189
|
+
throw new Error("Component traversal exceeds budget");
|
|
190
|
+
const shape = stack.pop();
|
|
191
|
+
if (!shape.id || seen.has(shape.id))
|
|
192
|
+
throw new Error("Invalid component identity");
|
|
193
|
+
seen.add(shape.id);
|
|
194
|
+
out.push(shape);
|
|
195
|
+
for (const child of shape.children || [])
|
|
196
|
+
stack.push(child);
|
|
197
|
+
} return out; };
|
|
198
|
+
const inst = (parent, path, name, texts = {}, library = penpot.library.local) => {
|
|
199
|
+
const components = library.components.filter(component => component.path === path && component.name === name);
|
|
200
|
+
if (components.length !== 1)
|
|
201
|
+
throw new Error("Component missing or ambiguous: " + path + "/" + name);
|
|
202
|
+
const entries = Object.entries(texts);
|
|
203
|
+
if (entries.some(([, value]) => typeof value !== "string" || !value))
|
|
204
|
+
throw new Error("Text overrides must be nonempty strings");
|
|
205
|
+
const validate = root => { const rows = descendants(root); return entries.map(([key, value]) => { const matches = rows.filter(shape => shape.type === "text" && shape.name === key); if (matches.length !== 1)
|
|
206
|
+
throw new Error("Override target missing or ambiguous: " + key); return { shape: matches[0], value }; }); };
|
|
207
|
+
if (typeof components[0].mainInstance === "function")
|
|
208
|
+
validate(components[0].mainInstance());
|
|
209
|
+
const shape = components[0].instance();
|
|
210
|
+
const overrides = validate(shape);
|
|
211
|
+
if (parent)
|
|
212
|
+
parent.appendChild(shape);
|
|
213
|
+
for (const override of overrides)
|
|
214
|
+
override.shape.characters = override.value;
|
|
215
|
+
return shape;
|
|
216
|
+
};
|
|
217
|
+
const fill = shape => { if (!shape.layoutChild)
|
|
218
|
+
throw new Error("Layout child unavailable"); shape.layoutChild.horizontalSizing = "fill"; return shape; };
|
|
219
|
+
const fillV = shape => { if (!shape.layoutChild)
|
|
220
|
+
throw new Error("Layout child unavailable"); shape.layoutChild.verticalSizing = "fill"; return shape; };
|
|
221
|
+
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
222
|
+
${layoutPolicySource}
|
|
223
|
+
const settle = async (root = penpot.currentPage.root, options = {}) => {
|
|
224
|
+
const fileId = penpot.currentFile.id, pageId = penpot.currentPage.id;
|
|
225
|
+
try { return await settleLayout(async nativeMs => {
|
|
226
|
+
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) throw new Error("Layout target changed between samples");
|
|
227
|
+
return observeLayout(penpot, root, { nativeMs, maxNodes: options.maxNodes });
|
|
228
|
+
}, { timeoutMs: options.timeoutMs === undefined ? 10000 : options.timeoutMs, intervalMs: options.intervalMs === undefined ? 100 : options.intervalMs, strategy: options.strategy === undefined ? "auto" : options.strategy, sleep: wait }); }
|
|
229
|
+
catch (error) { return { stable: false, method: "unavailable", samples: 0, reason: String(error && error.message || error) }; }
|
|
230
|
+
};
|
|
231
|
+
const flush = async (options = {}) => {
|
|
232
|
+
if (flushing)
|
|
233
|
+
throw new Error("Concurrent flush is unsupported");
|
|
234
|
+
flushing = true;
|
|
235
|
+
const timeout = options.timeoutMs === undefined ? 10000 : options.timeoutMs, started = Date.now();
|
|
236
|
+
let applied = 0;
|
|
237
|
+
const remaining = () => { const value = timeout - (Date.now() - started); if (value < 1)
|
|
238
|
+
throw new Error("Flush deadline exceeded"); return value; };
|
|
239
|
+
try {
|
|
240
|
+
if (queuedContext !== null && context() !== queuedContext)
|
|
241
|
+
throw new Error("Active token context changed before flush");
|
|
242
|
+
let layout = await settle(options.root || penpot.currentPage.root, { timeoutMs: remaining(), strategy: options.strategy });
|
|
243
|
+
if (!layout.stable)
|
|
244
|
+
throw new Error(layout.reason || "Layout did not settle");
|
|
245
|
+
if (queuedContext !== null && context() !== queuedContext)
|
|
246
|
+
throw new Error("Active token context changed during layout");
|
|
247
|
+
while (styles.length) {
|
|
248
|
+
remaining();
|
|
249
|
+
styles[0]();
|
|
250
|
+
styles.shift();
|
|
251
|
+
}
|
|
252
|
+
while (bindings.length) {
|
|
253
|
+
remaining();
|
|
254
|
+
if (context() !== queuedContext)
|
|
255
|
+
throw new Error("Token context changed during binding");
|
|
256
|
+
const binding = bindings[0];
|
|
257
|
+
await binding.shape.applyToken(tok(binding.name), binding.properties);
|
|
258
|
+
bindings.shift();
|
|
259
|
+
applied++;
|
|
260
|
+
}
|
|
261
|
+
layout = await settle(options.root || penpot.currentPage.root, { timeoutMs: remaining(), strategy: options.strategy });
|
|
262
|
+
if (!layout.stable)
|
|
263
|
+
throw new Error(layout.reason || "Layout did not settle");
|
|
264
|
+
queuedContext = null;
|
|
265
|
+
return { applied, layout };
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
flushing = false;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const defineRecipe = (name, build, description = "") => { if (!/^[a-z][a-z0-9-]*$/.test(name) || typeof build !== "function" || recipes.has(name))
|
|
272
|
+
throw new Error("Invalid or duplicate recipe: " + name); recipes.set(name, { build, description }); return build; };
|
|
273
|
+
const recipe = (name, parent, props) => { const found = recipes.get(name); if (!found)
|
|
274
|
+
throw new Error("Unknown recipe: " + name); return found.build(parent, props || {}); };
|
|
275
|
+
defineRecipe("hello-card", (parent, props) => { if (!props.box || !props.text)
|
|
276
|
+
throw new Error("hello-card needs explicit box/text token styles"); tok(props.text.typo); tok(props.text.color); const board = box(parent, props.box); text(board, props.label || "Hello, card", props.text); return board; }, "A neutral text-in-board example, with explicit token styles");
|
|
277
|
+
return { tok, bind, box, text, icon, dot, rule, inst, style, fill, fillV, settle, flush, defineRecipe, recipe, recipes: () => [...recipes].map(([name, value]) => ({ name, description: value.description })) };
|
|
278
|
+
})();
|
|
279
|
+
`;
|
package/dist/kit.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface RecipeInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BuildKit {
|
|
6
|
+
include: string;
|
|
7
|
+
source: string;
|
|
8
|
+
recipes: RecipeInfo[];
|
|
9
|
+
requires?: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const coreKit: BuildKit;
|
|
12
|
+
/** Plain JavaScript to concatenate into a reviewed plugin body; defines the pp namespace. */
|
|
13
|
+
export declare const kit: () => string;
|
|
14
|
+
export declare function checkKits(kits: BuildKit[]): {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
bytes: number;
|
|
17
|
+
sources: {
|
|
18
|
+
include: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}[];
|
|
22
|
+
problems: string[];
|
|
23
|
+
};
|
|
24
|
+
/** Adapter imports happen only when an explicitly requested alias cannot be supplied by the core. */
|
|
25
|
+
export declare function loadKits(root: string, includes?: string[]): Promise<BuildKit[]>;
|
package/dist/kit.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { coreKitSource } from "./kit-source.js";
|
|
3
|
+
import { compiles, snippet } from "./snippet.js";
|
|
4
|
+
import { loadAdapters } from "./adapters.js";
|
|
5
|
+
import { loadOptionalConfig } from "./instance/config.js";
|
|
6
|
+
export const coreKit = {
|
|
7
|
+
include: "@kit",
|
|
8
|
+
source: coreKitSource,
|
|
9
|
+
recipes: [
|
|
10
|
+
{
|
|
11
|
+
name: "hello-card",
|
|
12
|
+
description: "A neutral text-in-board example, with explicit token styles",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
/** Plain JavaScript to concatenate into a reviewed plugin body; defines the pp namespace. */
|
|
17
|
+
export const kit = () => coreKitSource;
|
|
18
|
+
export function checkKits(kits) {
|
|
19
|
+
const problems = [];
|
|
20
|
+
const names = new Set();
|
|
21
|
+
const recipeNames = new Set();
|
|
22
|
+
const sources = kits.map((value, index) => {
|
|
23
|
+
if (!value ||
|
|
24
|
+
typeof value !== "object" ||
|
|
25
|
+
typeof value.include !== "string" ||
|
|
26
|
+
typeof value.source !== "string") {
|
|
27
|
+
problems.push("Malformed kit at index " + index);
|
|
28
|
+
return {
|
|
29
|
+
include: "invalid",
|
|
30
|
+
bytes: 0,
|
|
31
|
+
sha256: "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (!/^@[a-z][a-z0-9-]*$/.test(value.include) || names.has(value.include)) {
|
|
35
|
+
problems.push("Invalid or duplicate kit include: " + value.include);
|
|
36
|
+
}
|
|
37
|
+
if (value.requires !== undefined &&
|
|
38
|
+
(!Array.isArray(value.requires) || value.requires.some((name) => typeof name !== "string"))) {
|
|
39
|
+
problems.push("Invalid kit dependencies: " + value.include);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const dependency of value.requires ?? []) {
|
|
43
|
+
if (!names.has(dependency)) {
|
|
44
|
+
problems.push(`${value.include} requires earlier include ${dependency}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
names.add(value.include);
|
|
49
|
+
if (!value.source.startsWith("// penpair: read\n")) {
|
|
50
|
+
problems.push(`${value.include} must declare read-only top-level inclusion`);
|
|
51
|
+
}
|
|
52
|
+
const bytes = Buffer.byteLength(value.source);
|
|
53
|
+
if (bytes > 32768) {
|
|
54
|
+
problems.push(`${value.include} exceeds the 32 KiB source budget`);
|
|
55
|
+
}
|
|
56
|
+
const error = compiles(value.source);
|
|
57
|
+
if (error) {
|
|
58
|
+
problems.push(`${value.include}: ${error}`);
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(value.recipes)) {
|
|
61
|
+
problems.push(`${value.include} has invalid recipe metadata`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
for (const recipe of value.recipes) {
|
|
65
|
+
if (!recipe ||
|
|
66
|
+
typeof recipe.name !== "string" ||
|
|
67
|
+
!/^[a-z][a-z0-9-]*$/.test(recipe.name) ||
|
|
68
|
+
typeof recipe.description !== "string") {
|
|
69
|
+
problems.push(`${value.include} has invalid recipe metadata`);
|
|
70
|
+
}
|
|
71
|
+
else if (recipeNames.has(recipe.name)) {
|
|
72
|
+
problems.push("Duplicate recipe: " + recipe.name);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
recipeNames.add(recipe.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
include: value.include,
|
|
81
|
+
bytes,
|
|
82
|
+
sha256: createHash("sha256").update(value.source).digest("hex"),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
let bytes = sources.reduce((sum, source) => sum + source.bytes, 0);
|
|
86
|
+
try {
|
|
87
|
+
const combined = snippet("kit-check", kits
|
|
88
|
+
.filter((value) => value && typeof value.source === "string")
|
|
89
|
+
.map((value) => value.source)
|
|
90
|
+
.join("\n") + "\nreturn {ok:true};");
|
|
91
|
+
bytes = Buffer.byteLength(combined.code);
|
|
92
|
+
const error = compiles(combined.code);
|
|
93
|
+
if (error) {
|
|
94
|
+
problems.push(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
problems.push(String(error.message));
|
|
99
|
+
}
|
|
100
|
+
if (bytes > 49152) {
|
|
101
|
+
problems.push("Combined kit exceeds the 48 KiB budget reserved before project code");
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
valid: problems.length === 0,
|
|
105
|
+
bytes,
|
|
106
|
+
sources,
|
|
107
|
+
problems,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Adapter imports happen only when an explicitly requested alias cannot be supplied by the core. */
|
|
111
|
+
export async function loadKits(root, includes = [
|
|
112
|
+
"@kit",
|
|
113
|
+
]) {
|
|
114
|
+
const config = await loadOptionalConfig(root);
|
|
115
|
+
if (includes.length > 20 || new Set(includes).size !== includes.length) {
|
|
116
|
+
throw new Error("Use at most 20 distinct kit includes");
|
|
117
|
+
}
|
|
118
|
+
const available = new Map([
|
|
119
|
+
[
|
|
120
|
+
"@kit",
|
|
121
|
+
coreKit,
|
|
122
|
+
],
|
|
123
|
+
]);
|
|
124
|
+
if (includes.some((name) => name !== "@kit")) {
|
|
125
|
+
const loaded = await loadAdapters(root, config?.adapters);
|
|
126
|
+
for (const adapter of loaded.adapters) {
|
|
127
|
+
if (adapter.kits !== undefined && !Array.isArray(adapter.kits)) {
|
|
128
|
+
throw new Error("Adapter kits must be an array: " + adapter.name);
|
|
129
|
+
}
|
|
130
|
+
for (const value of adapter.kits ?? []) {
|
|
131
|
+
if (!value || typeof value.include !== "string") {
|
|
132
|
+
throw new Error("Malformed adapter kit: " + adapter.name);
|
|
133
|
+
}
|
|
134
|
+
if (available.has(value.include)) {
|
|
135
|
+
throw new Error("Ambiguous kit alias: " + value.include);
|
|
136
|
+
}
|
|
137
|
+
available.set(value.include, value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const name of includes) {
|
|
141
|
+
if (!available.has(name)) {
|
|
142
|
+
throw new Error(`Unknown kit ${name}${loaded.problems.length ? ": " + loaded.problems.join("; ") : "; configure an adapter that supplies it"}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const selected = includes.map((name) => {
|
|
147
|
+
const value = available.get(name);
|
|
148
|
+
if (!value) {
|
|
149
|
+
throw new Error("Unknown kit: " + name);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
});
|
|
153
|
+
const report = checkKits(selected);
|
|
154
|
+
if (!report.valid) {
|
|
155
|
+
throw new Error(report.problems.join("; "));
|
|
156
|
+
}
|
|
157
|
+
return selected;
|
|
158
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Trusted, dependency-free sandbox policy shared by the host observer and shipped kit. */
|
|
2
|
+
export const layoutPolicySource = String.raw `
|
|
3
|
+
const observeLayout = async (penpot, root, options) => {
|
|
4
|
+
if (!Number.isInteger(options.maxNodes === undefined ? 10000 : options.maxNodes) || (options.maxNodes !== undefined && (options.maxNodes < 1 || options.maxNodes > 10000))) throw new Error("Invalid settling node budget");
|
|
5
|
+
const fileId = penpot.currentFile.id, pageId = penpot.currentPage.id;
|
|
6
|
+
const validTarget = () => {
|
|
7
|
+
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) throw new Error("Layout target changed while waiting");
|
|
8
|
+
if (root.id !== penpot.currentPage.root.id && typeof penpot.currentPage.getShapeById === "function" && !penpot.currentPage.getShapeById(root.id)) throw new Error("Layout target no longer exists");
|
|
9
|
+
};
|
|
10
|
+
if (!root) throw new Error("layout target does not exist");
|
|
11
|
+
let nativeOutcome = "skipped", nativeError;
|
|
12
|
+
const target = typeof root.waitForLayoutUpdate === "function" ? root : penpot;
|
|
13
|
+
if (options.nativeMs > 0) {
|
|
14
|
+
nativeOutcome = "unsupported";
|
|
15
|
+
if (typeof target.waitForLayoutUpdate === "function") {
|
|
16
|
+
let timer;
|
|
17
|
+
try {
|
|
18
|
+
await Promise.race([target.waitForLayoutUpdate(options.nativeMs), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Native layout attempt timed out")), options.nativeMs); })]);
|
|
19
|
+
nativeOutcome = "completed";
|
|
20
|
+
} catch (error) { nativeOutcome = "failed"; nativeError = String(error && error.message || error); }
|
|
21
|
+
finally { clearTimeout(timer); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
validTarget();
|
|
25
|
+
// Keep hierarchy and sibling order as well as geometry; repeated IDs include cycles.
|
|
26
|
+
const geometry = [], stack = [[root, null, 0]], seen = new Set();
|
|
27
|
+
while (stack.length) {
|
|
28
|
+
if (geometry.length >= (options.maxNodes || 10000)) throw new Error("Layout sample exceeded node budget");
|
|
29
|
+
const [shape, parent, index] = stack.pop();
|
|
30
|
+
if (!shape || typeof shape.id !== "string" || !shape.id || seen.has(shape.id) || ![shape.x, shape.y, shape.width, shape.height].every(Number.isFinite)) throw new Error("Malformed layout geometry or duplicate IDs");
|
|
31
|
+
seen.add(shape.id);
|
|
32
|
+
geometry.push([shape.id, shape.x, shape.y, shape.width, shape.height, parent, index]);
|
|
33
|
+
const children = shape.children || [];
|
|
34
|
+
if (!Array.isArray(children)) throw new Error("Malformed layout hierarchy");
|
|
35
|
+
if (geometry.length + stack.length + children.length > (options.maxNodes || 10000)) throw new Error("Layout sample exceeded node budget");
|
|
36
|
+
for (let index = children.length - 1; index >= 0; index--) stack.push([children[index], shape.id, index]);
|
|
37
|
+
}
|
|
38
|
+
validTarget();
|
|
39
|
+
return { ok: true, fileId, pageId, native: nativeOutcome === "completed", nativeOutcome, nativeError, geometry, truncated: false };
|
|
40
|
+
};
|
|
41
|
+
const settleLayout = async (observe, options) => {
|
|
42
|
+
const { timeoutMs, intervalMs, strategy = "auto", sleep } = options;
|
|
43
|
+
if (!["auto", "native", "geometry"].includes(strategy) || !Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000 || !Number.isFinite(intervalMs) || intervalMs <= 0 || Math.ceil(timeoutMs / intervalMs) + 1 > 1000) throw new Error("Invalid layout strategy or settling budgets");
|
|
44
|
+
const started = Date.now(), deadline = started + timeoutMs;
|
|
45
|
+
let samples = 0, nativeOutcome = "skipped", fallbackReason, identity, previous, equal = 0;
|
|
46
|
+
const result = (stable, method, reason) => ({ stable, method, samples, strategy, nativeOutcome, ...(fallbackReason ? { fallbackReason } : {}), elapsedMs: Date.now() - started, ...(reason ? { reason } : {}) });
|
|
47
|
+
for (let count = 0; count < Math.ceil(timeoutMs / intervalMs) + 1; count++) {
|
|
48
|
+
const remaining = deadline - Date.now();
|
|
49
|
+
if (remaining <= 0) return result(false, strategy === "native" ? "native" : "geometry", "Layout observation exceeded its deadline");
|
|
50
|
+
const nativeMs = count === 0 && strategy !== "geometry" ? strategy === "native" ? remaining : Math.max(1, Math.min(500, Math.floor(remaining / 4))) : 0;
|
|
51
|
+
const state = await observe(nativeMs, remaining);
|
|
52
|
+
samples++;
|
|
53
|
+
if (!state || state.ok !== true || typeof state.fileId !== "string" || !state.fileId || typeof state.pageId !== "string" || !state.pageId) throw new Error("Layout returned no target identity evidence");
|
|
54
|
+
if (typeof state.native !== "boolean" || typeof state.truncated !== "boolean" || state.nativeOutcome !== undefined && !["skipped", "unsupported", "completed", "failed"].includes(state.nativeOutcome)) throw new Error("Malformed layout attempt evidence");
|
|
55
|
+
const nextIdentity = JSON.stringify([state.fileId, state.pageId, state.geometry?.[0]?.[0]]);
|
|
56
|
+
if (identity && identity !== nextIdentity) throw new Error("Layout target changed between samples");
|
|
57
|
+
identity = nextIdentity;
|
|
58
|
+
if (state.truncated || !Array.isArray(state.geometry) || !state.geometry.length || state.geometry.length > 10000) throw new Error("Incomplete layout geometry evidence");
|
|
59
|
+
const ids = new Set(), childIndices = new Map();
|
|
60
|
+
for (const row of state.geometry) {
|
|
61
|
+
if (!Array.isArray(row) || row.length !== 7 || typeof row[0] !== "string" || !row[0] || ids.has(row[0]) || !row.slice(1,5).every(value => typeof value === "number" && Number.isFinite(value)) || ((row[5] !== null && !ids.has(row[5])) || !Number.isInteger(row[6]) || row[6] < 0)) throw new Error("Layout API returned no geometry evidence or malformed hierarchy");
|
|
62
|
+
if (!ids.size ? row[5] !== null || row[6] !== 0 : row[5] === null || typeof row[5] !== "string") throw new Error("Malformed layout root hierarchy");
|
|
63
|
+
if (row[5] !== null) {
|
|
64
|
+
const expectedIndex = childIndices.get(row[5]) || 0;
|
|
65
|
+
if (row[6] !== expectedIndex) throw new Error("Malformed layout sibling order");
|
|
66
|
+
childIndices.set(row[5], expectedIndex + 1);
|
|
67
|
+
}
|
|
68
|
+
ids.add(row[0]);
|
|
69
|
+
}
|
|
70
|
+
if (count === 0) {
|
|
71
|
+
nativeOutcome = state.nativeOutcome || (state.native ? state.error ? "failed" : "completed" : strategy === "geometry" ? "skipped" : "unsupported");
|
|
72
|
+
if (Date.now() >= deadline) return result(false, strategy === "native" || state.native ? "native" : "geometry", "Layout observation exceeded its deadline");
|
|
73
|
+
if (nativeOutcome === "completed") return result(true, "native");
|
|
74
|
+
if (strategy === "native") return result(false, "native", state.nativeError || state.error || "Native layout API unavailable");
|
|
75
|
+
if (strategy === "auto") fallbackReason = state.nativeError || state.error || "Native layout API unavailable";
|
|
76
|
+
}
|
|
77
|
+
if (Date.now() >= deadline) return result(false, "geometry", "Layout observation exceeded its deadline");
|
|
78
|
+
const current = JSON.stringify(state.geometry);
|
|
79
|
+
equal = current === previous ? equal + 1 : 1;
|
|
80
|
+
if (equal >= 3) return result(true, "geometry");
|
|
81
|
+
previous = current;
|
|
82
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
83
|
+
}
|
|
84
|
+
return result(false, "geometry", "Layout did not settle within its bounded samples");
|
|
85
|
+
};
|
|
86
|
+
`;
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PenpotToolClient } from "./client.js";
|
|
2
|
+
export type LayoutStrategy = "auto" | "native" | "geometry";
|
|
3
|
+
export interface LayoutOptions {
|
|
4
|
+
shapeId?: string;
|
|
5
|
+
strategy?: LayoutStrategy;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
sleep?: (ms: number) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface LayoutResult {
|
|
11
|
+
stable: boolean;
|
|
12
|
+
method: "native" | "geometry" | "unavailable";
|
|
13
|
+
samples: number;
|
|
14
|
+
reason?: string;
|
|
15
|
+
strategy?: LayoutStrategy;
|
|
16
|
+
nativeOutcome?: "skipped" | "unsupported" | "completed" | "failed";
|
|
17
|
+
fallbackReason?: string;
|
|
18
|
+
elapsedMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
21
|
+
export declare function waitForStableLayout(client: PenpotToolClient, options?: LayoutOptions): Promise<LayoutResult>;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PenpairError } from "./errors.js";
|
|
2
|
+
import { commandSleep } from "./command-result.js";
|
|
3
|
+
import { runSnippet, snippet } from "./snippet.js";
|
|
4
|
+
import { layoutPolicySource } from "./layout-policy.js";
|
|
5
|
+
// This evaluates only the static policy above, never project or transport content.
|
|
6
|
+
const settle = new Function(`${layoutPolicySource}\nreturn settleLayout;`)();
|
|
7
|
+
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
8
|
+
export async function waitForStableLayout(client, options = {}) {
|
|
9
|
+
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, } = options;
|
|
10
|
+
try {
|
|
11
|
+
return await settle(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst shape = CFG.shapeId ? penpot.currentPage.getShapeById(CFG.shapeId) : penpot.currentPage.root;\nreturn await observeLayout(penpot, shape, CFG);`, {
|
|
12
|
+
shapeId,
|
|
13
|
+
nativeMs,
|
|
14
|
+
}, "read"), {
|
|
15
|
+
retries: 0,
|
|
16
|
+
timeoutMs: remaining,
|
|
17
|
+
}), {
|
|
18
|
+
timeoutMs,
|
|
19
|
+
intervalMs,
|
|
20
|
+
strategy,
|
|
21
|
+
sleep,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof PenpairError && error.code === "mcp-unavailable") {
|
|
26
|
+
return {
|
|
27
|
+
stable: false,
|
|
28
|
+
method: "unavailable",
|
|
29
|
+
samples: 0,
|
|
30
|
+
strategy,
|
|
31
|
+
reason: "Layout observation did not complete within its request budget",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PenpotToolClient } from "./client.js";
|
|
2
|
+
import { type DeepOptions, type DeepReport } from "./deep-check.js";
|
|
3
|
+
/** A live write probe is never scheduled blindly: the caller explicitly identifies a disposable file and tested version. */
|
|
4
|
+
export declare function runLiveSmoke(client: PenpotToolClient, options: DeepOptions & {
|
|
5
|
+
scratchFileId: string;
|
|
6
|
+
expectedVersion: string;
|
|
7
|
+
confirmedScratch: boolean;
|
|
8
|
+
}): Promise<DeepReport>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { executeCode } from "./client.js";
|
|
2
|
+
import { runDeepCheck } from "./deep-check.js";
|
|
3
|
+
import { guardFile } from "./file-guard.js";
|
|
4
|
+
/** A live write probe is never scheduled blindly: the caller explicitly identifies a disposable file and tested version. */
|
|
5
|
+
export async function runLiveSmoke(client, options) {
|
|
6
|
+
if (!options.confirmedScratch ||
|
|
7
|
+
!options.scratchFileId.trim() ||
|
|
8
|
+
!/^\d+\.\d+\.\d+$/.test(options.expectedVersion)) {
|
|
9
|
+
throw new Error("Live smoke requires an explicitly confirmed scratch file ID and expected Penpot version");
|
|
10
|
+
}
|
|
11
|
+
const guarded = guardFile(client, {
|
|
12
|
+
id: options.scratchFileId,
|
|
13
|
+
});
|
|
14
|
+
const { result } = await executeCode(guarded, "return { version: penpot.version, fileId: penpot.currentFile.id };", {
|
|
15
|
+
effect: "read",
|
|
16
|
+
retries: 0,
|
|
17
|
+
});
|
|
18
|
+
const actual = result;
|
|
19
|
+
if (actual?.version !== options.expectedVersion || actual?.fileId !== options.scratchFileId) {
|
|
20
|
+
throw new Error("Live smoke target/version differs from the pinned expectation. No probe was applied.");
|
|
21
|
+
}
|
|
22
|
+
return runDeepCheck(guarded, options);
|
|
23
|
+
}
|