@xenonbyte/da-vinci-workflow 0.1.25 → 0.2.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +48 -67
  3. package/README.zh-CN.md +36 -66
  4. package/SKILL.md +3 -0
  5. package/commands/claude/dv/continue.md +5 -0
  6. package/commands/claude/dv/design.md +1 -0
  7. package/commands/codex/prompts/dv-continue.md +6 -1
  8. package/commands/codex/prompts/dv-design.md +1 -0
  9. package/commands/gemini/dv/continue.toml +5 -0
  10. package/commands/gemini/dv/design.toml +1 -0
  11. package/commands/templates/dv-continue.shared.md +33 -0
  12. package/docs/dv-command-reference.md +45 -2
  13. package/docs/execution-chain-migration.md +46 -0
  14. package/docs/execution-chain-plan.md +125 -0
  15. package/docs/pencil-rendering-workflow.md +9 -7
  16. package/docs/prompt-entrypoints.md +6 -0
  17. package/docs/prompt-presets/README.md +4 -0
  18. package/docs/visual-assist-presets/README.md +4 -0
  19. package/docs/workflow-examples.md +23 -11
  20. package/docs/workflow-overview.md +27 -0
  21. package/docs/zh-CN/dv-command-reference.md +45 -2
  22. package/docs/zh-CN/execution-chain-migration.md +46 -0
  23. package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
  24. package/docs/zh-CN/prompt-entrypoints.md +6 -0
  25. package/docs/zh-CN/prompt-presets/README.md +5 -1
  26. package/docs/zh-CN/visual-assist-presets/README.md +5 -1
  27. package/docs/zh-CN/workflow-examples.md +23 -11
  28. package/docs/zh-CN/workflow-overview.md +27 -0
  29. package/examples/greenfield-spec-markupflow/README.md +6 -1
  30. package/lib/artifact-parsers.js +120 -0
  31. package/lib/async-offload-worker.js +26 -0
  32. package/lib/async-offload.js +82 -0
  33. package/lib/audit-parsers.js +152 -32
  34. package/lib/audit.js +145 -23
  35. package/lib/cli.js +1068 -437
  36. package/lib/diff-spec.js +242 -0
  37. package/lib/execution-signals.js +136 -0
  38. package/lib/fs-safety.js +1 -4
  39. package/lib/icon-aliases.js +7 -7
  40. package/lib/icon-search.js +21 -14
  41. package/lib/icon-sync.js +220 -41
  42. package/lib/install.js +128 -60
  43. package/lib/lint-bindings.js +143 -0
  44. package/lib/lint-spec.js +408 -0
  45. package/lib/lint-tasks.js +176 -0
  46. package/lib/mcp-runtime-gate.js +4 -7
  47. package/lib/pen-persistence.js +318 -46
  48. package/lib/pencil-lock.js +237 -25
  49. package/lib/pencil-preflight.js +233 -12
  50. package/lib/pencil-session.js +216 -36
  51. package/lib/planning-parsers.js +567 -0
  52. package/lib/scaffold.js +193 -0
  53. package/lib/scope-check.js +603 -0
  54. package/lib/sidecars.js +369 -0
  55. package/lib/supervisor-review.js +82 -35
  56. package/lib/utils.js +129 -0
  57. package/lib/verify.js +652 -0
  58. package/lib/workflow-bootstrap.js +255 -0
  59. package/lib/workflow-contract.js +107 -0
  60. package/lib/workflow-persisted-state.js +297 -0
  61. package/lib/workflow-state.js +785 -0
  62. package/package.json +21 -3
  63. package/references/artifact-templates.md +26 -0
  64. package/references/checkpoints.md +16 -0
  65. package/references/design-inputs.md +2 -0
  66. package/references/modes.md +10 -0
  67. package/references/pencil-design-to-code.md +2 -0
  68. package/scripts/fixtures/complex-sample.pen +0 -295
  69. package/scripts/fixtures/mock-pencil.js +0 -49
  70. package/scripts/test-audit-context-delta.js +0 -446
  71. package/scripts/test-audit-design-supervisor.js +0 -691
  72. package/scripts/test-audit-safety.js +0 -92
  73. package/scripts/test-icon-aliases.js +0 -96
  74. package/scripts/test-icon-search.js +0 -77
  75. package/scripts/test-icon-sync.js +0 -178
  76. package/scripts/test-mcp-runtime-gate.js +0 -287
  77. package/scripts/test-mode-consistency.js +0 -344
  78. package/scripts/test-pen-persistence.js +0 -403
  79. package/scripts/test-pencil-lock.js +0 -130
  80. package/scripts/test-pencil-preflight.js +0 -169
  81. package/scripts/test-pencil-session.js +0 -192
  82. package/scripts/test-persistence-flows.js +0 -345
  83. package/scripts/test-supervisor-review-cli.js +0 -619
  84. package/scripts/test-supervisor-review-integration.js +0 -115
