#!/usr/bin/env node /** * Repair missing public/*.docx download targets before release link checks. * Runtime-safe: no imports from the full MindSpace finish-sync graph. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const DOCX_HREF_RE = /]*\bhref=(["'])([^"']+\.docx)\1[^>]*>[\s\S]*?<\/a>/gi; const DOCX_HREF_CAPTURE_RE = /]*\bhref=(["'])([^"']+\.docx)\1/gi; function listDocxHrefBasenames(html) { const basenames = new Set(); let match = DOCX_HREF_CAPTURE_RE.exec(html); while (match) { const base = path.basename(String(match[2] ?? '')).trim(); if (base) basenames.add(base); match = DOCX_HREF_CAPTURE_RE.exec(html); } DOCX_HREF_CAPTURE_RE.lastIndex = 0; return [...basenames]; } function listMissingDocxTargets(html, publicDir) { return listDocxHrefBasenames(html).filter((basename) => { const docxPath = path.join(publicDir, basename); return !fs.existsSync(docxPath); }); } function stripBrokenDocxAnchors(html, missingBasenames) { const missing = new Set(missingBasenames.map((item) => String(item ?? '').toLowerCase())); if (!missing.size) return { html, changed: false }; let changed = false; const next = html.replace(DOCX_HREF_RE, (match, _quote, hrefValue) => { const base = path.basename(String(hrefValue ?? '')).toLowerCase(); if (!missing.has(base)) return match; changed = true; return ''; }); return { html: next, changed }; } function tryCopyDocxFromOa(userRoot, publicDir, basename) { const destination = path.join(publicDir, basename); if (fs.existsSync(destination)) return false; const source = path.join(userRoot, 'oa', basename); if (!fs.existsSync(source) || !fs.statSync(source).isFile()) return false; fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.copyFileSync(source, destination); return true; } export function repairMindspacePublicDownloads({ publishDir } = {}) { const root = path.resolve(String(publishDir ?? '')); if (!root || !fs.existsSync(root)) { return { synced: [], stripped: [] }; } const synced = []; const stripped = []; for (const userDir of fs.readdirSync(root, { withFileTypes: true })) { if (!userDir.isDirectory()) continue; const userRoot = path.join(root, userDir.name); const publicDir = path.join(userRoot, 'public'); if (!fs.existsSync(publicDir)) continue; for (const entry of fs.readdirSync(publicDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith('.html')) continue; const htmlPath = path.join(publicDir, entry.name); const html = fs.readFileSync(htmlPath, 'utf8'); for (const basename of listDocxHrefBasenames(html)) { if (tryCopyDocxFromOa(userRoot, publicDir, basename)) { synced.push(path.join(userDir.name, 'public', basename)); } } const missingBasenames = listMissingDocxTargets(html, publicDir); const { html: nextHtml, changed } = stripBrokenDocxAnchors(html, missingBasenames); if (!changed) continue; fs.writeFileSync(htmlPath, nextHtml, 'utf8'); stripped.push(path.relative(root, htmlPath)); } } return { synced, stripped }; } function main() { const args = process.argv.slice(2); let publishDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'MindSpace'); for (let i = 0; i < args.length; i += 1) { if (args[i] === '--root' && args[i + 1]) { publishDir = args[i + 1]; i += 1; } } const result = repairMindspacePublicDownloads({ publishDir }); console.log(JSON.stringify(result, null, 2)); } const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); if (isMain) { main(); }