From 9f2f925a1737aefb1d4c2328599a4495a55abfcb Mon Sep 17 00:00:00 2001 From: Archicratia Date: Mon, 19 Jan 2026 12:25:41 +0100 Subject: [PATCH] tools: track apply-ticket script --- .gitignore | 4 + scripts/apply-ticket.mjs | 384 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100755 scripts/apply-ticket.mjs diff --git a/.gitignore b/.gitignore index 011e72a..4d1e470 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ src/layouts/*.BROKEN* # Astro generated .astro/ + +# --- allow the apply-ticket tool script to be versioned --- +!scripts/ +!scripts/apply-ticket.mjs diff --git a/scripts/apply-ticket.mjs b/scripts/apply-ticket.mjs new file mode 100755 index 0000000..3b0cf61 --- /dev/null +++ b/scripts/apply-ticket.mjs @@ -0,0 +1,384 @@ +#!/usr/bin/env node +import fs from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { spawnSync } from "node:child_process"; + +function usage(exitCode = 0) { + console.log(` +apply-ticket — applique une proposition de correction depuis un ticket Gitea (robuste) + +Usage: + node scripts/apply-ticket.mjs [--dry-run] [--no-build] + +Env (recommandé): + FORGE_API = base API (LAN) ex: http://192.168.1.20:3000 (évite DNS) + FORGE_BASE = base web ex: https://gitea.xxx.tld + FORGE_TOKEN = PAT (avec accès au repo + issues) + GITEA_OWNER = owner (optionnel si auto-détecté depuis git remote) + GITEA_REPO = repo (optionnel si auto-détecté depuis git remote) + +Notes: + - Si dist//index.html est absent, le script lance "npm run build" sauf si --no-build. + - Sauvegarde automatique: .bak.issue- +`); + process.exit(exitCode); +} + +const argv = process.argv.slice(2); +if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) usage(0); + +const issueNum = Number(argv[0]); +if (!Number.isFinite(issueNum) || issueNum <= 0) { + console.error("❌ Numéro de ticket invalide."); + usage(1); +} + +const DRY_RUN = argv.includes("--dry-run"); +const NO_BUILD = argv.includes("--no-build"); + +const CWD = process.cwd(); +const CONTENT_ROOT = path.join(CWD, "src", "content"); +const DIST_ROOT = path.join(CWD, "dist"); + +function normalizeText(s) { + return String(s ?? "") + .normalize("NFKD") + .replace(/\p{Diacritic}/gu, "") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); +} + +// stripping très pragmatique (anti-fragile > parfait) +function stripMd(mdx) { + let s = String(mdx ?? ""); + s = s.replace(/`[^`]*`/g, " "); // inline code + s = s.replace(/!\[[^\]]*\]\([^)]+\)/g, " "); // images + s = s.replace(/\[[^\]]*\]\([^)]+\)/g, " "); // links + s = s.replace(/[*_~]/g, " "); // emphasis-ish + s = s.replace(/<[^>]+>/g, " "); // html tags + s = s.replace(/\s+/g, " ").trim(); + return s; +} + +function run(cmd, args, opts = {}) { + const r = spawnSync(cmd, args, { stdio: "inherit", ...opts }); + if (r.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`); +} + +async function fileExists(p) { + try { await fs.access(p); return true; } catch { return false; } +} + +function getEnv(name, fallback = "") { + return (process.env[name] ?? fallback).trim(); +} + +function inferOwnerRepoFromGit() { + const r = spawnSync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }); + if (r.status !== 0) return null; + const u = (r.stdout || "").trim(); + // supports: https://host/owner/repo.git or ssh + const m = u.match(/[:/](?[^/]+)\/(?[^/]+?)(?:\.git)?$/); + if (!m?.groups) return null; + return { owner: m.groups.owner, repo: m.groups.repo }; +} + +function pickLine(body, key) { + // tolère espaces/indent + const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*([^\\n\\r]+)`, "mi"); + const m = body.match(re); + return m ? m[1].trim() : ""; +} + +function pickHeadingValue(body, headingKey) { + // ex: "## Chemin ..." ligne suivante contenant /... + const re = new RegExp(`^##\\s*${escapeRegExp(headingKey)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n\\s*$)`, "mi"); + const m = body.match(re); + if (!m) return ""; + // première ligne non vide et non commentée + const lines = m[1].split(/\r?\n/).map(l => l.trim()); + for (const l of lines) { + if (!l) continue; + if (l.startsWith("