@xenonbyte/da-vinci-workflow 0.1.21 → 0.1.23

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.
@@ -0,0 +1,361 @@
1
+ const fs = require("fs");
2
+ const https = require("https");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ MATERIAL_ROUNDED,
7
+ MATERIAL_OUTLINED,
8
+ MATERIAL_SHARP
9
+ } = require("./icon-search");
10
+
11
+ const MATERIAL_METADATA_URL = "https://fonts.google.com/metadata/icons";
12
+ const LUCIDE_TREE_URL = "https://api.github.com/repos/lucide-icons/lucide/git/trees/main?recursive=1";
13
+ const FEATHER_TREE_URL = "https://api.github.com/repos/feathericons/feather/git/trees/main?recursive=1";
14
+ const PHOSPHOR_TREE_URL = "https://api.github.com/repos/phosphor-icons/core/git/trees/main?recursive=1";
15
+ const DEFAULT_TIMEOUT_MS = 20000;
16
+
17
+ function toBoolean(value) {
18
+ if (value === true || value === false) {
19
+ return value;
20
+ }
21
+ if (typeof value === "string") {
22
+ const normalized = value.trim().toLowerCase();
23
+ if (["1", "true", "yes", "on"].includes(normalized)) {
24
+ return true;
25
+ }
26
+ if (["0", "false", "no", "off"].includes(normalized)) {
27
+ return false;
28
+ }
29
+ }
30
+ return Boolean(value);
31
+ }
32
+
33
+ function ensureDir(filePath) {
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ }
36
+
37
+ function normalizeName(name) {
38
+ return String(name || "").trim();
39
+ }
40
+
41
+ function makeIconRecord(family, name) {
42
+ const normalized = normalizeName(name);
43
+ if (!family || !normalized) {
44
+ return null;
45
+ }
46
+ return {
47
+ family,
48
+ name: normalized,
49
+ semantic: normalized.replace(/[_-]+/g, " "),
50
+ tags: []
51
+ };
52
+ }
53
+
54
+ function parseGoogleMaterialMetadata(raw) {
55
+ const text = String(raw || "");
56
+ const objectStart = text.indexOf("{");
57
+ if (objectStart < 0) {
58
+ throw new Error("Material metadata payload is not valid JSON.");
59
+ }
60
+ const parsed = JSON.parse(text.slice(objectStart));
61
+ const icons = Array.isArray(parsed.icons) ? parsed.icons : [];
62
+ const roundedFamilyKey = "Material Icons Round";
63
+ const outlinedFamilyKey = "Material Icons Outlined";
64
+ const sharpFamilyKey = "Material Icons Sharp";
65
+
66
+ return icons.flatMap((item) => {
67
+ const name = normalizeName(item && item.name);
68
+ if (!name) {
69
+ return [];
70
+ }
71
+ const unsupported = new Set(Array.isArray(item.unsupported_families) ? item.unsupported_families : []);
72
+ const records = [];
73
+
74
+ if (!unsupported.has(roundedFamilyKey)) {
75
+ records.push(makeIconRecord(MATERIAL_ROUNDED, name));
76
+ }
77
+ if (!unsupported.has(outlinedFamilyKey)) {
78
+ records.push(makeIconRecord(MATERIAL_OUTLINED, name));
79
+ }
80
+ if (!unsupported.has(sharpFamilyKey)) {
81
+ records.push(makeIconRecord(MATERIAL_SHARP, name));
82
+ }
83
+
84
+ return records.filter(Boolean);
85
+ });
86
+ }
87
+
88
+ function parseGitHubTreeIcons(raw, options) {
89
+ const { family, prefix, suffix } = options;
90
+ const parsed = JSON.parse(String(raw || ""));
91
+ const tree = Array.isArray(parsed.tree) ? parsed.tree : [];
92
+
93
+ return tree
94
+ .flatMap((node) => {
95
+ const nodePath = normalizeName(node && node.path);
96
+ if (!nodePath || !nodePath.startsWith(prefix) || !nodePath.endsWith(suffix)) {
97
+ return [];
98
+ }
99
+ const name = nodePath.slice(prefix.length, nodePath.length - suffix.length);
100
+ if (!name || name.includes("/")) {
101
+ return [];
102
+ }
103
+ const record = makeIconRecord(family, name);
104
+ return record ? [record] : [];
105
+ });
106
+ }
107
+
108
+ function dedupeIconRecords(records) {
109
+ const map = new Map();
110
+ for (const record of records) {
111
+ if (!record) {
112
+ continue;
113
+ }
114
+ const key = `${record.family}::${record.name}`;
115
+ if (!map.has(key)) {
116
+ map.set(key, record);
117
+ }
118
+ }
119
+ return Array.from(map.values());
120
+ }
121
+
122
+ function summarizeSourceResults(sourceResults = {}) {
123
+ const entries = Object.values(sourceResults);
124
+ const total = entries.length;
125
+ const errorCount = entries.filter((source) => source && source.status === "error").length;
126
+ const okCount = entries.filter((source) => source && source.status === "ok").length;
127
+ return {
128
+ total,
129
+ okCount,
130
+ errorCount,
131
+ degraded: errorCount > 0
132
+ };
133
+ }
134
+
135
+ function fetchText(url, options = {}) {
136
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
137
+ ? Number(options.timeoutMs)
138
+ : DEFAULT_TIMEOUT_MS;
139
+
140
+ return new Promise((resolve, reject) => {
141
+ const request = https.get(
142
+ url,
143
+ {
144
+ headers: {
145
+ "User-Agent": "da-vinci-workflow/icon-sync",
146
+ Accept: "application/json,text/plain,*/*"
147
+ }
148
+ },
149
+ (response) => {
150
+ let data = "";
151
+ response.setEncoding("utf8");
152
+ response.on("data", (chunk) => {
153
+ data += chunk;
154
+ });
155
+ response.on("end", () => {
156
+ if (response.statusCode && response.statusCode >= 400) {
157
+ reject(new Error(`HTTP ${response.statusCode} for ${url}`));
158
+ return;
159
+ }
160
+ resolve(data);
161
+ });
162
+ }
163
+ );
164
+
165
+ request.on("error", (error) => {
166
+ reject(error);
167
+ });
168
+
169
+ request.setTimeout(timeoutMs, () => {
170
+ request.destroy(new Error(`Request timeout after ${timeoutMs}ms for ${url}`));
171
+ });
172
+ });
173
+ }
174
+
175
+ function getDefaultCatalogPath(homeDir) {
176
+ const root = homeDir ? path.resolve(homeDir) : os.homedir();
177
+ return path.join(root, ".da-vinci", "icon-catalog.json");
178
+ }
179
+
180
+ function resolveCatalogPath(options = {}) {
181
+ if (options.catalogPath) {
182
+ return path.resolve(options.catalogPath);
183
+ }
184
+ if (options.outputPath) {
185
+ return path.resolve(options.outputPath);
186
+ }
187
+ return path.resolve(getDefaultCatalogPath(options.homeDir));
188
+ }
189
+
190
+ function loadIconCatalog(options = {}) {
191
+ const catalogPath = resolveCatalogPath(options);
192
+ if (!fs.existsSync(catalogPath)) {
193
+ return {
194
+ catalogPath,
195
+ catalog: null
196
+ };
197
+ }
198
+
199
+ const parsed = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
200
+ if (!parsed || !Array.isArray(parsed.icons)) {
201
+ throw new Error(`Invalid icon catalog format: ${catalogPath}`);
202
+ }
203
+
204
+ return {
205
+ catalogPath,
206
+ catalog: parsed
207
+ };
208
+ }
209
+
210
+ async function syncIconCatalog(options = {}) {
211
+ const catalogPath = resolveCatalogPath(options);
212
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
213
+ ? Number(options.timeoutMs)
214
+ : DEFAULT_TIMEOUT_MS;
215
+ const strict = toBoolean(options.strict);
216
+ const fetchTextImpl = typeof options.fetchText === "function" ? options.fetchText : fetchText;
217
+
218
+ const sourceSpecs = [
219
+ {
220
+ key: "material",
221
+ url: MATERIAL_METADATA_URL,
222
+ parse: parseGoogleMaterialMetadata
223
+ },
224
+ {
225
+ key: "lucide",
226
+ url: LUCIDE_TREE_URL,
227
+ parse: (raw) =>
228
+ parseGitHubTreeIcons(raw, {
229
+ family: "lucide",
230
+ prefix: "icons/",
231
+ suffix: ".json"
232
+ })
233
+ },
234
+ {
235
+ key: "feather",
236
+ url: FEATHER_TREE_URL,
237
+ parse: (raw) =>
238
+ parseGitHubTreeIcons(raw, {
239
+ family: "feather",
240
+ prefix: "icons/",
241
+ suffix: ".svg"
242
+ })
243
+ },
244
+ {
245
+ key: "phosphor",
246
+ url: PHOSPHOR_TREE_URL,
247
+ parse: (raw) =>
248
+ parseGitHubTreeIcons(raw, {
249
+ family: "phosphor",
250
+ prefix: "assets/regular/",
251
+ suffix: ".svg"
252
+ })
253
+ }
254
+ ];
255
+
256
+ const sourceResults = {};
257
+ const collected = [];
258
+
259
+ await Promise.all(
260
+ sourceSpecs.map(async (spec) => {
261
+ try {
262
+ const raw = await fetchTextImpl(spec.url, {
263
+ timeoutMs
264
+ });
265
+ const records = spec.parse(raw);
266
+ sourceResults[spec.key] = {
267
+ status: "ok",
268
+ url: spec.url,
269
+ count: records.length
270
+ };
271
+ collected.push(...records);
272
+ } catch (error) {
273
+ sourceResults[spec.key] = {
274
+ status: "error",
275
+ url: spec.url,
276
+ count: 0,
277
+ error: error.message || String(error)
278
+ };
279
+ }
280
+ })
281
+ );
282
+
283
+ const icons = dedupeIconRecords(collected);
284
+ const sourceSummary = summarizeSourceResults(sourceResults);
285
+ if (icons.length === 0) {
286
+ throw new Error(
287
+ [
288
+ "icon-sync failed: no icon records were fetched.",
289
+ ...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
290
+ ].join("\n")
291
+ );
292
+ }
293
+ if (strict && sourceSummary.errorCount > 0) {
294
+ throw new Error(
295
+ [
296
+ "icon-sync strict mode failed: one or more upstream sources could not be fetched.",
297
+ ...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
298
+ ].join("\n")
299
+ );
300
+ }
301
+
302
+ const catalog = {
303
+ schema: 1,
304
+ generatedAt: new Date().toISOString(),
305
+ sourceResults,
306
+ sourceSummary,
307
+ syncStatus: sourceSummary.degraded ? "degraded" : "ok",
308
+ iconCount: icons.length,
309
+ icons
310
+ };
311
+
312
+ ensureDir(catalogPath);
313
+ fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
314
+
315
+ return {
316
+ catalogPath,
317
+ catalog
318
+ };
319
+ }
320
+
321
+ function formatIconSyncReport(result) {
322
+ const sourceSummary = result.catalog.sourceSummary || summarizeSourceResults(result.catalog.sourceResults || {});
323
+ const statusLine = sourceSummary.degraded
324
+ ? `DEGRADED (${sourceSummary.errorCount}/${sourceSummary.total} source failures)`
325
+ : "OK";
326
+ const lines = [
327
+ "Icon Sync",
328
+ `Catalog path: ${result.catalogPath}`,
329
+ `Generated at: ${result.catalog.generatedAt}`,
330
+ `Status: ${statusLine}`,
331
+ `Icon count: ${result.catalog.iconCount}`,
332
+ "Sources:"
333
+ ];
334
+
335
+ for (const [key, source] of Object.entries(result.catalog.sourceResults || {})) {
336
+ const base = `- ${key}: ${source.status} (${source.count})`;
337
+ if (source.error) {
338
+ lines.push(`${base} ${source.error}`);
339
+ continue;
340
+ }
341
+ lines.push(base);
342
+ }
343
+
344
+ return lines.join("\n");
345
+ }
346
+
347
+ module.exports = {
348
+ MATERIAL_METADATA_URL,
349
+ LUCIDE_TREE_URL,
350
+ FEATHER_TREE_URL,
351
+ PHOSPHOR_TREE_URL,
352
+ getDefaultCatalogPath,
353
+ resolveCatalogPath,
354
+ loadIconCatalog,
355
+ syncIconCatalog,
356
+ formatIconSyncReport,
357
+ parseGoogleMaterialMetadata,
358
+ parseGitHubTreeIcons,
359
+ dedupeIconRecords,
360
+ summarizeSourceResults
361
+ };
@@ -0,0 +1,27 @@
1
+ function normalizeIconText(text) {
2
+ return String(text || "")
3
+ .normalize("NFKD")
4
+ .replace(/[\u0300-\u036f]/g, "")
5
+ .toLowerCase()
6
+ .replace(/[_/]+/g, " ")
7
+ .replace(/[^\p{L}\p{N}\s-]+/gu, " ")
8
+ .replace(/\s+/g, " ")
9
+ .trim();
10
+ }
11
+
12
+ function tokenizeIconText(text) {
13
+ const normalized = normalizeIconText(text);
14
+ if (!normalized) {
15
+ return [];
16
+ }
17
+
18
+ return normalized
19
+ .split(/[\s-]+/)
20
+ .map((token) => token.trim())
21
+ .filter(Boolean);
22
+ }
23
+
24
+ module.exports = {
25
+ normalizeIconText,
26
+ tokenizeIconText
27
+ };
package/lib/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const os = require("os");
4
+ const { listFilesRecursiveSafe } = require("./fs-safety");
4
5
 
