@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.
Files changed (101) hide show
  1. package/dist/adapters.js +58 -11
  2. package/dist/approvals.js +143 -29
  3. package/dist/atomic-output.js +9 -4
  4. package/dist/audit-rules.js +345 -79
  5. package/dist/backup-note.js +29 -8
  6. package/dist/baseline-coverage.js +169 -35
  7. package/dist/baselines-config.js +119 -29
  8. package/dist/baselines.js +280 -57
  9. package/dist/cli-arguments.js +18 -6
  10. package/dist/cli-main.js +1621 -392
  11. package/dist/client.d.ts +5 -2
  12. package/dist/client.js +127 -28
  13. package/dist/command-result.js +341 -52
  14. package/dist/coordination.js +91 -29
  15. package/dist/credentials.js +57 -13
  16. package/dist/deep-check.js +56 -12
  17. package/dist/design-spec.js +189 -49
  18. package/dist/endpoint.js +102 -34
  19. package/dist/export.js +86 -18
  20. package/dist/file-archive.js +33 -7
  21. package/dist/file-guard.js +29 -11
  22. package/dist/formats.js +215 -14
  23. package/dist/index.d.ts +8 -7
  24. package/dist/index.js +8 -7
  25. package/dist/inspection.d.ts +1 -1
  26. package/dist/inspection.js +378 -65
  27. package/dist/instance/config-schema.js +400 -41
  28. package/dist/instance/config.js +101 -30
  29. package/dist/instance/connect.js +151 -44
  30. package/dist/instance/connection.d.ts +26 -5
  31. package/dist/instance/connection.js +194 -21
  32. package/dist/instance/gitignore.js +5 -2
  33. package/dist/instance/init.js +32 -7
  34. package/dist/instance/runner.js +32 -10
  35. package/dist/instance/setup.js +46 -13
  36. package/dist/instance/tty.js +26 -8
  37. package/dist/journal.d.ts +10 -0
  38. package/dist/journal.js +212 -49
  39. package/dist/kit-smoke.js +82 -20
  40. package/dist/kit-source.js +10 -40
  41. package/dist/kit.js +102 -34
  42. package/dist/layout-policy.d.ts +2 -0
  43. package/dist/layout-policy.js +86 -0
  44. package/dist/layout.d.ts +7 -1
  45. package/dist/layout.js +27 -70
  46. package/dist/live-smoke.js +13 -4
  47. package/dist/local-profile.js +2 -1
  48. package/dist/mcp-server.js +360 -47
  49. package/dist/native-api.js +47 -11
  50. package/dist/onboard.js +33 -7
  51. package/dist/operation.d.ts +2 -2
  52. package/dist/operation.js +279 -69
  53. package/dist/page-session.d.ts +23 -0
  54. package/dist/page-session.js +142 -0
  55. package/dist/png.js +18 -3
  56. package/dist/portable-tokens.js +254 -57
  57. package/dist/probe.d.ts +1 -1
  58. package/dist/probe.js +48 -11
  59. package/dist/project-input.js +24 -10
  60. package/dist/project-snippets.js +18 -6
  61. package/dist/project-status.js +133 -24
  62. package/dist/registry.js +217 -60
  63. package/dist/run-file.js +129 -30
  64. package/dist/shots.js +108 -26
  65. package/dist/skill-instructions.js +91 -22
  66. package/dist/skills.js +129 -39
  67. package/dist/snippet.js +27 -7
  68. package/dist/snippets/read.js +8 -2
  69. package/dist/structure-snapshot.d.ts +12 -0
  70. package/dist/structure-snapshot.js +439 -85
  71. package/dist/token-edit.js +219 -64
  72. package/dist/token-projection.d.ts +56 -0
  73. package/dist/token-projection.js +286 -0
  74. package/dist/tokens-diff.js +125 -32
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +1 -1
  78. package/skills/penpair-design/SKILL.md +3 -0
  79. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  80. package/skills/penpair-design/references/project-facts.md +2 -0
  81. package/skills/penpair-handoff/SKILL.md +2 -0
  82. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  83. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  84. package/skills/penpair-recipes/SKILL.md +5 -0
  85. package/skills/penpair-setup/SKILL.md +3 -1
  86. package/dist/cli.d.ts +0 -2
  87. package/dist/cli.js +0 -3
  88. package/dist/docker/lifecycle.d.ts +0 -96
  89. package/dist/docker/lifecycle.js +0 -273
  90. package/dist/docker/secrets.d.ts +0 -11
  91. package/dist/docker/secrets.js +0 -38
  92. package/dist/docker/template.d.ts +0 -8
  93. package/dist/docker/template.js +0 -158
  94. package/dist/docker-profile.d.ts +0 -114
  95. package/dist/docker-profile.js +0 -18
  96. package/dist/instance/lifecycle.d.ts +0 -129
  97. package/dist/instance/lifecycle.js +0 -336
  98. package/dist/instance/secrets.d.ts +0 -11
  99. package/dist/instance/secrets.js +0 -38
  100. package/dist/instance/template.d.ts +0 -8
  101. 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", "snapshot", "approve", "mcp", "spec", "inspect", "probe", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "snippets", "run", "run-file", "kit", "recipes", "skills", "init", "setup", "connect",
