sync-doc-paths.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env node
  2. /**
  3. * sync-doc-paths — Propagate DocCenter path changes to all workflow JSONs.
  4. *
  5. * Usage:
  6. * node scripts/sync-doc-paths.js # dry-run (show renames only)
  7. * node scripts/sync-doc-paths.js --apply # actually write changes
  8. * node scripts/sync-doc-paths.js --rename 6:16 # move path 6 → 16 everywhere
  9. * node scripts/sync-doc-paths.js --rename 6:16,2:12 # multiple renames
  10. * node scripts/sync-doc-paths.js --sync-desc # also sync descriptions from registry
  11. *
  12. * What it does:
  13. * 1. Reads src/data/doc-paths.js as the source of truth
  14. * 2. Scans all workflow JSONs (both .vl-code/workflows/ and seed-workflows/)
  15. * 3. For each workflow:
  16. * a. Updates registry.docs descriptions to match doc-paths.js
  17. * b. If --rename given, remaps old path → new path in registry.docs keys + step docs arrays
  18. * 4. Updates hardcoded docId references in JS source files
  19. */
  20. import fs from 'fs';
  21. import path from 'path';
  22. import { fileURLToPath } from 'url';
  23. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  24. const ROOT = path.resolve(__dirname, '..');
  25. // Dynamic import of doc-paths (ES module)
  26. const { PATH_MAP, DOC_REGISTRY } = await import('../src/data/doc-paths.js');
  27. const args = process.argv.slice(2);
  28. const apply = args.includes('--apply');
  29. const syncDesc = args.includes('--sync-desc');
  30. const renameIdx = args.indexOf('--rename');
  31. const renames = new Map(); // oldPath → newPath
  32. if (renameIdx !== -1 && args[renameIdx + 1]) {
  33. for (const pair of args[renameIdx + 1].split(',')) {
  34. const [from, to] = pair.split(':').map(Number);
  35. if (!isNaN(from) && !isNaN(to)) {
  36. renames.set(from, to);
  37. }
  38. }
  39. }
  40. // ─── Workflow JSON directories ──────────────────────────────────
  41. const WORKFLOW_DIRS = [
  42. path.join(ROOT, '.vl-code', 'workflows'),
  43. path.join(ROOT, 'public', 'seed-workflows'),
  44. ];
  45. let totalChanges = 0;
  46. console.log(`\n📄 DocCenter Path Sync ${apply ? '(APPLY MODE)' : '(DRY RUN)'}`);
  47. console.log(` Registry: ${Object.keys(DOC_REGISTRY).length} entries`);
  48. if (renames.size > 0) {
  49. console.log(` Renames: ${[...renames.entries()].map(([f, t]) => `${f}→${t}`).join(', ')}`);
  50. }
  51. console.log('');
  52. // ─── Phase 1: Update workflow JSONs ─────────────────────────────
  53. for (const dir of WORKFLOW_DIRS) {
  54. if (!fs.existsSync(dir)) continue;
  55. const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
  56. for (const file of files) {
  57. const fp = path.join(dir, file);
  58. const relPath = path.relative(ROOT, fp);
  59. let content = fs.readFileSync(fp, 'utf-8');
  60. let data;
  61. try { data = JSON.parse(content); } catch { continue; }
  62. const changes = [];
  63. const registryDocs = data?.registry?.docs;
  64. if (registryDocs && typeof registryDocs === 'object') {
  65. // 1a. Apply renames to registry.docs keys
  66. if (renames.size > 0) {
  67. const newDocs = {};
  68. for (const [pathStr, desc] of Object.entries(registryDocs)) {
  69. const pathNum = Number(pathStr);
  70. const newPath = renames.get(pathNum);
  71. if (newPath !== undefined) {
  72. newDocs[String(newPath)] = desc;
  73. changes.push(` registry.docs: key ${pathStr} → ${newPath}`);
  74. } else {
  75. newDocs[pathStr] = desc;
  76. }
  77. }
  78. data.registry.docs = newDocs;
  79. }
  80. // 1b. Sync descriptions from doc-paths.js (only with --sync-desc flag)
  81. // NOTE: Some path numbers are reused across workflow families with different meanings.
  82. // Only use --sync-desc when you've verified the registry descriptions are correct.
  83. if (syncDesc) {
  84. for (const [pathStr, desc] of Object.entries(data.registry.docs)) {
  85. const pathNum = Number(pathStr);
  86. const canonical = PATH_MAP.get(pathNum);
  87. if (canonical && canonical.desc !== desc) {
  88. changes.push(` registry.docs["${pathStr}"]: "${desc}" → "${canonical.desc}"`);
  89. data.registry.docs[pathStr] = canonical.desc;
  90. }
  91. }
  92. }
  93. }
  94. // 1c. Apply renames to step docs arrays
  95. if (renames.size > 0 && data.steps) {
  96. const renameInSteps = (steps) => {
  97. for (const step of steps) {
  98. if (step.in?.docs && Array.isArray(step.in.docs)) {
  99. for (let i = 0; i < step.in.docs.length; i++) {
  100. const pathNum = Number(step.in.docs[i]);
  101. const newPath = renames.get(pathNum);
  102. if (newPath !== undefined) {
  103. changes.push(` ${step.id}.in.docs: "${step.in.docs[i]}" → "${newPath}"`);
  104. step.in.docs[i] = String(newPath);
  105. }
  106. }
  107. }
  108. // Recurse into children (for Fork nodes)
  109. if (step.children && Array.isArray(step.children)) {
  110. // children are IDs, not nested steps — check steps array
  111. }
  112. }
  113. };
  114. renameInSteps(data.steps);
  115. }
  116. if (changes.length > 0) {
  117. totalChanges += changes.length;
  118. console.log(`📝 ${relPath} (${changes.length} changes)`);
  119. for (const c of changes) console.log(c);
  120. if (apply) {
  121. fs.writeFileSync(fp, JSON.stringify(data, null, 2) + '\n', 'utf-8');
  122. console.log(` ✅ Written`);
  123. }
  124. console.log('');
  125. }
  126. }
  127. }
  128. // ─── Phase 2: Check JS source files for hardcoded docId/path ───
  129. const JS_FILES_TO_CHECK = [
  130. 'src/vl/workflow-executor.js',
  131. 'src/web/server.js',
  132. 'src/tools/vl-parse.js',
  133. 'src/cloud/cloud-api.js',
  134. ];
  135. console.log('─── JS source file checks ───');
  136. for (const relFile of JS_FILES_TO_CHECK) {
  137. const fp = path.join(ROOT, relFile);
  138. if (!fs.existsSync(fp)) continue;
  139. const content = fs.readFileSync(fp, 'utf-8');
  140. // Check for hardcoded docId numbers that should use doc-paths.js
  141. const docIdMatches = [...content.matchAll(/docId[:\s]*(\d+)/g)];
  142. for (const m of docIdMatches) {
  143. const docId = Number(m[1]);
  144. // Find if this docId is in our registry
  145. for (const [alias, entry] of Object.entries(DOC_REGISTRY)) {
  146. if (entry.docId === docId) {
  147. console.log(` ⚠ ${relFile}: hardcoded docId ${docId} → should use docIdFor('${alias}') from doc-paths.js`);
  148. totalChanges++;
  149. }
  150. }
  151. }
  152. }
  153. // ─── Summary ────────────────────────────────────────────────────
  154. console.log(`\n${apply ? '✅' : '📋'} Total: ${totalChanges} change(s) ${apply ? 'applied' : 'found (use --apply to write)'}\n`);