const fs = require("fs"); const path = require("path"); const util = require("util"); const yaml = require("js-yaml"); const readline = require("readline"); const { execFile } = require("child_process"); const execFileAsync = util.promisify(execFile); const SITE_ROOT = path.resolve(__dirname, ".."); const CONFIG_PATH = path.join(__dirname, "config.json"); let config = {}; if (fs.existsSync(CONFIG_PATH)) { try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch (error) { console.warn( `Impossible de parser ${path.relative( SITE_ROOT, CONFIG_PATH )}. Valeurs par défaut utilisées. (${error.message})` ); } } const externalConfig = { cacheDir: path.join(__dirname, "cache"), cacheFile: "external_links.yaml", ...(config.externalLinks || {}), }; const CACHE_DIR = path.isAbsolute(externalConfig.cacheDir) ? externalConfig.cacheDir : path.resolve(SITE_ROOT, externalConfig.cacheDir); const CACHE_PATH = path.isAbsolute(externalConfig.cacheFile) ? externalConfig.cacheFile : path.join(CACHE_DIR, externalConfig.cacheFile); function ensureDirectoryExists(targetFile) { fs.mkdirSync(path.dirname(targetFile), { recursive: true }); } function loadCache() { if (!fs.existsSync(CACHE_PATH)) return {}; try { return yaml.load(fs.readFileSync(CACHE_PATH, "utf8")) || {}; } catch (e) { console.error("Erreur de lecture du cache YAML:", e.message); return {}; } } function saveCache(cache) { ensureDirectoryExists(CACHE_PATH); fs.writeFileSync(CACHE_PATH, yaml.dump(cache), "utf8"); } function promptFactory() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const question = (q) => new Promise((resolve) => rl.question(q, (ans) => resolve(ans.trim()))); return { async ask(q) { return await question(q); }, close() { rl.close(); }, }; } async function ensureCheckRanIfNeeded() { if (fs.existsSync(CACHE_PATH)) return; console.log( "Cache introuvable. Exécution préalable de tools/check_external_links.js..." ); await execFileAsync("node", [path.join(__dirname, "check_external_links.js")], { cwd: SITE_ROOT, env: process.env, }); } function listBrokenUrls(cache) { const result = []; for (const [url, info] of Object.entries(cache)) { const status = info && typeof info.status === "number" ? info.status : null; const killed = info && info.manually_killed === true; const validated = info && info.manually_validated === true; if (killed) continue; // on ne traite plus ces URL if (validated) continue; // déjà validé manuellement if (status !== null && (status >= 400 || status === 0)) { result.push({ url, info }); } } return result; } function getFilesForUrl(info) { let files = []; if (Array.isArray(info?.files) && info.files.length > 0) { files = info.files; } else if (Array.isArray(info?.locations) && info.locations.length > 0) { files = Array.from(new Set(info.locations.map((s) => String(s).split(":")[0]))); } return files.map((p) => path.resolve(SITE_ROOT, p)); } function replaceInFile(filePath, from, to) { if (!fs.existsSync(filePath)) return { changed: false }; const original = fs.readFileSync(filePath, "utf8"); if (!original.includes(from)) return { changed: false }; const updated = original.split(from).join(to); if (updated !== original) { fs.writeFileSync(filePath, updated, "utf8"); return { changed: true }; } return { changed: false }; } async function main() { await ensureCheckRanIfNeeded(); let cache = loadCache(); const broken = listBrokenUrls(cache); if (broken.length === 0) { console.log("Aucun lien en erreur (>= 400) à traiter."); return; } const p = promptFactory(); try { for (const { url, info } of broken) { const statusLabel = typeof info.status === "number" ? String(info.status) : "inconnu"; const locations = Array.isArray(info.locations) ? info.locations : []; const files = Array.isArray(info.files) ? info.files : Array.from(new Set(locations.map((s) => String(s).split(":")[0]))); console.log("\nURL: ", url); console.log("Statut: ", statusLabel); if (locations.length > 0) { console.log("Emplacements:"); for (const loc of locations) console.log(" - ", loc); } else if (files.length > 0) { console.log("Emplacements:"); for (const f of files) console.log(" - ", `${f}:?`); } else { console.log("Fichiers: (aucun chemin enregistré)"); } const choice = ( await p.ask( "Action ? [i]gnorer, [c]onfirmer, [r]emplacer, [m]ort, [q]uitter (défaut: i) : " ) ).toLowerCase() || "i"; if (choice === "q") { console.log("Arrêt demandé."); break; } if (choice === "i") { // Ignorer continue; } if (choice === "c") { const nowIso = new Date().toISOString(); cache[url] = { ...(cache[url] || {}), manually_validated: true, manually_killed: cache[url]?.manually_killed === true, status: 200, errorType: null, method: "MANUAL", checked: nowIso, }; saveCache(cache); console.log("Marqué comme validé manuellement."); continue; } if (choice === "m") { cache[url] = { ...(cache[url] || {}), manually_killed: true, manually_validated: cache[url]?.manually_validated === true, status: cache[url]?.status ?? null, errorType: cache[url]?.errorType ?? null, method: cache[url]?.method ?? null, }; saveCache(cache); console.log("Marqué comme mort (plus jamais retesté)."); continue; } if (choice === "r") { if (!(Array.isArray(files) && files.length > 0)) { console.log( "Impossible de remplacer: aucun fichier enregistré pour cet URL. Relancez d'abord tools/check_external_links.js." ); continue; } const newUrl = await p.ask("Nouvel URL: "); if (!newUrl || !newUrl.includes("://")) { console.log("URL invalide, opération annulée."); continue; } // Remplacements dans les fichiers listés let changedFiles = 0; for (const rel of files) { const abs = path.resolve(SITE_ROOT, rel); const { changed } = replaceInFile(abs, url, newUrl); if (changed) changedFiles++; } console.log(`Remplacements effectués dans ${changedFiles} fichier(s).`); // Mettre à jour la base: déplacer l'entrée vers la nouvelle clé const oldEntry = cache[url] || {}; const newEntryExisting = cache[newUrl] || {}; cache[newUrl] = { ...newEntryExisting, files: Array.isArray(oldEntry.files) ? [...oldEntry.files] : files, locations: Array.isArray(oldEntry.locations) ? [...oldEntry.locations] : Array.isArray(oldEntry.files) ? oldEntry.files.map((f) => `${f}:?`) : Array.isArray(locations) ? [...locations] : [], manually_validated: false, manually_killed: false, status: null, errorType: null, method: newEntryExisting.method || null, checked: null, }; delete cache[url]; saveCache(cache); console.log("Base mise à jour pour le nouvel URL."); continue; } console.log("Choix non reconnu. Ignoré."); } } finally { p.close(); } console.log("\nTerminé. Vous pouvez relancer 'node tools/check_external_links.js' pour mettre à jour les statuts."); } main().catch((err) => { console.error("Erreur:", err); process.exitCode = 1; });