11
- "up", "down", "open", "status", "doctor", "backup", "restore", "baselines", "config", "runs", "onboard", "help",
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) || specifier.split("/").includes(".."))
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`), { code: "ERR_MODULE_NOT_FOUND" });
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
- else if (taken.has(command.name))
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
- else
96
+ }
97
+ else {
58
98
  taken.set(command.name, adapter.name);
99
+ }
59
100
  }
60
- adapters.push({ ...adapter, commands: adapter.commands.filter((c) => taken.get(c.name) === adapter.name) });
101
+ adapters.push({
102
+ ...adapter,
103
+ commands: adapter.commands.filter((c) => taken.get(c.name) === adapter.name),
104
+ });
61
105
  }
62
- return { adapters, problems };
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 || typeof capture.fileId !== "string" || !capture.fileId || typeof capture.pageId !== "string" || !capture.pageId || typeof capture.frameId !== "string" || !capture.frameId || !Number.isInteger(capture.revision) || !Number.isFinite(capture.scale) || capture.scale <= 0 || !/^[a-f0-9]{64}$/.test(sha256))
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
- if (intent)
15
- validateBaselineCase(intent, message => { throw new Error(message); });
16
- return observationHash({ capture, sha256, case: intent ?? null });
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 || v.schemaVersion !== 1 || v.kind !== "design-approvals" || !Array.isArray(v.records) || v.records.length > 10000)
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 || r.schemaVersion !== 1 || r.kind !== "owner-words" || typeof r.id !== "string" || !r.id || ids.has(r.id) || typeof r.note !== "string" || !r.note.trim() || r.note.length > 4000 || typeof r.by !== "string" || !r.by.trim() || r.by.length > 200 || !Number.isFinite(Date.parse(r.recordedAt)) || typeof r.image !== "string" || !r.image)
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) || r.fingerprint !== approvalFingerprint(r.capture, r.sha256, r.case))
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 { schemaVersion: 1, kind: "design-approvals", records: [] };
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 = { ...exported, capture: structuredClone(exported.capture), png: Buffer.from(exported.png) };
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 = { ...words };
51
- if (!exported.stable || !pngSize(exported.png))
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
- if (typeof words.note !== "string" || !words.note.trim() || words.note.length > 4000 || typeof words.by !== "string" || !words.by.trim() || words.by.length > 200)
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
- const sha256 = artifactSha(exported.png), fingerprint = approvalFingerprint(exported.capture, sha256, intent);
56
- if (intent && (!exported.capture.designSize || exported.capture.designSize.width !== intent.viewport.width || exported.capture.designSize.height !== intent.viewport.height || intent.activeThemeIds && observationHash([...(exported.capture.activeThemeIds ?? [])].sort()) !== observationHash([...intent.activeThemeIds].sort())))
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
- const body = { schemaVersion: 1, id: randomUUID(), kind: "owner-words", recordedAt: new Date().toISOString(), by: words.by, note: words.note, capture: exported.capture, ...(intent ? { case: intent } : {}), sha256, image: `design/approvals/${sha256}.png`, fingerprint };
59
- const record = { ...body, hash: observationHash(body) };
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), { recursive: true });
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), { recursive: true });
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" || artifactSha(await readFile(image)) !== sha256)
172
+ if (error.code !== "EEXIST" ||
173
+ artifactSha(await readFile(image)) !== sha256) {
77
174
  throw error;
175
+ }
78
176
  }
79
- const next = JSON.stringify({ ...previous, records: [...previous.records, record] }, null, 2) + "\n";
80
- if (Buffer.byteLength(next) > 16 * 1024 * 1024)
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
- await atomicWriteFile(file, next, { replace: true });
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 = [...(await readApprovals(root)).records].reverse().find(r => r.fingerprint === fingerprint);
93
- if (!record)
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 || artifactSha(await readFile(image)) !== sha256)
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
  }
@@ -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
- else
21
+ }
22
+ else {
22
23
  await link(temporary, destination);
24
+ }
23
25
  }
24
26
  finally {
25
- await unlink(temporary).catch((error) => { if (error.code !== "ENOENT")
26
- throw error; });
27
+ await unlink(temporary).catch((error) => {
28
+ if (error.code !== "ENOENT") {
29
+ throw error;
30
+ }
31
+ });
27
32
  }
28
33
  }