package/lib/icon-sync.js CHANGED
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const https = require("https");
3
3
  const os = require("os");
4
4
  const path = require("path");
5
+ const { parseJsonText, readJsonFile, writeFileAtomic } = require("./utils");
5
6
  const {
6
7
  MATERIAL_ROUNDED,
7
8
  MATERIAL_OUTLINED,
@@ -13,6 +14,15 @@ const LUCIDE_TREE_URL = "https://api.github.com/repos/lucide-icons/lucide/git/tr
13
14
  const FEATHER_TREE_URL = "https://api.github.com/repos/feathericons/feather/git/trees/main?recursive=1";
14
15
  const PHOSPHOR_TREE_URL = "https://api.github.com/repos/phosphor-icons/core/git/trees/main?recursive=1";
15
16
  const DEFAULT_TIMEOUT_MS = 20000;
17
+ const DEFAULT_MAX_REDIRECTS = 4;
18
+ const DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
19
+ const DEFAULT_SOURCE_CONCURRENCY = 1;
20
+ const DEFAULT_ICON_SOURCE_URLS = Object.freeze({
21
+ material: MATERIAL_METADATA_URL,
22
+ lucide: LUCIDE_TREE_URL,
23
+ feather: FEATHER_TREE_URL,
24
+ phosphor: PHOSPHOR_TREE_URL
25
+ });
16
26
 
17
27
  function toBoolean(value) {
18
28
  if (value === true || value === false) {
@@ -57,7 +67,7 @@ function parseGoogleMaterialMetadata(raw) {
57
67
  if (objectStart < 0) {
58
68
  throw new Error("Material metadata payload is not valid JSON.");
59
69
  }
60
- const parsed = JSON.parse(text.slice(objectStart));
70
+ const parsed = parseJsonText(text.slice(objectStart), "Google Material metadata JSON payload");
61
71
  const icons = Array.isArray(parsed.icons) ? parsed.icons : [];
62
72
  const roundedFamilyKey = "Material Icons Round";
63
73
  const outlinedFamilyKey = "Material Icons Outlined";
@@ -87,7 +97,7 @@ function parseGoogleMaterialMetadata(raw) {
87
97
 
88
98
  function parseGitHubTreeIcons(raw, options) {
89
99
  const { family, prefix, suffix } = options;
90
- const parsed = JSON.parse(String(raw || ""));
100
+ const parsed = parseJsonText(String(raw || ""), `GitHub icon tree JSON payload for ${family}`);
91
101
  const tree = Array.isArray(parsed.tree) ? parsed.tree : [];
92
102
 
93
103
  return tree
@@ -132,14 +142,47 @@ function summarizeSourceResults(sourceResults = {}) {
132
142
  };
133
143
  }
134
144
 
145
+ function resolveRedirectUrl(fromUrl, location) {
146
+ if (!location) {
147
+ return "";
148
+ }
149
+ try {
150
+ const resolved = new URL(String(location), fromUrl);
151
+ return resolved.protocol === "https:" ? resolved.toString() : "";
152
+ } catch (error) {
153
+ return "";
154
+ }
155
+ }
156
+
135
157
  function fetchText(url, options = {}) {
136
158
  const timeoutMs = Number.isFinite(Number(options.timeoutMs))
137
159
  ? Number(options.timeoutMs)
138
160
  : DEFAULT_TIMEOUT_MS;
161
+ const maxResponseBytes = Number.isFinite(Number(options.maxResponseBytes))
162
+ ? Math.max(1, Number(options.maxResponseBytes))
163
+ : DEFAULT_MAX_RESPONSE_BYTES;
164
+ const maxRedirects = Number.isFinite(Number(options.maxRedirects))
165
+ ? Math.max(0, Number(options.maxRedirects))
166
+ : DEFAULT_MAX_REDIRECTS;
167
+ const redirectCount = Number.isFinite(Number(options._redirectCount))
168
+ ? Number(options._redirectCount)
169
+ : 0;
170
+ const visited = options._visited instanceof Set ? options._visited : new Set();
171
+ const requestImpl = typeof options.httpsGet === "function" ? options.httpsGet : https.get;
172
+ const requestUrl = String(url || "");
139
173
 
140
174
  return new Promise((resolve, reject) => {
141
- const request = https.get(
142
- url,
175
+ let settled = false;
176
+ function finish(callback, value) {
177
+ if (settled) {
178
+ return;
179
+ }
180
+ settled = true;
181
+ callback(value);
182
+ }
183
+
184
+ const request = requestImpl(
185
+ requestUrl,
143
186
  {
144
187
  headers: {
145
188
  "User-Agent": "da-vinci-workflow/icon-sync",
@@ -148,26 +191,85 @@ function fetchText(url, options = {}) {
148
191
  },
149
192
  (response) => {
150
193
  let data = "";
194
+ let receivedBytes = 0;
151
195
  response.setEncoding("utf8");
152
196
  response.on("data", (chunk) => {
197
+ if (settled) {
198
+ return;
199
+ }
200
+ receivedBytes += Buffer.byteLength(chunk, "utf8");
201
+ if (receivedBytes > maxResponseBytes) {
202
+ const error = new Error(`Response body exceeded ${maxResponseBytes} bytes for ${requestUrl}.`);
203
+ finish(reject, error);
204
+ request.destroy(error);
205
+ return;
206
+ }
153
207
  data += chunk;
154
208
  });
209
+ response.on("error", (error) => {
210
+ finish(reject, error);
211
+ });
155
212
  response.on("end", () => {
213
+ if (settled) {
214
+ return;
215
+ }
216
+ const statusCode = Number(response.statusCode || 0);
217
+ if (statusCode >= 300 && statusCode < 400) {
218
+ if (redirectCount >= maxRedirects) {
219
+ finish(
220
+ reject,
221
+ new Error(
222
+ `Too many redirects while fetching ${requestUrl} (limit ${maxRedirects}).`
223
+ )
224
+ );
225
+ return;
226
+ }
227
+
228
+ const redirectUrl = resolveRedirectUrl(requestUrl, response.headers && response.headers.location);
229
+ if (!redirectUrl) {
230
+ finish(
231
+ reject,
232
+ new Error(`HTTP ${statusCode} redirect without valid location for ${requestUrl}`)
233
+ );
234
+ return;
235
+ }
236
+
237
+ if (visited.has(redirectUrl) || redirectUrl === requestUrl) {
238
+ finish(reject, new Error(`Redirect loop detected while fetching ${requestUrl}.`));
239
+ return;
240
+ }
241
+
242
+ const nextVisited = new Set(visited);
243
+ nextVisited.add(requestUrl);
244
+ finish(
245
+ resolve,
246
+ fetchText(redirectUrl, {
247
+ ...options,
248
+ _redirectCount: redirectCount + 1,
249
+ _visited: nextVisited
250
+ })
251
+ );
252
+ return;
253
+ }
254
+
156
255
  if (response.statusCode && response.statusCode >= 400) {
157
- reject(new Error(`HTTP ${response.statusCode} for ${url}`));
256
+ finish(reject, new Error(`HTTP ${response.statusCode} for ${requestUrl}`));
158
257
  return;
159
258
  }
160
- resolve(data);
259
+ finish(resolve, data);
161
260
  });
162
261
  }
163
262
  );
164
263
 
165
264
  request.on("error", (error) => {
166
- reject(error);
265
+ finish(reject, error);
167
266
  });
168
267
 
169
268
  request.setTimeout(timeoutMs, () => {
170
- request.destroy(new Error(`Request timeout after ${timeoutMs}ms for ${url}`));
269
+ if (settled) {
270
+ return;
271
+ }
272
+ request.destroy(new Error(`Request timeout after ${timeoutMs}ms for ${requestUrl}`));
171
273
  });
172
274
  });
173
275
  }
@@ -187,43 +289,46 @@ function resolveCatalogPath(options = {}) {
187
289
  return path.resolve(getDefaultCatalogPath(options.homeDir));
188
290
  }
189
291
 
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}`);
292
+ function normalizePositiveInt(value, fallback, minimum = 1, maximum = Number.POSITIVE_INFINITY) {
293
+ const parsed = Number.parseInt(String(value || ""), 10);
294
+ if (!Number.isFinite(parsed)) {
295
+ return fallback;
202
296
  }
297
+ return Math.max(minimum, Math.min(maximum, parsed));
298
+ }
203
299
 
300
+ function resolveSourceUrls(overrides = {}) {
204
301
  return {
205
- catalogPath,
206
- catalog: parsed
302
+ material:
303
+ typeof overrides.material === "string" && overrides.material.trim()
304
+ ? overrides.material.trim()
305
+ : DEFAULT_ICON_SOURCE_URLS.material,
306
+ lucide:
307
+ typeof overrides.lucide === "string" && overrides.lucide.trim()
308
+ ? overrides.lucide.trim()
309
+ : DEFAULT_ICON_SOURCE_URLS.lucide,
310
+ feather:
311
+ typeof overrides.feather === "string" && overrides.feather.trim()
312
+ ? overrides.feather.trim()
313
+ : DEFAULT_ICON_SOURCE_URLS.feather,
314
+ phosphor:
315
+ typeof overrides.phosphor === "string" && overrides.phosphor.trim()
316
+ ? overrides.phosphor.trim()
317
+ : DEFAULT_ICON_SOURCE_URLS.phosphor
207
318
  };
208
319
  }
209
320
 
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 = [
321
+ function buildSourceSpecs(options = {}) {
322
+ const sourceUrls = resolveSourceUrls(options.sourceUrls);
323
+ return [
219
324
  {
220
325
  key: "material",
221
- url: MATERIAL_METADATA_URL,
326
+ url: sourceUrls.material,
222
327
  parse: parseGoogleMaterialMetadata
223
328
  },
224
329
  {
225
330
  key: "lucide",
226
- url: LUCIDE_TREE_URL,
331
+ url: sourceUrls.lucide,
227
332
  parse: (raw) =>
228
333
  parseGitHubTreeIcons(raw, {
229
334
  family: "lucide",
@@ -233,7 +338,7 @@ async function syncIconCatalog(options = {}) {
233
338
  },
234
339
  {
235
340
  key: "feather",
236
- url: FEATHER_TREE_URL,
341
+ url: sourceUrls.feather,
237
342
  parse: (raw) =>
238
343
  parseGitHubTreeIcons(raw, {
239
344
  family: "feather",
@@ -243,7 +348,7 @@ async function syncIconCatalog(options = {}) {
243
348
  },
244
349
  {
245
350
  key: "phosphor",
246
- url: PHOSPHOR_TREE_URL,
351
+ url: sourceUrls.phosphor,
247
352
  parse: (raw) =>
248
353
  parseGitHubTreeIcons(raw, {
249
354
  family: "phosphor",
@@ -252,15 +357,83 @@ async function syncIconCatalog(options = {}) {
252
357
  })
253
358
  }
254
359
  ];
360
+ }
361
+
362
+ async function runSourcePool(items, worker, concurrency) {
363
+ const queue = Array.isArray(items) ? items.slice() : [];
364
+ if (queue.length === 0) {
365
+ return;
366
+ }
367
+
368
+ const workerCount = Math.min(
369
+ normalizePositiveInt(concurrency, DEFAULT_SOURCE_CONCURRENCY, 1, 8),
370
+ queue.length
371
+ );
372
+ let cursor = 0;
373
+
374
+ async function runWorker() {
375
+ while (true) {
376
+ const index = cursor;
377
+ cursor += 1;
378
+ if (index >= queue.length) {
379
+ return;
380
+ }
381
+ await worker(queue[index], index);
382
+ }
383
+ }
384
+
385
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
386
+ }
387
+
388
+ function loadIconCatalog(options = {}) {
389
+ const catalogPath = resolveCatalogPath(options);
390
+ if (!fs.existsSync(catalogPath)) {
391
+ return {
392
+ catalogPath,
393
+ catalog: null
394
+ };
395
+ }
396
+
397
+ const parsed = readJsonFile(catalogPath, `icon catalog JSON at ${catalogPath}`);
398
+ if (!parsed || !Array.isArray(parsed.icons)) {
399
+ throw new Error(`Invalid icon catalog format: ${catalogPath}`);
400
+ }
401
+
402
+ return {
403
+ catalogPath,
404
+ catalog: parsed
405
+ };
406
+ }
407
+
408
+ async function syncIconCatalog(options = {}) {
409
+ const catalogPath = resolveCatalogPath(options);
410
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
411
+ ? Number(options.timeoutMs)
412
+ : DEFAULT_TIMEOUT_MS;
413
+ const maxResponseBytes = Number.isFinite(Number(options.maxResponseBytes))
414
+ ? Math.max(1, Number(options.maxResponseBytes))
415
+ : DEFAULT_MAX_RESPONSE_BYTES;
416
+ const strict = toBoolean(options.strict);
417
+ const fetchTextImpl = typeof options.fetchText === "function" ? options.fetchText : fetchText;
418
+ const sourceConcurrency = normalizePositiveInt(
419
+ options.sourceConcurrency,
420
+ DEFAULT_SOURCE_CONCURRENCY,
421
+ 1,
422
+ 4
423
+ );
424
+
425
+ const sourceSpecs = buildSourceSpecs(options);
255
426
 
256
427
  const sourceResults = {};
257
428
  const collected = [];
258
429
 
259
- await Promise.all(
260
- sourceSpecs.map(async (spec) => {
430
+ await runSourcePool(
431
+ sourceSpecs,
432
+ async (spec) => {
261
433
  try {
262
434
  const raw = await fetchTextImpl(spec.url, {
263
- timeoutMs
435
+ timeoutMs,
436
+ maxResponseBytes
264
437
  });
265
438
  const records = spec.parse(raw);
266
439
  sourceResults[spec.key] = {
@@ -277,7 +450,8 @@ async function syncIconCatalog(options = {}) {
277
450
  error: error.message || String(error)
278
451
  };
279
452
  }
280
- })
453
+ },
454
+ sourceConcurrency
281
455
  );
282
456
 
283
457
  const icons = dedupeIconRecords(collected);
@@ -310,7 +484,7 @@ async function syncIconCatalog(options = {}) {
310
484
  };
311
485
 
312
486
  ensureDir(catalogPath);
313
- fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
487
+ writeFileAtomic(catalogPath, JSON.stringify(catalog, null, 2));
314
488
 
315
489
  return {
316
490
  catalogPath,
@@ -349,13 +523,18 @@ module.exports = {
349
523
  LUCIDE_TREE_URL,
350
524
  FEATHER_TREE_URL,
351
525
  PHOSPHOR_TREE_URL,
526
+ DEFAULT_ICON_SOURCE_URLS,
352
527
  getDefaultCatalogPath,
353
528
  resolveCatalogPath,
354
529
  loadIconCatalog,
355
530
  syncIconCatalog,
356
531
  formatIconSyncReport,
532
+ buildSourceSpecs,
357
533
  parseGoogleMaterialMetadata,
358
534
  parseGitHubTreeIcons,
359
535
  dedupeIconRecords,
360
- summarizeSourceResults
536
+ summarizeSourceResults,
537
+ fetchText,
538
+ resolveRedirectUrl,
539
+ DEFAULT_SOURCE_CONCURRENCY
361
540
  };
package/lib/install.js CHANGED
@@ -19,62 +19,71 @@ const REQUIRED_FILES = [
19
19
  ...listFiles(path.join(REPO_ROOT, "examples")).map((filePath) => path.relative(REPO_ROOT, filePath))
20
20
  ];
21
21
 
22
- const CODEX_PROMPT_TARGETS = listFiles(path.join(REPO_ROOT, "commands", "codex", "prompts")).map(
23
- (filePath) => path.join(".codex", "prompts", path.basename(filePath))
22
+ const CODEX_PROMPT_TARGET_PAIRS = listFiles(path.join(REPO_ROOT, "commands", "codex", "prompts")).map(
23
+ (filePath) => ({
24
+ sourcePath: filePath,
25
+ targetPath: path.join(".codex", "prompts", path.basename(filePath))
26
+ })
24
27
  );
25
28
 
26
- const CODEX_SKILL_TARGETS = [
27
- path.join(".codex", "skills", "da-vinci", "SKILL.md"),
28
- ...listFiles(path.join(REPO_ROOT, "agents")).map((filePath) =>
29
- path.join(".codex", "skills", "da-vinci", "agents", path.relative(path.join(REPO_ROOT, "agents"), filePath))
29
+ const CODEX_SKILL_TARGET_PAIRS = [
30
+ {
31
+ sourcePath: path.join(REPO_ROOT, "SKILL.md"),
32
+ targetPath: path.join(".codex", "skills", "da-vinci", "SKILL.md")
33
+ },
34
+ ...buildTargetPairs(
35
+ path.join(REPO_ROOT, "agents"),
36
+ path.join(".codex", "skills", "da-vinci", "agents")
30
37
  ),
31
- ...listFiles(path.join(REPO_ROOT, "references")).map((filePath) =>
32
- path.join(
33
- ".codex",
34
- "skills",
35
- "da-vinci",
36
- "references",
37
- path.relative(path.join(REPO_ROOT, "references"), filePath)
38
- )
38
+ ...buildTargetPairs(
39
+ path.join(REPO_ROOT, "references"),
40
+ path.join(".codex", "skills", "da-vinci", "references")
39
41
  ),
40
- ...listFiles(path.join(REPO_ROOT, "docs")).map((filePath) =>
41
- path.join(".codex", "skills", "da-vinci", "docs", path.relative(path.join(REPO_ROOT, "docs"), filePath))
42
+ ...buildTargetPairs(
43
+ path.join(REPO_ROOT, "docs"),
44
+ path.join(".codex", "skills", "da-vinci", "docs")
42
45
  ),
43
- ...listFiles(path.join(REPO_ROOT, "examples")).map((filePath) =>
44
- path.join(
45
- ".codex",
46
- "skills",
47
- "da-vinci",
48
- "examples",
49
- path.relative(path.join(REPO_ROOT, "examples"), filePath)
50
- )
46
+ ...buildTargetPairs(
47
+ path.join(REPO_ROOT, "examples"),
48
+ path.join(".codex", "skills", "da-vinci", "examples")
51
49
  )
52
50
  ];
53
51
 
54
- const CLAUDE_TARGETS = [
55
- path.join(".claude", "commands", "da-vinci.md"),
56
- path.join(".claude", "commands", "dv", "breakdown.md"),
57
- path.join(".claude", "commands", "dv", "build.md"),
58
- path.join(".claude", "commands", "dv", "continue.md"),
59
- path.join(".claude", "commands", "dv", "design.md"),
60
- path.join(".claude", "commands", "dv", "intake.md"),
61
- path.join(".claude", "commands", "dv", "prompt.md"),
62
- path.join(".claude", "commands", "dv", "tasks.md"),
63
- path.join(".claude", "commands", "dv", "verify.md")
52
+ const CLAUDE_COMMAND_TARGET_PAIRS = [
53
+ {
54
+ sourcePath: path.join(REPO_ROOT, "commands", "claude", "da-vinci.md"),
55
+ targetPath: path.join(".claude", "commands", "da-vinci.md")
56
+ }
64
57
  ];
65
58
 
66
- const GEMINI_TARGETS = [
67
- path.join(".gemini", "commands", "da-vinci.toml"),
68
- path.join(".gemini", "commands", "dv", "breakdown.toml"),
69
- path.join(".gemini", "commands", "dv", "build.toml"),
70
- path.join(".gemini", "commands", "dv", "continue.toml"),
71
- path.join(".gemini", "commands", "dv", "design.toml"),
72
- path.join(".gemini", "commands", "dv", "intake.toml"),
73
- path.join(".gemini", "commands", "dv", "prompt.toml"),
74
- path.join(".gemini", "commands", "dv", "tasks.toml"),
75
- path.join(".gemini", "commands", "dv", "verify.toml")
59
+ const CLAUDE_ACTION_TARGET_PAIRS = buildNamedTargetPairs(
60
+ path.join(REPO_ROOT, "commands", "claude", "dv"),
61
+ path.join(".claude", "commands", "dv"),
62
+ ["breakdown.md", "build.md", "continue.md", "design.md", "intake.md", "prompt.md", "tasks.md", "verify.md"]
63
+ );
64
+
65
+ const GEMINI_COMMAND_TARGET_PAIRS = [
66
+ {
67
+ sourcePath: path.join(REPO_ROOT, "commands", "gemini", "da-vinci.toml"),
68
+ targetPath: path.join(".gemini", "commands", "da-vinci.toml")
69
+ }
76
70
  ];
77
71
 
72
+ const GEMINI_ACTION_TARGET_PAIRS = buildNamedTargetPairs(
73
+ path.join(REPO_ROOT, "commands", "gemini", "dv"),
74
+ path.join(".gemini", "commands", "dv"),
75
+ [
76
+ "breakdown.toml",
77
+ "build.toml",
78
+ "continue.toml",
79
+ "design.toml",
80
+ "intake.toml",
81
+ "prompt.toml",
82
+ "tasks.toml",
83
+ "verify.toml"
84
+ ]
85
+ );
86
+
78
87
  function resolveHome(homeDir) {
79
88
  return homeDir || process.env.HOME || os.homedir();
80
89
  }
@@ -156,8 +165,51 @@ function listFiles(dirPath) {
156
165
  return scan.files;
157
166
  }
158
167
 
159
- function getMissingTargets(homeDir, relativePaths) {
160
- return relativePaths.filter((relativePath) => !fs.existsSync(path.join(homeDir, relativePath)));
168
+ function buildTargetPairs(sourceRoot, targetRoot) {
169
+ return listFiles(sourceRoot).map((filePath) => ({
170
+ sourcePath: filePath,
171
+ targetPath: path.join(targetRoot, path.relative(sourceRoot, filePath))
172
+ }));
173
+ }
174
+
175
+ function buildNamedTargetPairs(sourceDir, targetDir, fileNames) {
176
+ return fileNames.map((fileName) => ({
177
+ sourcePath: path.join(sourceDir, fileName),
178
+ targetPath: path.join(targetDir, fileName)
179
+ }));
180
+ }
181
+
182
+ function inspectInstalledTargets(homeDir, targetPairs) {
183
+ const missing = [];
184
+ const mismatched = [];
185
+ const unreadable = [];
186
+
187
+ for (const pair of targetPairs) {
188
+ const targetPath = path.join(homeDir, pair.targetPath);
189
+ if (!fs.existsSync(targetPath)) {
190
+ missing.push(pair.targetPath);
191
+ continue;
192
+ }
193
+
194
+ try {
195
+ const sourceContent = fs.readFileSync(pair.sourcePath);
196
+ const targetContent = fs.readFileSync(targetPath);
197
+ if (!sourceContent.equals(targetContent)) {
198
+ mismatched.push(pair.targetPath);
199
+ }
200
+ } catch (error) {
201
+ unreadable.push(
202
+ `${pair.targetPath}${error && error.code ? ` (${error.code})` : error && error.message ? ` (${error.message})` : ""}`
203
+ );
204
+ }
205
+ }
206
+
207
+ return {
208
+ ok: missing.length === 0 && mismatched.length === 0 && unreadable.length === 0,
209
+ missing,
210
+ mismatched,
211
+ unreadable
212
+ };
161
213
  }
162
214
 
163
215
  function installCodex(homeDir) {
@@ -312,29 +364,45 @@ function uninstallPlatforms(platforms, options = {}) {
312
364
 
313
365
  function getStatus(options = {}) {
314
366
  const homeDir = resolveHome(options.homeDir);
315
- const codexPromptMissing = getMissingTargets(homeDir, CODEX_PROMPT_TARGETS);
316
- const codexSkillMissing = getMissingTargets(homeDir, CODEX_SKILL_TARGETS);
317
- const claudeMissing = getMissingTargets(homeDir, CLAUDE_TARGETS);
318
- const geminiMissing = getMissingTargets(homeDir, GEMINI_TARGETS);
367
+ const codexPrompt = inspectInstalledTargets(homeDir, CODEX_PROMPT_TARGET_PAIRS);
368
+ const codexSkill = inspectInstalledTargets(homeDir, CODEX_SKILL_TARGET_PAIRS);
369
+ const claudeCommand = inspectInstalledTargets(homeDir, CLAUDE_COMMAND_TARGET_PAIRS);
370
+ const claudeActionSet = inspectInstalledTargets(homeDir, CLAUDE_ACTION_TARGET_PAIRS);
371
+ const geminiCommand = inspectInstalledTargets(homeDir, GEMINI_COMMAND_TARGET_PAIRS);
372
+ const geminiActionSet = inspectInstalledTargets(homeDir, GEMINI_ACTION_TARGET_PAIRS);
319
373
 
320
374
  return {
321
375
  version: VERSION,
322
376
  homeDir,
323
377
  codex: {
324
- prompt: codexPromptMissing.length === 0,
325
- skill: codexSkillMissing.length === 0,
326
- promptMissing: codexPromptMissing,
327
- skillMissing: codexSkillMissing
378
+ prompt: codexPrompt.ok,
379
+ skill: codexSkill.ok,
380
+ promptMissing: codexPrompt.missing,
381
+ promptMismatched: codexPrompt.mismatched,
382
+ promptUnreadable: codexPrompt.unreadable,
383
+ skillMissing: codexSkill.missing,
384
+ skillMismatched: codexSkill.mismatched,
385
+ skillUnreadable: codexSkill.unreadable
328
386
  },
329
387
  claude: {
330
- command: claudeMissing.length === 0,
331
- actionSet: claudeMissing.length === 0,
332
- missing: claudeMissing
388
+ command: claudeCommand.ok,
389
+ actionSet: claudeActionSet.ok,
390
+ commandMissing: claudeCommand.missing,
391
+ commandMismatched: claudeCommand.mismatched,
392
+ commandUnreadable: claudeCommand.unreadable,
393
+ actionSetMissing: claudeActionSet.missing,
394
+ actionSetMismatched: claudeActionSet.mismatched,
395
+ actionSetUnreadable: claudeActionSet.unreadable
333
396
  },
334
397
  gemini: {
335
- command: geminiMissing.length === 0,
336
- actionSet: geminiMissing.length === 0,
337
- missing: geminiMissing
398
+ command: geminiCommand.ok,
399
+ actionSet: geminiActionSet.ok,
400
+ commandMissing: geminiCommand.missing,
401
+ commandMismatched: geminiCommand.mismatched,
402
+ commandUnreadable: geminiCommand.unreadable,
403
+ actionSetMissing: geminiActionSet.missing,
404
+ actionSetMismatched: geminiActionSet.mismatched,
405
+ actionSetUnreadable: geminiActionSet.unreadable
338
406
  }
339
407
  };
340
408
  }