@penpair/core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.js +58 -11
- package/dist/approvals.js +143 -29
- package/dist/atomic-output.js +9 -4
- package/dist/audit-rules.js +345 -79
- package/dist/backup-note.js +29 -8
- package/dist/baseline-coverage.js +169 -35
- package/dist/baselines-config.js +119 -29
- package/dist/baselines.js +280 -57
- package/dist/cli-arguments.js +18 -6
- package/dist/cli-main.js +1645 -392
- package/dist/client.d.ts +5 -2
- package/dist/client.js +127 -28
- package/dist/command-result.js +341 -52
- package/dist/coordination.js +91 -29
- package/dist/credentials.js +57 -13
- package/dist/deep-check.js +56 -12
- package/dist/design-spec.js +189 -49
- package/dist/endpoint.js +102 -34
- package/dist/export.js +86 -18
- package/dist/file-archive.js +33 -7
- package/dist/file-guard.js +29 -11
- package/dist/formats.js +215 -14
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/inspection.d.ts +1 -1
- package/dist/inspection.js +378 -65
- package/dist/instance/config-schema.js +400 -41
- package/dist/instance/config.js +101 -30
- package/dist/instance/connect.js +151 -44
- package/dist/instance/connection.d.ts +26 -5
- package/dist/instance/connection.js +194 -21
- package/dist/instance/gitignore.js +5 -2
- package/dist/instance/init.js +32 -7
- package/dist/instance/runner.js +32 -10
- package/dist/instance/setup.js +46 -13
- package/dist/instance/tty.js +26 -8
- package/dist/journal.d.ts +10 -0
- package/dist/journal.js +212 -49
- package/dist/kit-smoke.js +82 -20
- package/dist/kit-source.js +10 -40
- package/dist/kit.js +102 -34
- package/dist/layout-policy.d.ts +2 -0
- package/dist/layout-policy.js +86 -0
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +27 -70
- package/dist/live-smoke.js +13 -4
- package/dist/local-profile.js +2 -1
- package/dist/mcp-server.js +360 -47
- package/dist/native-api.js +47 -11
- package/dist/onboard.js +33 -7
- package/dist/operation.d.ts +2 -2
- package/dist/operation.js +279 -69
- package/dist/page-session.d.ts +23 -0
- package/dist/page-session.js +142 -0
- package/dist/png.js +18 -3
- package/dist/portable-tokens.js +254 -57
- package/dist/probe.d.ts +1 -1
- package/dist/probe.js +48 -11
- package/dist/project-input.js +24 -10
- package/dist/project-snippets.js +18 -6
- package/dist/project-status.js +133 -24
- package/dist/registry.js +217 -60
- package/dist/run-file.js +129 -30
- package/dist/shots.js +108 -26
- package/dist/skill-check-entry.d.ts +17 -0
- package/dist/skill-check-entry.js +89 -0
- package/dist/skill-check-instructions.d.ts +7 -0
- package/dist/skill-check-instructions.js +39 -0
- package/dist/skill-check.d.ts +15 -0
- package/dist/skill-check.js +36 -0
- package/dist/skill-file.d.ts +2 -0
- package/dist/skill-file.js +31 -0
- package/dist/skill-instructions.d.ts +4 -0
- package/dist/skill-instructions.js +97 -22
- package/dist/skill-metadata.d.ts +8 -0
- package/dist/skill-metadata.js +52 -0
- package/dist/skill-routing.d.ts +2 -0
- package/dist/skill-routing.js +36 -0
- package/dist/skills.js +131 -41
- package/dist/snippet.js +27 -7
- package/dist/snippets/read.js +8 -2
- package/dist/structure-snapshot.d.ts +12 -0
- package/dist/structure-snapshot.js +439 -85
- package/dist/token-edit.js +219 -64
- package/dist/token-projection.d.ts +56 -0
- package/dist/token-projection.js +286 -0
- package/dist/tokens-diff.js +125 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/skills/design-judgment/SKILL.md +1 -1
- package/skills/design-judgment/agents/openai.yaml +6 -0
- package/skills/penpair-design/SKILL.md +14 -1
- package/skills/penpair-design/agents/openai.yaml +6 -0
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
- package/skills/penpair-design/references/project-facts.md +2 -0
- package/skills/penpair-handoff/SKILL.md +14 -1
- package/skills/penpair-handoff/agents/openai.yaml +6 -0
- package/skills/penpair-handoff/references/comparison-companion.md +16 -0
- package/skills/penpair-handoff/references/handoff-flow.md +2 -0
- package/skills/penpair-recipes/SKILL.md +13 -1
- package/skills/penpair-recipes/agents/openai.yaml +6 -0
- package/skills/penpair-recipes/assets/visual-checkpoint.md +48 -0
- package/skills/penpair-recipes/references/visual-checkpoint.md +37 -0
- package/skills/penpair-setup/SKILL.md +9 -3
- package/skills/penpair-setup/agents/openai.yaml +6 -0
- package/skills/penpair-verify/SKILL.md +49 -0
- package/skills/penpair-verify/agents/openai.yaml +6 -0
- package/skills/penpair-verify/references/browser-evidence.md +33 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -3
- package/dist/docker/lifecycle.d.ts +0 -96
- package/dist/docker/lifecycle.js +0 -273
- package/dist/docker/secrets.d.ts +0 -11
- package/dist/docker/secrets.js +0 -38
- package/dist/docker/template.d.ts +0 -8
- package/dist/docker/template.js +0 -158
- package/dist/docker-profile.d.ts +0 -114
- package/dist/docker-profile.js +0 -18
- package/dist/instance/lifecycle.d.ts +0 -129
- package/dist/instance/lifecycle.js +0 -336
- package/dist/instance/secrets.d.ts +0 -11
- package/dist/instance/secrets.js +0 -38
- package/dist/instance/template.d.ts +0 -8
- package/dist/instance/template.js +0 -158
package/dist/coordination.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inheritPageSession } from "./page-session.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { hostname, userInfo } from "node:os";
|
|
3
4
|
import { lstat, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
@@ -8,22 +9,43 @@ import { maxSnippetBytes } from "./client.js";
|
|
|
8
9
|
import { PenpairError } from "./errors.js";
|
|
9
10
|
/** Exclusive only within this checkout. Shared file markers below are advisory, not compare-and-swap. */
|
|
10
11
|
async function localLease(root, fileId, runId, force) {
|
|
11
|
-
await ensureGitignoreEntry(root, ".penpair", {
|
|
12
|
+
await ensureGitignoreEntry(root, ".penpair", {
|
|
13
|
+
directory: true,
|
|
14
|
+
comment: "penpair: private local operation evidence",
|
|
15
|
+
});
|
|
12
16
|
const parent = path.join(root, ".penpair");
|
|
13
|
-
await mkdir(parent, {
|
|
14
|
-
|
|
17
|
+
await mkdir(parent, {
|
|
18
|
+
mode: 0o700,
|
|
19
|
+
}).catch((error) => {
|
|
20
|
+
if (error.code !== "EEXIST") {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
15
24
|
const parentInfo = await lstat(parent);
|
|
16
|
-
if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink())
|
|
25
|
+
if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink()) {
|
|
17
26
|
throw new Error("Write locks require a real local directory");
|
|
27
|
+
}
|
|
18
28
|
const directory = path.join(parent, "locks");
|
|
19
|
-
await mkdir(directory, {
|
|
20
|
-
|
|
29
|
+
await mkdir(directory, {
|
|
30
|
+
mode: 0o700,
|
|
31
|
+
}).catch((error) => {
|
|
32
|
+
if (error.code !== "EEXIST") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
21
36
|
const info = await lstat(directory);
|
|
22
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
37
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
23
38
|
throw new Error("Write locks require a real local directory");
|
|
39
|
+
}
|
|
24
40
|
const destination = path.join(directory, `${createHash("sha256").update(fileId).digest("hex")}.json`);
|
|
25
41
|
const token = randomUUID();
|
|
26
|
-
const entry = {
|
|
42
|
+
const entry = {
|
|
43
|
+
token,
|
|
44
|
+
runId,
|
|
45
|
+
pid: process.pid,
|
|
46
|
+
host: hostname(),
|
|
47
|
+
createdAt: new Date().toISOString(),
|
|
48
|
+
};
|
|
27
49
|
const create = async () => {
|
|
28
50
|
const handle = await open(destination, "wx", 0o600);
|
|
29
51
|
try {
|
|
@@ -37,8 +59,9 @@ async function localLease(root, fileId, runId, force) {
|
|
|
37
59
|
await create();
|
|
38
60
|
}
|
|
39
61
|
catch (error) {
|
|
40
|
-
if (error.code !== "EEXIST")
|
|
62
|
+
if (error.code !== "EEXIST") {
|
|
41
63
|
throw error;
|
|
64
|
+
}
|
|
42
65
|
const previous = JSON.parse(await readFile(destination, "utf8"));
|
|
43
66
|
let running = false;
|
|
44
67
|
if (previous.host === hostname() && Number.isInteger(previous.pid) && previous.pid > 0) {
|
|
@@ -47,20 +70,23 @@ async function localLease(root, fileId, runId, force) {
|
|
|
47
70
|
running = true;
|
|
48
71
|
}
|
|
49
72
|
catch (failure) {
|
|
50
|
-
if (failure.code !== "ESRCH")
|
|
73
|
+
if (failure.code !== "ESRCH") {
|
|
51
74
|
running = true;
|
|
75
|
+
}
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
|
-
if (!force || running)
|
|
78
|
+
if (!force || running) {
|
|
55
79
|
throw new Error(`Local writer ${previous.runId} holds this file. ${running ? "Its process is still running; wait for it to finish." : "Inspect its journal, then use --force-lock to remove the abandoned local lock."}`);
|
|
80
|
+
}
|
|
56
81
|
// Explicit owner override only. Simultaneous force operations are not an exclusion guarantee.
|
|
57
82
|
await unlink(destination);
|
|
58
83
|
await create();
|
|
59
84
|
}
|
|
60
85
|
return async () => {
|
|
61
86
|
const current = JSON.parse(await readFile(destination, "utf8"));
|
|
62
|
-
if (current.token === token)
|
|
87
|
+
if (current.token === token) {
|
|
63
88
|
await unlink(destination);
|
|
89
|
+
}
|
|
64
90
|
};
|
|
65
91
|
}
|
|
66
92
|
const readMarker = `const f = penpot.currentFile;
|
|
@@ -72,36 +98,62 @@ if (raw) { try { marker = JSON.parse(raw); } catch { return { ok: false, message
|
|
|
72
98
|
export function writeCoordinator(client, options) {
|
|
73
99
|
return async (runId, target) => {
|
|
74
100
|
const releaseLocal = await localLease(options.root, target.fileId, runId, options.forceLock === true);
|
|
75
|
-
const marker = {
|
|
76
|
-
|
|
101
|
+
const marker = {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
token: randomUUID(),
|
|
104
|
+
runId,
|
|
105
|
+
operation: options.operation,
|
|
106
|
+
owner: userInfo().username,
|
|
107
|
+
createdAt: Date.now(),
|
|
108
|
+
expiresAt: Date.now() + 15 * 60_000,
|
|
109
|
+
};
|
|
110
|
+
const params = {
|
|
111
|
+
marker,
|
|
112
|
+
fileId: target.fileId,
|
|
113
|
+
force: options.forceLock === true,
|
|
114
|
+
};
|
|
77
115
|
const targetGuard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok:false, message:"Writer marker target changed" };';
|
|
78
116
|
try {
|
|
79
|
-
const before = await runSnippet(client, snippet("writer-inspect", targetGuard + readMarker +
|
|
117
|
+
const before = await runSnippet(client, snippet("writer-inspect", targetGuard + readMarker + "return { ok:true, supported:true, marker };", params, "read"));
|
|
80
118
|
if (before.supported === false) {
|
|
81
119
|
options.warn?.("Shared writer markers unavailable; local checkout lock only. Coordinate with colleagues before writing.");
|
|
82
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
check: async () => undefined,
|
|
122
|
+
release: releaseLocal,
|
|
123
|
+
};
|
|
83
124
|
}
|
|
84
|
-
const acquired = await runSnippet(client, snippet("writer-acquire", targetGuard +
|
|
125
|
+
const acquired = await runSnippet(client, snippet("writer-acquire", targetGuard +
|
|
126
|
+
readMarker +
|
|
127
|
+
`
|
|
85
128
|
if (marker && marker.token !== CFG.marker.token && (!Number.isFinite(marker.expiresAt) || marker.expiresAt > Date.now()) && !CFG.force)
|
|
86
129
|
return { ok:false, message:"Another writer is running " + String(marker.operation) + " (run " + String(marker.runId) + "). Coordinate before --force-lock." };
|
|
87
130
|
f.setSharedPluginData("penpair", "writer", JSON.stringify(CFG.marker));
|
|
88
131
|
return { ok:true, supported:true };`, params, "write"));
|
|
89
|
-
if (acquired.supported !== true)
|
|
132
|
+
if (acquired.supported !== true) {
|
|
90
133
|
throw new Error("Writer marker capability changed during acquisition");
|
|
134
|
+
}
|
|
91
135
|
const check = async () => {
|
|
92
|
-
const observed = await runSnippet(client, snippet("writer-check", targetGuard +
|
|
93
|
-
|
|
136
|
+
const observed = await runSnippet(client, snippet("writer-check", targetGuard +
|
|
137
|
+
readMarker +
|
|
138
|
+
"return { ok:true, supported:true, owned: !!marker && marker.token === CFG.marker.token && marker.expiresAt > Date.now() };", params, "read"));
|
|
139
|
+
if (observed.supported !== true || observed.owned !== true) {
|
|
94
140
|
throw new Error("Shared writer marker was lost or expired; stop and coordinate before resuming");
|
|
141
|
+
}
|
|
95
142
|
};
|
|
96
143
|
await check();
|
|
97
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
check,
|
|
146
|
+
release: async () => {
|
|
98
147
|
try {
|
|
99
|
-
await runSnippet(client, snippet("writer-release", targetGuard +
|
|
148
|
+
await runSnippet(client, snippet("writer-release", targetGuard +
|
|
149
|
+
readMarker +
|
|
150
|
+
'if (marker && marker.token === CFG.marker.token) f.setSharedPluginData("penpair", "writer", ""); return { ok:true };', params, "write"));
|
|
100
151
|
}
|
|
101
152
|
finally {
|
|
102
153
|
await releaseLocal();
|
|
103
154
|
}
|
|
104
|
-
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
105
157
|
}
|
|
106
158
|
catch (error) {
|
|
107
159
|
await releaseLocal();
|
|
@@ -111,12 +163,22 @@ return { ok:true, supported:true };`, params, "write"));
|
|
|
111
163
|
}
|
|
112
164
|
/** Revision precondition is checked inside the delivered mutation, after all guard calls. It is not a server CAS. */
|
|
113
165
|
export function guardRevision(client, target) {
|
|
114
|
-
return
|
|
115
|
-
|
|
166
|
+
return inheritPageSession({
|
|
167
|
+
...client,
|
|
168
|
+
close: () => client.close(),
|
|
169
|
+
async callTool(name, args, options) {
|
|
170
|
+
if (name !== "execute_code" || typeof args.code !== "string") {
|
|
116
171
|
throw new Error("Revision guards require execute_code");
|
|
117
|
-
|
|
118
|
-
if (
|
|
172
|
+
}
|
|
173
|
+
const code = `{ if (penpot.currentFile.id !== ${JSON.stringify(target.fileId)} || penpot.currentFile.revn !== ${JSON.stringify(target.revision)}) return {ok:false, message:"File revision changed before mutation; inspect again"}; }\n` +
|
|
174
|
+
args.code;
|
|
175
|
+
if (Buffer.byteLength(code) > maxSnippetBytes) {
|
|
119
176
|
throw new PenpairError("snippet-too-large", "Snippet including revision guard exceeds 90 KB");
|
|
120
|
-
|
|
121
|
-
|
|
177
|
+
}
|
|
178
|
+
return client.callTool(name, {
|
|
179
|
+
...args,
|
|
180
|
+
code,
|
|
181
|
+
}, options);
|
|
182
|
+
},
|
|
183
|
+
}, client);
|
|
122
184
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -4,36 +4,74 @@ import { open } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { PenpairError } from "./errors.js";
|
|
6
6
|
export function validateCredentialReference(raw) {
|
|
7
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
7
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8
8
|
throw new Error("Credential reference must be a file or keychain object");
|
|
9
|
+
}
|
|
9
10
|
const v = raw;
|
|
10
11
|
if (v.type === "file") {
|
|
11
|
-
if (Object.keys(v).some(
|
|
12
|
+
if (Object.keys(v).some((k) => ![
|
|
13
|
+
"type",
|
|
14
|
+
"path",
|
|
15
|
+
].includes(k)) ||
|
|
16
|
+
typeof v.path !== "string" ||
|
|
17
|
+
!v.path.trim() ||
|
|
18
|
+
v.path.length > 4096 ||
|
|
19
|
+
/[\x00-\x1f]/.test(v.path)) {
|
|
12
20
|
throw new Error("Invalid credential file reference");
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
else if (v.type === "keychain") {
|
|
15
|
-
if (Object.keys(v).some(
|
|
24
|
+
if (Object.keys(v).some((k) => ![
|
|
25
|
+
"type",
|
|
26
|
+
"service",
|
|
27
|
+
"account",
|
|
28
|
+
].includes(k)) ||
|
|
29
|
+
[
|
|
30
|
+
v.service,
|
|
31
|
+
v.account,
|
|
32
|
+
].some((s) => typeof s !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._ /:@-]{0,199}$/.test(s))) {
|
|
16
33
|
throw new Error("Invalid keychain service/account reference");
|
|
34
|
+
}
|
|
17
35
|
}
|
|
18
|
-
else
|
|
36
|
+
else {
|
|
19
37
|
throw new Error("Only file and macOS keychain credential references are supported; arbitrary command providers are deferred");
|
|
38
|
+
}
|
|
20
39
|
return structuredClone(v);
|
|
21
40
|
}
|
|
22
|
-
const keychainRead = (command, args, options) => new Promise((resolve, reject) => execFile(command, args, {
|
|
41
|
+
const keychainRead = (command, args, options) => new Promise((resolve, reject) => execFile(command, args, {
|
|
42
|
+
...options,
|
|
43
|
+
killSignal: "SIGKILL",
|
|
44
|
+
}, (error, stdout) => error
|
|
45
|
+
? reject(new Error("Credential provider unavailable or access denied"))
|
|
46
|
+
: resolve(stdout)));
|
|
23
47
|
const normalize = (raw) => {
|
|
24
48
|
const value = raw.trim();
|
|
25
|
-
if (!value || Buffer.byteLength(value) > 8192 || /[\r\n\x00]/.test(value))
|
|
49
|
+
if (!value || Buffer.byteLength(value) > 8192 || /[\r\n\x00]/.test(value)) {
|
|
26
50
|
throw new PenpairError("endpoint-invalid", "Credential provider must return one bounded MCP URL line");
|
|
51
|
+
}
|
|
27
52
|
return value;
|
|
28
53
|
};
|
|
29
54
|
/** Resolve only at connection time. No URL, key, child stderr or command error object enters diagnostics. */
|
|
30
55
|
export async function readCredential(reference, options) {
|
|
31
|
-
const ref = validateCredentialReference(reference)
|
|
56
|
+
const ref = validateCredentialReference(reference);
|
|
57
|
+
const platform = options.platform ?? process.platform;
|
|
32
58
|
if (ref.type === "keychain") {
|
|
33
|
-
if (platform !== "darwin")
|
|
59
|
+
if (platform !== "darwin") {
|
|
34
60
|
throw new PenpairError("endpoint-unconfigured", "macOS Keychain references require macOS; configure a private credential file on this platform");
|
|
61
|
+
}
|
|
35
62
|
try {
|
|
36
|
-
return normalize(await (options.keychainRead ?? keychainRead)("/usr/bin/security", [
|
|
63
|
+
return normalize(await (options.keychainRead ?? keychainRead)("/usr/bin/security", [
|
|
64
|
+
"find-generic-password",
|
|
65
|
+
"-s",
|
|
66
|
+
ref.service,
|
|
67
|
+
"-a",
|
|
68
|
+
ref.account,
|
|
69
|
+
"-w",
|
|
70
|
+
], {
|
|
71
|
+
timeout: 5000,
|
|
72
|
+
maxBuffer: 16384,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
}));
|
|
37
75
|
}
|
|
38
76
|
catch {
|
|
39
77
|
throw new PenpairError("endpoint-unconfigured", "OS credential lookup failed, timed out, or returned invalid output; inspect access in the OS credential UI");
|
|
@@ -43,13 +81,19 @@ export async function readCredential(reference, options) {
|
|
|
43
81
|
try {
|
|
44
82
|
handle = await open(path.resolve(options.root, ref.path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
45
83
|
const info = await handle.stat();
|
|
46
|
-
if (!info.isFile() || info.size > 16384)
|
|
84
|
+
if (!info.isFile() || info.size > 16384) {
|
|
47
85
|
throw new Error("not a bounded regular file");
|
|
48
|
-
|
|
86
|
+
}
|
|
87
|
+
if (platform !== "win32" &&
|
|
88
|
+
((info.mode & 0o077) !== 0 ||
|
|
89
|
+
(typeof process.getuid === "function" && info.uid !== process.getuid()))) {
|
|
49
90
|
throw new Error("not private to current owner");
|
|
50
|
-
|
|
51
|
-
|
|
91
|
+
}
|
|
92
|
+
const buffer = Buffer.alloc(16385);
|
|
93
|
+
const read = await handle.read(buffer, 0, buffer.length, 0);
|
|
94
|
+
if (read.bytesRead > 16384) {
|
|
52
95
|
throw new Error("credential file byte budget exceeded");
|
|
96
|
+
}
|
|
53
97
|
return normalize(buffer.subarray(0, read.bytesRead).toString("utf8"));
|
|
54
98
|
}
|
|
55
99
|
catch {
|
package/dist/deep-check.js
CHANGED
|
@@ -68,30 +68,74 @@ for (const c of penpot.library.local.components) if (c.name.startsWith(tag)) lef
|
|
|
68
68
|
return { ok: true, readOnly: true, left };`;
|
|
69
69
|
export async function runDeepCheck(client, options = {}) {
|
|
70
70
|
const tag = `penpair-deep-${randomBytes(3).toString("hex")}`;
|
|
71
|
-
const first = (await executeCode(client, wrap(deepCheckBody, {
|
|
72
|
-
|
|
71
|
+
const first = (await executeCode(client, wrap(deepCheckBody, {
|
|
72
|
+
tag,
|
|
73
|
+
}), {
|
|
74
|
+
...options,
|
|
75
|
+
effect: "write",
|
|
76
|
+
})).result;
|
|
77
|
+
if (first.ok === false) {
|
|
73
78
|
throw new Error(`the deep check could not run: ${first.message ?? "unknown error"}`);
|
|
79
|
+
}
|
|
74
80
|
await new Promise((resolve) => setTimeout(resolve, options.settleMs ?? 3_000));
|
|
75
|
-
const second = (await executeCode(client, wrap(deepLeftoversBody, {
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const second = (await executeCode(client, wrap(deepLeftoversBody, {
|
|
82
|
+
tag,
|
|
83
|
+
}), {
|
|
84
|
+
...options,
|
|
85
|
+
effect: "read",
|
|
86
|
+
})).result;
|
|
87
|
+
const steps = Array.isArray(first.steps)
|
|
88
|
+
? [
|
|
89
|
+
...first.steps,
|
|
90
|
+
]
|
|
91
|
+
: [];
|
|
92
|
+
const required = [
|
|
93
|
+
"create board",
|
|
94
|
+
"flex layout and children",
|
|
95
|
+
"text content",
|
|
96
|
+
"token set, token and binding",
|
|
97
|
+
"component and swap",
|
|
98
|
+
"clone",
|
|
99
|
+
"export png",
|
|
100
|
+
];
|
|
78
101
|
const complete = required.every((name) => steps.some((step) => step.step === name && step.ok === true));
|
|
79
|
-
if (!complete)
|
|
80
|
-
steps.push({
|
|
81
|
-
|
|
102
|
+
if (!complete) {
|
|
103
|
+
steps.push({
|
|
104
|
+
step: "complete evidence",
|
|
105
|
+
ok: false,
|
|
106
|
+
ms: 0,
|
|
107
|
+
detail: "Required deep-check observations are missing or failed",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const leftovers = Array.isArray(second.left)
|
|
111
|
+
? second.left
|
|
112
|
+
: [
|
|
113
|
+
"cleanup readback was missing; inspect the scratch file",
|
|
114
|
+
];
|
|
82
115
|
return {
|
|
83
116
|
tag,
|
|
84
|
-
...(first.penpotVersion
|
|
117
|
+
...(first.penpotVersion
|
|
118
|
+
? {
|
|
119
|
+
penpotVersion: first.penpotVersion,
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
85
122
|
steps,
|
|
86
123
|
cleanup: first.cleanup ?? [],
|
|
87
124
|
leftovers,
|
|
88
|
-
ok: first.ok === true &&
|
|
125
|
+
ok: first.ok === true &&
|
|
126
|
+
second.ok === true &&
|
|
127
|
+
complete &&
|
|
128
|
+
steps.every((s) => s.ok === true) &&
|
|
129
|
+
leftovers.length === 0,
|
|
89
130
|
};
|
|
90
131
|
}
|
|
91
132
|
export function formatDeepReport(report) {
|
|
92
133
|
const lines = report.steps.map((s) => `${s.ok ? "ok " : "FAIL"} ${s.step} (${s.ms} ms)${s.detail ? `: ${s.detail}` : ""}`);
|
|
93
|
-
lines.push(report.leftovers.length === 0
|
|
94
|
-
|
|
134
|
+
lines.push(report.leftovers.length === 0
|
|
135
|
+
? "ok clean-up: nothing left in the file"
|
|
136
|
+
: `FAIL clean-up: left in the file, remove by hand: ${report.leftovers.join(", ")}`);
|
|
137
|
+
if (report.penpotVersion) {
|
|
95
138
|
lines.push(`Penpot ${report.penpotVersion}`);
|
|
139
|
+
}
|
|
96
140
|
return lines.join("\n");
|
|
97
141
|
}
|