#!/usr/bin/env node /** * sync-doc-paths — Propagate DocCenter path changes to all workflow JSONs. * * Usage: * node scripts/sync-doc-paths.js # dry-run (show renames only) * node scripts/sync-doc-paths.js --apply # actually write changes * node scripts/sync-doc-paths.js --rename 6:16 # move path 6 → 16 everywhere * node scripts/sync-doc-paths.js --rename 6:16,2:12 # multiple renames * node scripts/sync-doc-paths.js --sync-desc # also sync descriptions from registry * * What it does: * 1. Reads src/data/doc-paths.js as the source of truth * 2. Scans all workflow JSONs (both .vl-code/workflows/ and seed-workflows/) * 3. For each workflow: * a. Updates registry.docs descriptions to match doc-paths.js * b. If --rename given, remaps old path → new path in registry.docs keys + step docs arrays * 4. Updates hardcoded docId references in JS source files */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); // Dynamic import of doc-paths (ES module) const { PATH_MAP, DOC_REGISTRY } = await import('../src/data/doc-paths.js'); const args = process.argv.slice(2); const apply = args.includes('--apply'); const syncDesc = args.includes('--sync-desc'); const renameIdx = args.indexOf('--rename'); const renames = new Map(); // oldPath → newPath if (renameIdx !== -1 && args[renameIdx + 1]) { for (const pair of args[renameIdx + 1].split(',')) { const [from, to] = pair.split(':').map(Number); if (!isNaN(from) && !isNaN(to)) { renames.set(from, to); } } } // ─── Workflow JSON directories ────────────────────────────────── const WORKFLOW_DIRS = [ path.join(ROOT, '.vl-code', 'workflows'), path.join(ROOT, 'public', 'seed-workflows'), ]; let totalChanges = 0; console.log(`\n📄 DocCenter Path Sync ${apply ? '(APPLY MODE)' : '(DRY RUN)'}`); console.log(` Registry: ${Object.keys(DOC_REGISTRY).length} entries`); if (renames.size > 0) { console.log(` Renames: ${[...renames.entries()].map(([f, t]) => `${f}→${t}`).join(', ')}`); } console.log(''); // ─── Phase 1: Update workflow JSONs ───────────────────────────── for (const dir of WORKFLOW_DIRS) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); for (const file of files) { const fp = path.join(dir, file); const relPath = path.relative(ROOT, fp); let content = fs.readFileSync(fp, 'utf-8'); let data; try { data = JSON.parse(content); } catch { continue; } const changes = []; const registryDocs = data?.registry?.docs; if (registryDocs && typeof registryDocs === 'object') { // 1a. Apply renames to registry.docs keys if (renames.size > 0) { const newDocs = {}; for (const [pathStr, desc] of Object.entries(registryDocs)) { const pathNum = Number(pathStr); const newPath = renames.get(pathNum); if (newPath !== undefined) { newDocs[String(newPath)] = desc; changes.push(` registry.docs: key ${pathStr} → ${newPath}`); } else { newDocs[pathStr] = desc; } } data.registry.docs = newDocs; } // 1b. Sync descriptions from doc-paths.js (only with --sync-desc flag) // NOTE: Some path numbers are reused across workflow families with different meanings. // Only use --sync-desc when you've verified the registry descriptions are correct. if (syncDesc) { for (const [pathStr, desc] of Object.entries(data.registry.docs)) { const pathNum = Number(pathStr); const canonical = PATH_MAP.get(pathNum); if (canonical && canonical.desc !== desc) { changes.push(` registry.docs["${pathStr}"]: "${desc}" → "${canonical.desc}"`); data.registry.docs[pathStr] = canonical.desc; } } } } // 1c. Apply renames to step docs arrays if (renames.size > 0 && data.steps) { const renameInSteps = (steps) => { for (const step of steps) { if (step.in?.docs && Array.isArray(step.in.docs)) { for (let i = 0; i < step.in.docs.length; i++) { const pathNum = Number(step.in.docs[i]); const newPath = renames.get(pathNum); if (newPath !== undefined) { changes.push(` ${step.id}.in.docs: "${step.in.docs[i]}" → "${newPath}"`); step.in.docs[i] = String(newPath); } } } // Recurse into children (for Fork nodes) if (step.children && Array.isArray(step.children)) { // children are IDs, not nested steps — check steps array } } }; renameInSteps(data.steps); } if (changes.length > 0) { totalChanges += changes.length; console.log(`📝 ${relPath} (${changes.length} changes)`); for (const c of changes) console.log(c); if (apply) { fs.writeFileSync(fp, JSON.stringify(data, null, 2) + '\n', 'utf-8'); console.log(` ✅ Written`); } console.log(''); } } } // ─── Phase 2: Check JS source files for hardcoded docId/path ─── const JS_FILES_TO_CHECK = [ 'src/vl/workflow-executor.js', 'src/web/server.js', 'src/tools/vl-parse.js', 'src/cloud/cloud-api.js', ]; console.log('─── JS source file checks ───'); for (const relFile of JS_FILES_TO_CHECK) { const fp = path.join(ROOT, relFile); if (!fs.existsSync(fp)) continue; const content = fs.readFileSync(fp, 'utf-8'); // Check for hardcoded docId numbers that should use doc-paths.js const docIdMatches = [...content.matchAll(/docId[:\s]*(\d+)/g)]; for (const m of docIdMatches) { const docId = Number(m[1]); // Find if this docId is in our registry for (const [alias, entry] of Object.entries(DOC_REGISTRY)) { if (entry.docId === docId) { console.log(` ⚠ ${relFile}: hardcoded docId ${docId} → should use docIdFor('${alias}') from doc-paths.js`); totalChanges++; } } } } // ─── Summary ──────────────────────────────────────────────────── console.log(`\n${apply ? '✅' : '📋'} Total: ${totalChanges} change(s) ${apply ? 'applied' : 'found (use --apply to write)'}\n`);