#!/usr/bin/env node // Step 5 — Reference-rot rewriter. Operates on an extracted repo in $WORK/. // 1. Replaces the "[Full disclosure →](../../README.md#ai-generated-code-disclosure)" footnote with // INLINE disclosure text (M13 — self-contained, no dangling cross-repo anchor; the monorepo root // README has no such section, so the link was dead even in-repo). // 2. Rewrites any remaining ../../README.md and ../../.claude-plugin/marketplace.json reference // (e.g. graceful-handoff README footer, linkedin-studio remediation docs) to the absolute catalog URL. // 3. Sets plugin.json `repository` (and package.json homepage/repository/bugs, when present) to the // standalone open/ HTTPS URL read from plugin-map.json, and DROPS any monorepo-relative // `repository.directory` sub-path (e.g. llm-security's "plugins/llm-security") — it points nowhere // once the content lives at the standalone repo root. // ai-psychosis already points at open/ai-psychosis (verify-only no-op, M14). Idempotent: a second run // rewrites zero files. Reports every file it touched. NULL push (D8) — operates only inside $WORK/. // // Usage: node 30-fix-references.mjs import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; const here = path.dirname(fileURLToPath(import.meta.url)); const MAP = path.join(here, 'plugin-map.json'); const WORK = process.env.WORK || '/tmp/polyrepo-migration'; const CATALOG = 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main'; const DISCLOSURE_LINK = /\[Full disclosure →\]\(\.\.\/\.\.\/README\.md#ai-generated-code-disclosure\)/g; const DISCLOSURE_INLINE = 'Every change is human-directed, reviewed, and validated before commit.'; const readJson = async (p) => JSON.parse(await fs.readFile(p, 'utf8')); const exists = async (p) => { try { await fs.access(p); return true; } catch { return false; } }; async function walkMd(dir, out = []) { for (const e of await fs.readdir(dir, { withFileTypes: true })) { if (e.name === '.git') continue; const full = path.join(dir, e.name); if (e.isDirectory()) await walkMd(full, out); else if (e.isFile() && e.name.endsWith('.md')) out.push(full); } return out; } async function main() { const key = process.argv[2]; if (!key) { console.error('usage: 30-fix-references.mjs '); process.exit(2); } const map = await readJson(MAP); const t = map.targets[key]; if (!t) { console.error(`unknown target ${key}`); process.exit(2); } const dest = path.join(WORK, key); if (!(await exists(path.join(dest, '.git')))) { const r = spawnSync('bash', [path.join(here, '10-extract.sh'), key], { stdio: 'inherit', env: { ...process.env, WORK } }); if (r.status !== 0) { console.error('extract failed'); process.exit(1); } } const base = t.repo_url.replace(/\.git$/, ''); // https://git.fromaitochitta.com/open/ const changed = []; // 1) + 2) markdown rewrites for (const file of await walkMd(dest)) { const orig = await fs.readFile(file, 'utf8'); let next = orig.replace(DISCLOSURE_LINK, DISCLOSURE_INLINE); next = next.split('../../README.md').join(`${CATALOG}/README.md`); next = next.split('../../.claude-plugin/marketplace.json').join(`${CATALOG}/.claude-plugin/marketplace.json`); if (next !== orig) { await fs.writeFile(file, next, 'utf8'); changed.push(path.relative(dest, file)); } } // 3a) plugin.json repository → standalone URL (+ drop any stale monorepo-relative repository.directory) const pjPath = path.join(dest, '.claude-plugin', 'plugin.json'); if (await exists(pjPath)) { const pj = await readJson(pjPath); let touched = false; if (pj.repository && typeof pj.repository === 'object') { if (pj.repository.url !== base) { pj.repository.url = base; touched = true; } // A monorepo-relative repository.directory points nowhere in the standalone repo — drop it. if ('directory' in pj.repository) { delete pj.repository.directory; touched = true; } } else if (pj.repository !== base) { pj.repository = base; touched = true; } if (touched) { await fs.writeFile(pjPath, JSON.stringify(pj, null, 2) + '\n', 'utf8'); changed.push('.claude-plugin/plugin.json'); } } // 3b) package.json homepage/repository/bugs (when present) const pkgPath = path.join(dest, 'package.json'); if (await exists(pkgPath)) { const pkg = await readJson(pkgPath); let touched = false; if (pkg.homepage !== base) { pkg.homepage = base; touched = true; } if (pkg.repository && typeof pkg.repository === 'object') { if (pkg.repository.url !== base) { pkg.repository.url = base; touched = true; } // Drop any stale monorepo-relative repository.directory (points nowhere in the standalone repo). if ('directory' in pkg.repository) { delete pkg.repository.directory; touched = true; } } else if (pkg.repository && pkg.repository !== base) { pkg.repository = base; touched = true; } if (pkg.bugs && typeof pkg.bugs === 'object' && pkg.bugs.url !== `${base}/issues`) { pkg.bugs.url = `${base}/issues`; touched = true; } if (touched) { await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); changed.push('package.json'); } } for (const f of changed) console.log(` rewrote ${f}`); console.log(`FIX-REFERENCES OK ${key} → rewrote ${changed.length} file(s)`); } main().catch((e) => { console.error(`Error: ${e.message}`); process.exit(1); });