@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
@@ -1,112 +1,213 @@
1
1
  import { snippet } from "./snippet.js";
2
- export const tokenTypes = ["borderRadius", "shadow", "color", "dimension", "fontFamilies", "fontSizes", "fontWeights", "letterSpacing", "number", "opacity", "rotation", "sizing", "spacing", "borderWidth", "textCase", "textDecoration", "typography"];
2
+ export const tokenTypes = [
3
+ "borderRadius",
4
+ "shadow",
5
+ "color",
6
+ "dimension",
7
+ "fontFamilies",
8
+ "fontSizes",
9
+ "fontWeights",
10
+ "letterSpacing",
11
+ "number",
12
+ "opacity",
13
+ "rotation",
14
+ "sizing",
15
+ "spacing",
16
+ "borderWidth",
17
+ "textCase",
18
+ "textDecoration",
19
+ "typography",
20
+ ];
3
21
  export function validateEditableToken(value) {
4
- if (!value || typeof value !== "object" || Array.isArray(value))
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
23
  throw new Error("Token must be an object");
24
+ }
6
25
  const token = value;
7
- if (Object.keys(token).some(key => !["name", "type", "value", "description"].includes(key)))
26
+ if (Object.keys(token).some((key) => ![
27
+ "name",
28
+ "type",
29
+ "value",
30
+ "description",
31
+ ].includes(key))) {
8
32
  throw new Error("Unknown token field");
9
- if (typeof token.name !== "string" || !token.name.trim() || token.name.length > 200 || /[{}\x00-\x1f]/.test(token.name))
33
+ }
34
+ if (typeof token.name !== "string" ||
35
+ !token.name.trim() ||
36
+ token.name.length > 200 ||
37
+ /[{}\x00-\x1f]/.test(token.name)) {
10
38
  throw new Error("Token name must be nonempty, at most 200 characters and contain no braces or control characters");
11
- if (!tokenTypes.includes(token.type))
39
+ }
40
+ if (!tokenTypes.includes(token.type)) {
12
41
  throw new Error("Unsupported token type");
13
- if (!(typeof token.value === "string" && token.value.length > 0 || token.value && typeof token.value === "object" && !Array.isArray(token.value) && Object.keys(token.value).length > 0 && Object.values(token.value).every(v => typeof v === "string")))
42
+ }
43
+ if (!((typeof token.value === "string" && token.value.length > 0) ||
44
+ (token.value &&
45
+ typeof token.value === "object" &&
46
+ !Array.isArray(token.value) &&
47
+ Object.keys(token.value).length > 0 &&
48
+ Object.values(token.value).every((v) => typeof v === "string")))) {
14
49
  throw new Error("Token value must be a nonempty string or composite string-valued object");
15
- if (token.description !== undefined && typeof token.description !== "string")
50
+ }
51
+ if (token.description !== undefined && typeof token.description !== "string") {
16
52
  throw new Error("Token description must be a string");
17
- if (Buffer.byteLength(JSON.stringify(token)) > 16384)
53
+ }
54
+ if (Buffer.byteLength(JSON.stringify(token)) > 16384) {
18
55
  throw new Error("Token exceeds 16 KiB");
19
- return { name: token.name, type: token.type, value: token.value, description: token.description ?? "" };
56
+ }
57
+ return {
58
+ name: token.name,
59
+ type: token.type,
60
+ value: token.value,
61
+ description: token.description ?? "",
62
+ };
20
63
  }
21
64
  export function validateTokenEdit(edit) {
22
- if (!["add-set", "set", "remove", "promote"].includes(edit.kind))
65
+ if (![
66
+ "add-set",
67
+ "set",
68
+ "remove",
69
+ "promote",
70
+ ].includes(edit.kind)) {
23
71
  throw new Error("Unknown token edit");
24
- for (const name of [edit.set, ...(edit.kind === "promote" ? [edit.to] : [])])
25
- if (typeof name !== "string" || !name.trim() || name.length > 200 || /[\x00-\x1f]/.test(name))
72
+ }
73
+ for (const name of [
74
+ edit.set,
75
+ ...(edit.kind === "promote"
76
+ ? [
77
+ edit.to,
78
+ ]
79
+ : []),
80
+ ]) {
81
+ if (typeof name !== "string" || !name.trim() || name.length > 200 || /[\x00-\x1f]/.test(name)) {
26
82
  throw new Error("Set names must be nonempty and at most 200 characters without control characters");
27
- if (edit.kind === "set")
28
- return { ...edit, token: validateEditableToken(edit.token) };
29
- if (edit.kind === "remove" && (typeof edit.name !== "string" || !edit.name.trim()))
83
+ }
84
+ }
85
+ if (edit.kind === "set") {
86
+ return {
87
+ ...edit,
88
+ token: validateEditableToken(edit.token),
89
+ };
90
+ }
91
+ if (edit.kind === "remove" && (typeof edit.name !== "string" || !edit.name.trim())) {
30
92
  throw new Error("Remove requires a token name");
31
- if (edit.kind === "promote" && edit.set === edit.to)
93
+ }
94
+ if (edit.kind === "promote" && edit.set === edit.to) {
32
95
  throw new Error("Promotion requires distinct source and destination sets");
96
+ }
33
97
  return edit;
34
98
  }
