@penpair/core 0.2.0 → 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.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 +1621 -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-instructions.js +91 -22
- package/dist/skills.js +129 -39
- 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 +1 -1
- package/skills/penpair-design/SKILL.md +3 -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 +2 -0
- package/skills/penpair-handoff/references/comparison-companion.md +13 -0
- package/skills/penpair-handoff/references/handoff-flow.md +2 -0
- package/skills/penpair-recipes/SKILL.md +5 -0
- package/skills/penpair-setup/SKILL.md +3 -1
- 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/adapters.js
CHANGED
|
@@ -7,22 +7,60 @@ import { readProjectConfig } from "./instance/config.js";
|
|
|
7
7
|
export const defineAdapter = (adapter) => adapter;
|
|
8
8
|
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
9
9
|
export const reservedCommands = [
|
|
10
|
-
"formats",
|
|
11
|
-
"
|
|
10
|
+
"formats",
|
|
11
|
+
"snapshot",
|
|
12
|
+
"approve",
|
|
13
|
+
"mcp",
|
|
14
|
+
"spec",
|
|
15
|
+
"inspect",
|
|
16
|
+
"probe",
|
|
17
|
+
"pages",
|
|
18
|
+
"tree",
|
|
19
|
+
"audit",
|
|
20
|
+
"tokens",
|
|
21
|
+
"export",
|
|
22
|
+
"shots",
|
|
23
|
+
"archive",
|
|
24
|
+
"snippets",
|
|
25
|
+
"run",
|
|
26
|
+
"run-file",
|
|
27
|
+
"kit",
|
|
28
|
+
"recipes",
|
|
29
|
+
"skills",
|
|
30
|
+
"init",
|
|
31
|
+
"setup",
|
|
32
|
+
"connect",
|
|
33
|
+
"up",
|
|
34
|
+
"down",
|
|
35
|
+
"open",
|
|
36
|
+
"status",
|
|
37
|
+
"doctor",
|
|
38
|
+
"backup",
|
|
39
|
+
"restore",
|
|
40
|
+
"baselines",
|
|
41
|
+
"config",
|
|
42
|
+
"runs",
|
|
43
|
+
"onboard",
|
|
44
|
+
"help",
|
|
12
45
|
];
|
|
13
46
|
/** Resolves an adapter the way the project would: a relative path from the project's folder, or a package installed in the project. */
|
|
14
47
|
export const importFromProject = async (specifier, root) => {
|
|
15
48
|
let file;
|
|
16
|
-
if (specifier.startsWith(".") || path.isAbsolute(specifier))
|
|
49
|
+
if (specifier.startsWith(".") || path.isAbsolute(specifier)) {
|
|
17
50
|
file = await resolveProjectInput(root, specifier);
|
|
51
|
+
}
|
|
18
52
|
else {
|
|
19
|
-
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:\/[a-z0-9._-]+)*$/i.test(specifier) ||
|
|
53
|
+
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:\/[a-z0-9._-]+)*$/i.test(specifier) ||
|
|
54
|
+
specifier.split("/").includes("..")) {
|
|
20
55
|
throw new Error("Adapter must name an installed package or a relative project module");
|
|
56
|
+
}
|
|
21
57
|
try {
|
|
22
58
|
file = createRequire(path.join(root, "package.json")).resolve(specifier);
|
|
23
59
|
}
|
|
24
60
|
catch {
|
|
25
|
-
const error = Object.assign(new Error(`${specifier} is not installed in this project`), {
|
|
61
|
+
const error = Object.assign(new Error(`${specifier} is not installed in this project`), {
|
|
62
|
+
code: "ERR_MODULE_NOT_FOUND",
|
|
63
|
+
});
|
|
26
64
|
throw error;
|
|
27
65
|
}
|
|
28
66
|
}
|
|
@@ -50,18 +88,27 @@ export async function loadAdapters(root, specifiers, importer = importFromProjec
|
|
|
50
88
|
continue;
|
|
51
89
|
}
|
|
52
90
|
for (const command of adapter.commands) {
|
|
53
|
-
if (reservedCommands.includes(command.name))
|
|
91
|
+
if (reservedCommands.includes(command.name)) {
|
|
54
92
|
problems.push(`adapter ${specifier}: the command "${command.name}" is one the core already has`);
|
|
55
|
-
|
|
93
|
+
}
|
|
94
|
+
else if (taken.has(command.name)) {
|
|
56
95
|
problems.push(`adapter ${specifier}: the command "${command.name}" is already provided by ${taken.get(command.name)}`);
|
|
57
|
-
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
58
98
|
taken.set(command.name, adapter.name);
|
|
99
|
+
}
|
|
59
100
|
}
|
|
60
|
-
adapters.push({
|
|
101
|
+
adapters.push({
|
|
102
|
+
...adapter,
|
|
103
|
+
commands: adapter.commands.filter((c) => taken.get(c.name) === adapter.name),
|
|
104
|
+
});
|
|
61
105
|
}
|
|
62
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
adapters,
|
|
108
|
+
problems,
|
|
109
|
+
};
|
|
63
110
|
}
|
|
64
111
|
/** The whole penpair.config.json as written (an adapter's own settings live in it). Empty when there is no file. */
|
|
65
112
|
export async function readRawConfig(root) {
|
|
66
|
-
return await readProjectConfig(root) ?? {};
|
|
113
|
+
return (await readProjectConfig(root)) ?? {};
|
|
67
114
|
}
|
package/dist/approvals.js
CHANGED
|
@@ -9,77 +9,184 @@ import { pngSize } from "./png.js";
|
|
|
9
9
|
export const approvalFile = "design/approvals.json";
|
|
10
10
|
export const artifactSha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
11
11
|
export function approvalFingerprint(capture, sha256, intent) {
|
|
12
|
-
if (!capture ||
|
|
12
|
+
if (!capture ||
|
|
13
|
+
typeof capture.fileId !== "string" ||
|
|
14
|
+
!capture.fileId ||
|
|
15
|
+
typeof capture.pageId !== "string" ||
|
|
16
|
+
!capture.pageId ||
|
|
17
|
+
typeof capture.frameId !== "string" ||
|
|
18
|
+
!capture.frameId ||
|
|
19
|
+
!Number.isInteger(capture.revision) ||
|
|
20
|
+
!Number.isFinite(capture.scale) ||
|
|
21
|
+
capture.scale <= 0 ||
|
|
22
|
+
!/^[a-f0-9]{64}$/.test(sha256)) {
|
|
13
23
|
throw new Error("Approval requires exact capture identity, revision, scale and SHA-256");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
}
|
|
25
|
+
if (intent) {
|
|
26
|
+
validateBaselineCase(intent, (message) => {
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return observationHash({
|
|
31
|
+
capture,
|
|
32
|
+
sha256,
|
|
33
|
+
case: intent ?? null,
|
|
34
|
+
});
|
|
17
35
|
}
|
|
18
36
|
export function validateApprovals(raw) {
|
|
19
37
|
const v = raw;
|
|
20
|
-
if (!v ||
|
|
38
|
+
if (!v ||
|
|
39
|
+
v.schemaVersion !== 1 ||
|
|
40
|
+
v.kind !== "design-approvals" ||
|
|
41
|
+
!Array.isArray(v.records) ||
|
|
42
|
+
v.records.length > 10000) {
|
|
21
43
|
throw new Error("Unsupported approval document");
|
|
44
|
+
}
|
|
22
45
|
const ids = new Set();
|
|
23
46
|
for (const r of v.records) {
|
|
24
|
-
if (!r ||
|
|
47
|
+
if (!r ||
|
|
48
|
+
r.schemaVersion !== 1 ||
|
|
49
|
+
r.kind !== "owner-words" ||
|
|
50
|
+
typeof r.id !== "string" ||
|
|
51
|
+
!r.id ||
|
|
52
|
+
ids.has(r.id) ||
|
|
53
|
+
typeof r.note !== "string" ||
|
|
54
|
+
!r.note.trim() ||
|
|
55
|
+
r.note.length > 4000 ||
|
|
56
|
+
typeof r.by !== "string" ||
|
|
57
|
+
!r.by.trim() ||
|
|
58
|
+
r.by.length > 200 ||
|
|
59
|
+
!Number.isFinite(Date.parse(r.recordedAt)) ||
|
|
60
|
+
typeof r.image !== "string" ||
|
|
61
|
+
!r.image) {
|
|
25
62
|
throw new Error("Malformed owner approval record");
|
|
63
|
+
}
|
|
26
64
|
ids.add(r.id);
|
|
27
65
|
const { hash, ...body } = r;
|
|
28
|
-
if (hash !== observationHash(body) ||
|
|
66
|
+
if (hash !== observationHash(body) ||
|
|
67
|
+
r.fingerprint !== approvalFingerprint(r.capture, r.sha256, r.case)) {
|
|
29
68
|
throw new Error("Approval evidence hash mismatch");
|
|
69
|
+
}
|
|
30
70
|
}
|
|
31
71
|
return v;
|
|
32
72
|
}
|
|
33
73
|
export async function readApprovals(root) {
|
|
34
74
|
try {
|
|
35
75
|
const file = await resolveProjectInput(root, approvalFile);
|
|
36
|
-
if ((await stat(file)).size > 16 * 1024 * 1024)
|
|
76
|
+
if ((await stat(file)).size > 16 * 1024 * 1024) {
|
|
37
77
|
throw new Error("Approval file exceeds 16 MiB");
|
|
78
|
+
}
|
|
38
79
|
return validateApprovals(JSON.parse(await readFile(file, "utf8")));
|
|
39
80
|
}
|
|
40
81
|
catch (error) {
|
|
41
|
-
if (error.code === "ENOENT")
|
|
42
|
-
return {
|
|
82
|
+
if (error.code === "ENOENT") {
|
|
83
|
+
return {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
kind: "design-approvals",
|
|
86
|
+
records: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
43
89
|
throw error;
|
|
44
90
|
}
|
|
45
91
|
}
|
|
46
92
|
/** Records supplied owner words; hashes detect accidental changes, not identity or consent. */
|
|
47
93
|
export async function recordApproval(root, exported, words, intent, redact) {
|
|
48
|
-
exported = {
|
|
94
|
+
exported = {
|
|
95
|
+
...exported,
|
|
96
|
+
capture: structuredClone(exported.capture),
|
|
97
|
+
png: Buffer.from(exported.png),
|
|
98
|
+
};
|
|
49
99
|
intent = intent ? structuredClone(intent) : undefined;
|
|
50
|
-
words = {
|
|
51
|
-
|
|
100
|
+
words = {
|
|
101
|
+
...words,
|
|
102
|
+
};
|
|
103
|
+
if (!exported.stable || !pngSize(exported.png)) {
|
|
52
104
|
throw new Error("Approval requires a stable independently exported PNG");
|
|
53
|
-
|
|
105
|
+
}
|
|
106
|
+
if (typeof words.note !== "string" ||
|
|
107
|
+
!words.note.trim() ||
|
|
108
|
+
words.note.length > 4000 ||
|
|
109
|
+
typeof words.by !== "string" ||
|
|
110
|
+
!words.by.trim() ||
|
|
111
|
+
words.by.length > 200) {
|
|
54
112
|
throw new Error("Supply the owner's exact nonempty words and identity; an agent cannot invent approval");
|
|
55
|
-
|
|
56
|
-
|
|
113
|
+
}
|
|
114
|
+
const sha256 = artifactSha(exported.png);
|
|
115
|
+
const fingerprint = approvalFingerprint(exported.capture, sha256, intent);
|
|
116
|
+
if (intent &&
|
|
117
|
+
(!exported.capture.designSize ||
|
|
118
|
+
exported.capture.designSize.width !== intent.viewport.width ||
|
|
119
|
+
exported.capture.designSize.height !== intent.viewport.height ||
|
|
120
|
+
(intent.activeThemeIds &&
|
|
121
|
+
observationHash([
|
|
122
|
+
...(exported.capture.activeThemeIds ?? []),
|
|
123
|
+
].sort()) !==
|
|
124
|
+
observationHash([
|
|
125
|
+
...intent.activeThemeIds,
|
|
126
|
+
].sort())))) {
|
|
57
127
|
throw new Error("Approval case assertions do not match observed capture");
|
|
58
|
-
|
|
59
|
-
const
|
|
128
|
+
}
|
|
129
|
+
const body = {
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
id: randomUUID(),
|
|
132
|
+
kind: "owner-words",
|
|
133
|
+
recordedAt: new Date().toISOString(),
|
|
134
|
+
by: words.by,
|
|
135
|
+
note: words.note,
|
|
136
|
+
capture: exported.capture,
|
|
137
|
+
...(intent
|
|
138
|
+
? {
|
|
139
|
+
case: intent,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
sha256,
|
|
143
|
+
image: `design/approvals/${sha256}.png`,
|
|
144
|
+
fingerprint,
|
|
145
|
+
};
|
|
146
|
+
const record = {
|
|
147
|
+
...body,
|
|
148
|
+
hash: observationHash(body),
|
|
149
|
+
};
|
|
60
150
|
const serialized = JSON.stringify(record);
|
|
61
|
-
if (redact && redact(serialized) !== serialized)
|
|
151
|
+
if (redact && redact(serialized) !== serialized) {
|
|
62
152
|
throw new Error("Approval requires credential redaction; no record was written");
|
|
153
|
+
}
|
|
63
154
|
const file = await resolveProjectOutput(root, approvalFile);
|
|
64
|
-
await mkdir(path.dirname(file), {
|
|
155
|
+
await mkdir(path.dirname(file), {
|
|
156
|
+
recursive: true,
|
|
157
|
+
});
|
|
65
158
|
const lock = await open(file + ".lock", "wx", 0o600);
|
|
66
159
|
try {
|
|
67
160
|
const previous = await readApprovals(root);
|
|
68
|
-
if (previous.records.length >= 10000)
|
|
161
|
+
if (previous.records.length >= 10000) {
|
|
69
162
|
throw new Error("Approval document record limit reached");
|
|
163
|
+
}
|
|
70
164
|
const image = await resolveProjectOutput(root, record.image);
|
|
71
|
-
await mkdir(path.dirname(image), {
|
|
165
|
+
await mkdir(path.dirname(image), {
|
|
166
|
+
recursive: true,
|
|
167
|
+
});
|
|
72
168
|
try {
|
|
73
169
|
await atomicWriteFile(image, exported.png);
|
|
74
170
|
}
|
|
75
171
|
catch (error) {
|
|
76
|
-
if (error.code !== "EEXIST" ||
|
|
172
|
+
if (error.code !== "EEXIST" ||
|
|
173
|
+
artifactSha(await readFile(image)) !== sha256) {
|
|
77
174
|
throw error;
|
|
175
|
+
}
|
|
78
176
|
}
|
|
79
|
-
const next = JSON.stringify({
|
|
80
|
-
|
|
177
|
+
const next = JSON.stringify({
|
|
178
|
+
...previous,
|
|
179
|
+
records: [
|
|
180
|
+
...previous.records,
|
|
181
|
+
record,
|
|
182
|
+
],
|
|
183
|
+
}, null, 2) + "\n";
|
|
184
|
+
if (Buffer.byteLength(next) > 16 * 1024 * 1024) {
|
|
81
185
|
throw new Error("Approval document byte limit reached");
|
|
82
|
-
|
|
186
|
+
}
|
|
187
|
+
await atomicWriteFile(file, next, {
|
|
188
|
+
replace: true,
|
|
189
|
+
});
|
|
83
190
|
return record;
|
|
84
191
|
}
|
|
85
192
|
finally {
|
|
@@ -89,11 +196,18 @@ export async function recordApproval(root, exported, words, intent, redact) {
|
|
|
89
196
|
}
|
|
90
197
|
export async function matchingApproval(root, capture, sha256, intent) {
|
|
91
198
|
const fingerprint = approvalFingerprint(capture, sha256, intent);
|
|
92
|
-
const record = [
|
|
93
|
-
|
|
199
|
+
const record = [
|
|
200
|
+
...(await readApprovals(root)).records,
|
|
201
|
+
]
|
|
202
|
+
.reverse()
|
|
203
|
+
.find((r) => r.fingerprint === fingerprint);
|
|
204
|
+
if (!record) {
|
|
94
205
|
return undefined;
|
|
206
|
+
}
|
|
95
207
|
const image = await resolveProjectInput(root, record.image);
|
|
96
|
-
if ((await stat(image)).size > 64 * 1024 * 1024 ||
|
|
208
|
+
if ((await stat(image)).size > 64 * 1024 * 1024 ||
|
|
209
|
+
artifactSha(await readFile(image)) !== sha256) {
|
|
97
210
|
throw new Error("Approved image bytes changed");
|
|
211
|
+
}
|
|
98
212
|
return record;
|
|
99
213
|
}
|
package/dist/atomic-output.js
CHANGED
|
@@ -16,13 +16,18 @@ export async function atomicWriteFile(destination, data, options = {}) {
|
|
|
16
16
|
}
|
|
17
17
|
await handle.close();
|
|
18
18
|
try {
|
|
19
|
-
if (options.replace)
|
|
19
|
+
if (options.replace) {
|
|
20
20
|
await rename(temporary, destination);
|
|
21
|
-
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
22
23
|
await link(temporary, destination);
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
finally {
|
|
25
|
-
await unlink(temporary).catch((error) => {
|
|
26
|
-
|
|
27
|
+
await unlink(temporary).catch((error) => {
|
|
28
|
+
if (error.code !== "ENOENT") {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
27
32
|
}
|
|
28
33
|
}
|