Files
memind/scripts/repair-mindspace-public-downloads.mjs
T
john 5f8ef9ddb0 fix: make MindSpace docx repair script runtime-safe
Avoid importing mindspace-public-finish-sync in production runtime so release
link repair can copy oa docx files or strip broken anchors without extra deps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 11:29:17 +08:00

107 lines
3.7 KiB
JavaScript
Executable File

#!/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 = /<a\b[^>]*\bhref=(["'])([^"']+\.docx)\1[^>]*>[\s\S]*?<\/a>/gi;
const DOCX_HREF_CAPTURE_RE = /<a\b[^>]*\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();
}