5
6
  const REPO_ROOT = path.resolve(__dirname, "..");
6
7
  const PACKAGE_JSON = require(path.join(REPO_ROOT, "package.json"));
@@ -135,17 +136,24 @@ function tryRemoveEmptyDir(targetPath) {
135
136
  }
136
137
 
137
138
  function listFiles(dirPath) {
138
- return fs.readdirSync(dirPath, { withFileTypes: true }).flatMap((entry) => {
139
- if (entry.name.startsWith(".")) {
140
- return [];
141
- }
142
-
143
- const fullPath = path.join(dirPath, entry.name);
144
- if (entry.isDirectory()) {
145
- return listFiles(fullPath);
146
- }
147
- return [fullPath];
139
+ const scan = listFilesRecursiveSafe(dirPath, {
140
+ maxDepth: 24,
141
+ maxEntries: 25000,
142
+ includeDotfiles: false
148
143
  });
144
+
145
+ if (scan.readErrors.length > 0) {
146
+ throw new Error(`Unable to enumerate install assets under ${dirPath}: ${scan.readErrors[0].error}`);
147
+ }
148
+
149
+ if (scan.truncated) {
150
+ const reason = scan.entryLimitHit
151
+ ? `file limit ${scan.maxEntries} reached`
152
+ : `depth limit ${scan.maxDepth} reached`;
153
+ throw new Error(`Install asset enumeration exceeded safe traversal limits under ${dirPath}: ${reason}.`);
154
+ }
155
+
156
+ return scan.files;
149
157
  }
150
158
 
151
159
  function getMissingTargets(homeDir, relativePaths) {
@@ -9,6 +9,8 @@ const FAIL = "FAIL";
9
9
  const HARD_BATCH_LIMIT = 25;
10
10
  const ANCHOR_BATCH_WARNING_LIMIT = 12;
11
11
  const MICRO_BATCH_LIMIT = 6;
12
+ const PREVIEW_VM_TIMEOUT_MS = 750;
13
+ const MAX_OPERATION_SOURCE_BYTES = 80000;
12
14
 
13
15
  const VALID_HEX_COLOR = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
14
16
  const VARIABLE_REFERENCE = /^\$[A-Za-z0-9._-]+$/;
@@ -36,6 +38,25 @@ const CALLEE_RULES = {
36
38
  G: { minArgs: 3, maxArgs: 3 }
37
39
  };
38
40
 
41
+ const UNSAFE_OPERATION_PATTERNS = [
42
+ {
43
+ pattern: /\b(?:globalThis|global|process|require|module|exports|constructor|prototype|__proto__)\b/i,
44
+ message: "Operation batch references runtime globals or prototype internals, which are not allowed."
45
+ },
46
+ {
47
+ pattern: /\b(?:function|class|new|import|export|eval)\b/i,
48
+ message: "Operation batch contains executable code constructs; only declarative operation calls are allowed."
49
+ },
50
+ {
51
+ pattern: /=>/,
52
+ message: "Operation batch contains arrow functions; only declarative operation calls are allowed."
53
+ },
54
+ {
55
+ pattern: /;/,
56
+ message: "Operation batch contains statement separators; keep one operation expression per line."
57
+ }
58
+ ];
59
+
39
60
  function relativePath(basePath, targetPath) {
40
61
  return path.relative(basePath, targetPath) || ".";
41
62
  }
@@ -63,6 +84,59 @@ function normalizeLines(operations) {
63
84
  .filter(Boolean);
64
85
  }
65
86
 
87
+ function stripQuotedLiterals(sourceText) {
88
+ const source = String(sourceText || "");
89
+ let result = "";
90
+ let quote = null;
91
+ let escaped = false;
92
+
93
+ for (let index = 0; index < source.length; index += 1) {
94
+ const char = source[index];
95
+
96
+ if (!quote) {
97
+ if (char === "'" || char === '"' || char === "`") {
98
+ quote = char;
99
+ result += " ";
100
+ } else {
101
+ result += char;
102
+ }
103
+ continue;
104
+ }
105
+
106
+ if (escaped) {
107
+ escaped = false;
108
+ result += " ";
109
+ continue;
110
+ }
111
+
112
+ if (char === "\\") {
113
+ escaped = true;
114
+ result += " ";
115
+ continue;
116
+ }
117
+
118
+ if (char === quote) {
119
+ quote = null;
120
+ result += " ";
121
+ continue;
122
+ }
123
+
124
+ result += " ";
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ function detectUnsafeOperationSource(operations) {
131
+ const sanitizedSource = stripQuotedLiterals(operations);
132
+ for (const rule of UNSAFE_OPERATION_PATTERNS) {
133
+ if (rule.pattern.test(sanitizedSource)) {
134
+ return rule.message;
135
+ }
136
+ }
137
+ return "";
138
+ }
139
+
66
140
  function createIssue(level, message) {
67
141
  return { level, message };
68
142
  }
@@ -256,7 +330,7 @@ function simulateOperations(operations) {
256
330
  let syntheticId = 0;
257
331
 
258
332
  function makeStub(callee) {
259
- return (...args) => {
333
+ return Object.freeze((...args) => {
260
334
  calls.push({ callee, args });
261
335
 
262
336
  if (callee === "I" || callee === "C" || callee === "R") {
@@ -265,23 +339,68 @@ function simulateOperations(operations) {
265
339
  }
266
340
 
267
341
  return undefined;
268
- };
342
+ });
269
343
  }
270
344
 
271
- const context = {
272
- document: "document",
273
- I: makeStub("I"),
274
- C: makeStub("C"),
275
- U: makeStub("U"),
276
- R: makeStub("R"),
277
- M: makeStub("M"),
278
- D: makeStub("D"),
279
- G: makeStub("G")
280
- };
345
+ const context = Object.create(null);
346
+ Object.defineProperties(context, {
347
+ document: {
348
+ value: "document",
349
+ enumerable: true,
350
+ writable: false,
351
+ configurable: false
352
+ },
353
+ I: {
354
+ value: makeStub("I"),
355
+ enumerable: true,
356
+ writable: false,
357
+ configurable: false
358
+ },
359
+ C: {
360
+ value: makeStub("C"),
361
+ enumerable: true,
362
+ writable: false,
363
+ configurable: false
364
+ },
365
+ U: {
366
+ value: makeStub("U"),
367
+ enumerable: true,
368
+ writable: false,
369
+ configurable: false
370
+ },
371
+ R: {
372
+ value: makeStub("R"),
373
+ enumerable: true,
374
+ writable: false,
375
+ configurable: false
376
+ },
377
+ M: {
378
+ value: makeStub("M"),
379
+ enumerable: true,
380
+ writable: false,
381
+ configurable: false
382
+ },
383
+ D: {
384
+ value: makeStub("D"),
385
+ enumerable: true,
386
+ writable: false,
387
+ configurable: false
388
+ },
389
+ G: {
390
+ value: makeStub("G"),
391
+ enumerable: true,
392
+ writable: false,
393
+ configurable: false
394
+ }
395
+ });
281
396
 
282
397
  vm.runInNewContext(operations, context, {
283
- timeout: 1000,
284
- displayErrors: true
398
+ timeout: PREVIEW_VM_TIMEOUT_MS,
399
+ displayErrors: true,
400
+ contextCodeGeneration: {
401
+ strings: false,
402
+ wasm: false
403
+ }
285
404
  });
286
405
 
287
406
  return calls;
@@ -330,6 +449,7 @@ function preflightPencilBatch(operations, options = {}) {
330
449
  const notes = [];
331
450
  const warnings = [];
332
451
  const normalizedOps = String(operations || "");
452
+ const sourceByteLength = Buffer.byteLength(normalizedOps, "utf8");
333
453
  const nonEmptyLines = normalizeLines(normalizedOps);
334
454
  const opLineCount = nonEmptyLines.length;
335
455
 
@@ -356,17 +476,46 @@ function preflightPencilBatch(operations, options = {}) {
356
476
  }
357
477
 
358
478
  let calls = [];
359
- try {
360
- calls = simulateOperations(normalizedOps);
361
- } catch (error) {
479
+ if (sourceByteLength > MAX_OPERATION_SOURCE_BYTES) {
362
480
  issues.push(
363
481
  createIssue(
364
482
  "FAIL",
365
- `Pencil batch has invalid JavaScript-like syntax or references: ${error.message || String(error)}`
483
+ `Batch source is too large (${sourceByteLength} bytes). Keep Pencil operation batches below ${MAX_OPERATION_SOURCE_BYTES} bytes.`
366
484
  )
367
485
  );
368
486
  }
369
487
 
488
+ const unsafeSourceMessage = detectUnsafeOperationSource(normalizedOps);
489
+ if (unsafeSourceMessage) {
490
+ issues.push(createIssue("FAIL", unsafeSourceMessage));
491
+ }
492
+
493
+ if (issues.length === 0) {
494
+ try {
495
+ calls = simulateOperations(normalizedOps);
496
+ } catch (error) {
497
+ const errorMessage = error && error.message ? error.message : String(error);
498
+ const timeoutHit =
499
+ (error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") ||
500
+ /Script execution timed out/i.test(errorMessage);
501
+ if (timeoutHit) {
502
+ issues.push(
503
+ createIssue(
504
+ "FAIL",
505
+ "Pencil batch execution timed out in preflight sandbox. Keep operations declarative and avoid loops or computed code."
506
+ )
507
+ );
508
+ } else {
509
+ issues.push(
510
+ createIssue(
511
+ "FAIL",
512
+ `Pencil batch has invalid JavaScript-like syntax or references: ${errorMessage}`
513
+ )
514
+ );
515
+ }
516
+ }
517
+ }
518
+
370
519
  if (calls.length > 0) {
371
520
  validateCalls(calls, issues);
372
521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenonbyte/da-vinci-workflow",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
5
5
  "bin": {
6
6
  "da-vinci": "bin/da-vinci.js"
@@ -22,6 +22,7 @@
22
22
  "scripts": {
23
23
  "postinstall": "node scripts/postinstall.js",
24
24
  "validate-assets": "node scripts/validate-assets.js",
25
+ "test:audit-safety": "node scripts/test-audit-safety.js",
25
26
  "test:audit-context-delta": "node scripts/test-audit-context-delta.js",
26
27
  "test:audit-design-supervisor": "node scripts/test-audit-design-supervisor.js",
27
28
  "test:pencil-lock": "node scripts/test-pencil-lock.js",
@@ -30,7 +31,10 @@
30
31
  "test:persistence-flows": "node scripts/test-persistence-flows.js",
31
32
  "test:pencil-session": "node scripts/test-pencil-session.js",
32
33
  "test:pencil-preflight": "node scripts/test-pencil-preflight.js",
33
- "test:pen-persistence": "node scripts/test-pen-persistence.js"
34
+ "test:pen-persistence": "node scripts/test-pen-persistence.js",
35
+ "test:icon-search": "node scripts/test-icon-search.js",
36
+ "test:icon-sync": "node scripts/test-icon-sync.js",
37
+ "test:icon-aliases": "node scripts/test-icon-aliases.js"
34
38
  },
35
39
  "engines": {
36
40
  "node": ">=18"