35
99
  /** Pure desired-state planner. Kept self-contained so the delivered mutation rechecks the same facts. */
36
100
  export function planTokenEdit(before, edit) {
37
101
  const canonical = (value) => {
38
- if (Array.isArray(value))
102
+ if (Array.isArray(value)) {
39
103
  return "[" + value.map(canonical).join(",") + "]";
40
- if (value && typeof value === "object")
41
- return "{" + Object.keys(value).sort().map(key => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
104
+ }
105
+ if (value && typeof value === "object") {
106
+ return ("{" +
107
+ Object.keys(value)
108
+ .sort()
109
+ .map((key) => JSON.stringify(key) + ":" + canonical(value[key]))
110
+ .join(",") +
111
+ "}");
112
+ }
42
113
  return JSON.stringify(value);
43
114
  };
44
115
  const after = JSON.parse(JSON.stringify(before));
45
- if (!Array.isArray(after.sets) || !Array.isArray(after.themes) || !Array.isArray(after.bindings))
116
+ if (!Array.isArray(after.sets) ||
117
+ !Array.isArray(after.themes) ||
118
+ !Array.isArray(after.bindings)) {
46
119
  throw new Error("Missing token observations");
120
+ }
47
121
  const names = new Set();
48
122
  for (const set of after.sets) {
49
- if (names.has(set.name))
123
+ if (names.has(set.name)) {
50
124
  throw new Error("Ambiguous set: " + set.name);
125
+ }
51
126
  names.add(set.name);
52
127
  const tokens = new Set();
53
128
  for (const token of set.tokens) {
54
- if (tokens.has(token.name))
129
+ if (tokens.has(token.name)) {
55
130
  throw new Error("Ambiguous token: " + token.name);
131
+ }
56
132
  tokens.add(token.name);
57
133
  }
58
134
  }
59
135
  const impact = [];
60
- const set = after.sets.find(s => s.name === edit.set);
136
+ const set = after.sets.find((s) => s.name === edit.set);
61
137
  if (edit.kind === "add-set") {
62
- if (!set)
63
- after.sets.push({ name: edit.set, active: false, tokens: [] });
138
+ if (!set) {
139
+ after.sets.push({
140
+ name: edit.set,
141
+ active: false,
142
+ tokens: [],
143
+ });
144
+ }
64
145
  }
65
146
  else {
66
- if (!set)
147
+ if (!set) {
67
148
  throw new Error("Missing set: " + edit.set);
149
+ }
68
150
  if (edit.kind === "set") {
69
- const token = set.tokens.find(t => t.name === edit.token.name);
70
- if (token && token.type !== edit.token.type)
151
+ const token = set.tokens.find((t) => t.name === edit.token.name);
152
+ if (token && token.type !== edit.token.type) {
71
153
  throw new Error("Type replacement requires a separate reviewed removal");
72
- if (token)
154
+ }
155
+ if (token) {
73
156
  Object.assign(token, edit.token);
74
- else
157
+ }
158
+ else {
75
159
  set.tokens.push(edit.token);
76
- for (const row of before.bindings)
77
- if (Object.values(row.tokens).includes(edit.token.name))
160
+ }
161
+ for (const row of before.bindings) {
162
+ if (Object.values(row.tokens).includes(edit.token.name)) {
78
163
  impact.push(row.page + "/" + row.id);
164
+ }
165
+ }
79
166
  }
80
167
  else if (edit.kind === "remove") {
81
168
  // Conservatively refuse even when another active set might supply the same name.
82
- if (set.tokens.some(t => t.name === edit.name)) {
169
+ if (set.tokens.some((t) => t.name === edit.name)) {
83
170
  const pattern = "{" + edit.name + "}";
84
- for (const source of after.sets)
85
- for (const token of source.tokens)
86
- if (!(source.name === set.name && token.name === edit.name) && JSON.stringify(token.value).includes(pattern))
171
+ for (const source of after.sets) {
172
+ for (const token of source.tokens) {
173
+ if (!(source.name === set.name && token.name === edit.name) &&
174
+ JSON.stringify(token.value).includes(pattern)) {
87
175
  throw new Error("Retained reference from " + source.name + "/" + token.name);
88
- if (before.bindings.some(row => Object.values(row.tokens).includes(edit.name)))
176
+ }
177
+ }
178
+ }
179
+ if (before.bindings.some((row) => Object.values(row.tokens).includes(edit.name))) {
89
180
  throw new Error("Token is bound to a retained shape");
90
- set.tokens = set.tokens.filter(t => t.name !== edit.name);
181
+ }
182
+ set.tokens = set.tokens.filter((t) => t.name !== edit.name);
91
183
  }
92
184
  }
93
185
  else {
94
- const target = after.sets.find(s => s.name === edit.to);
95
- if (!target)
186
+ const target = after.sets.find((s) => s.name === edit.to);
187
+ if (!target) {
96
188
  throw new Error("Missing promotion destination: " + edit.to);
189
+ }
97
190
  for (const token of set.tokens) {
98
- const existing = target.tokens.find(t => t.name === token.name);
99
- if (existing && canonical(existing) !== canonical(token))
191
+ const existing = target.tokens.find((t) => t.name === token.name);
192
+ if (existing && canonical(existing) !== canonical(token)) {
100
193
  throw new Error("Promotion conflicts with destination token: " + token.name);
101
- if (!existing)
194
+ }
195
+ if (!existing) {
102
196
  target.tokens.push(token);
197
+ }
103
198
  }
104
199
  impact.push("Copies tokens; source, themes and active selection are preserved");
105
200
  }
106
201
  }
107
- for (const set of after.sets)
108
- set.tokens.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
109
- return { before, after, changed: canonical(before) !== canonical(after), impact };
202
+ for (const set of after.sets) {
203
+ set.tokens.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
204
+ }
205
+ return {
206
+ before,
207
+ after,
208
+ changed: canonical(before) !== canonical(after),
209
+ impact,
210
+ };
110
211
  }
111
212
  // Snapshot getter-backed API fields explicitly; refuse partial/oversized observations before any edit.
112
213
  const observe = `
@@ -131,33 +232,75 @@ const observe = () => {
131
232
  return state;
132
233
  };`;
133
234
  const canonical = (value) => {
134
- if (Array.isArray(value))
235
+ if (Array.isArray(value)) {
135
236
  return "[" + value.map(canonical).join(",") + "]";
136
- if (value && typeof value === "object")
137
- return "{" + Object.keys(value).sort().map(key => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
237
+ }
238
+ if (value && typeof value === "object") {
239
+ return ("{" +
240
+ Object.keys(value)
241
+ .sort()
242
+ .map((key) => JSON.stringify(key) + ":" + canonical(value[key]))
243
+ .join(",") +
244
+ "}");
245
+ }
138
246
  return JSON.stringify(value);
139
247
  };
140
248
  function comparable(state) {
141
- return { ...state, sets: state.sets.map(set => ({ ...set, tokens: set.tokens.map(token => {
249
+ return {
250
+ ...state,
251
+ sets: state.sets.map((set) => ({
252
+ ...set,
253
+ tokens: set.tokens.map((token) => {
142
254
  let value = token.value;
143
- if (token.type === "fontFamilies")
144
- value = Array.isArray(value) ? value : [value];
145
- if (token.type === "typography" && value && typeof value === "object" && !Array.isArray(value)) {
255
+ if (token.type === "fontFamilies") {
256
+ value = Array.isArray(value)
257
+ ? value
258
+ : [
259
+ value,
260
+ ];
261
+ }
262
+ if (token.type === "typography" &&
263
+ value &&
264
+ typeof value === "object" &&
265
+ !Array.isArray(value)) {
146
266
  const { fontFamilies, fontFamily, ...rest } = value;
147
267
  const family = fontFamily ?? fontFamilies;
148
- value = { ...rest, ...(family === undefined ? {} : { fontFamily: Array.isArray(family) ? family : [family] }) };
268
+ value = {
269
+ ...rest,
270
+ ...(family === undefined
271
+ ? {}
272
+ : {
273
+ fontFamily: Array.isArray(family)
274
+ ? family
275
+ : [
276
+ family,
277
+ ],
278
+ }),
279
+ };
149
280
  }
150
- return { ...token, value };
151
- }) })) };
281
+ return {
282
+ ...token,
283
+ value,
284
+ };
285
+ }),
286
+ })),
287
+ };
152
288
  }
153
289
  const stateFrom = (result) => {
154
- if (result.ok !== true || !result.state)
290
+ if (result.ok !== true || !result.state) {
155
291
  throw new Error("No independent token-state evidence");
292
+ }
156
293
  return result.state;
157
294
  };
158
295
  export const tokenEditOperation = {
159
- name: "tokens-edit", page: () => undefined,
160
- inspect(edit) { edit = validateTokenEdit(edit); return snippet("tokens-edit-inspect", `${observe}\nconst state=observe();const plan=(${planTokenEdit.toString()})(state,CFG.edit);return {ok:true,readOnly:true,state,plan};`, { edit }); },
296
+ name: "tokens-edit",
297
+ page: () => undefined,
298
+ inspect(edit) {
299
+ edit = validateTokenEdit(edit);
300
+ return snippet("tokens-edit-inspect", `${observe}\nconst state=observe();const plan=(${planTokenEdit.toString()})(state,CFG.edit);return {ok:true,readOnly:true,state,plan};`, {
301
+ edit,
302
+ });
303
+ },
161
304
  apply(edit, inspected) {
162
305
  edit = validateTokenEdit(edit);
163
306
  return snippet("tokens-edit-apply", `${observe}
@@ -180,18 +323,30 @@ if(plan.changed){
180
323
  }
181
324
  }
182
325
  }
183
- return {ok:true,changed:plan.changed};`, { edit, before: inspected?.state ?? null });
326
+ return {ok:true,changed:plan.changed};`, {
327
+ edit,
328
+ before: inspected?.state ?? null,
329
+ });
184
330
  },
185
331
  readback: () => snippet("tokens-edit-readback", `${observe}\nreturn {ok:true,readOnly:true,state:observe()};`),
186
332
  verify({ inspect, readback }) {
187
333
  try {
188
334
  const plan = inspect.plan;
189
- if (!plan?.after)
190
- return ["Missing original desired-state plan"];
191
- return canonical(comparable(stateFrom(readback))) === canonical(comparable(plan.after)) ? [] : ["Independent token state differs from the inspected plan"];
335
+ if (!plan?.after) {
336
+ return [
337
+ "Missing original desired-state plan",
338
+ ];
339
+ }
340
+ return canonical(comparable(stateFrom(readback))) === canonical(comparable(plan.after))
341
+ ? []
342
+ : [
343
+ "Independent token state differs from the inspected plan",
344
+ ];
192
345
  }
193
346
  catch (error) {
194
- return [String(error)];
347
+ return [
348
+ String(error),
349
+ ];
195
350
  }
196
351
  },
197
352
  };
@@ -0,0 +1,56 @@
1
+ export interface DeclaredToken {
2
+ name: string;
3
+ type: string;
4
+ value: unknown;
5
+ }
6
+ export interface ProjectionCatalog {
7
+ sets: Array<{
8
+ id?: string;
9
+ name: string;
10
+ tokens: DeclaredToken[];
11
+ }>;
12
+ themes: Array<{
13
+ id?: string;
14
+ group: string;
15
+ name: string;
16
+ sets: string[];
17
+ }>;
18
+ }
19
+ export interface ResolvedDeclaredToken {
20
+ name: string;
21
+ type: string;
22
+ value?: unknown;
23
+ unknown?: string;
24
+ }
25
+ /** A small arithmetic grammar, never JavaScript evaluation. Unsupported units/functions are explicit unknowns. */
26
+ export declare function numericExpression(source: string): number;
27
+ /** Catalog order is authoritative; theme membership does not reorder sets. No live API is touched. */
28
+ export declare function resolveDeclaredTokens(catalog: Pick<ProjectionCatalog, "sets">, selectedSets: string[]): ResolvedDeclaredToken[];
29
+ export interface ProjectedThemePreview {
30
+ schemaVersion: 1;
31
+ kind: "projected-theme-preview";
32
+ sourceHash: string;
33
+ theme: {
34
+ id: string | null;
35
+ group: string;
36
+ name: string;
37
+ };
38
+ sets: Array<{
39
+ id: string | null;
40
+ name: string;
41
+ order: number;
42
+ }>;
43
+ resolved: Array<{
44
+ name: string;
45
+ type: string;
46
+ value: unknown;
47
+ }>;
48
+ unknowns: Array<{
49
+ name: string;
50
+ type: string;
51
+ reason: string;
52
+ }>;
53
+ complete: boolean;
54
+ nativeBindings: false;
55
+ }
56
+ export declare function projectTheme(catalog: ProjectionCatalog, themeName: string): ProjectedThemePreview;