#!/usr/bin/env node // Dev tool ติดตั้ง (sideload) / ถอด Word add-in "e-Court Documents Versioning" // บนเครื่องทดสอบ Windows + macOS — โหลด manifest จาก server (default ตัวทดสอบ // ecourt-note.chamomind.com ที่ app/wordaddin/manifest.xml/route.ts rewrite origin // ให้ตรงโฮสต์ผู้ขออยู่แล้ว) แล้วจดทะเบียนให้ Word Desktop เห็น add-in // // กลไกตรงตาม office-addin-dev-settings@4 (อ่านจาก source ใน node_modules) แต่ // เขียนเองด้วย node builtins ล้วน เพื่อใช้ได้ทั้งรันใน repo (npm run addin:install) // และ copy ไฟล์เดี่ยวนี้ไปรัน `node install-wordaddin.mjs` บนเครื่องที่ไม่มี repo: // - Windows: registry HKCU\Software\Microsoft\Office\16.0\Wef\Developer // value ชื่อ = GUID ของ add-in, REG_SZ, data = path manifest บนดิสก์ (ต้องอยู่ถาวร) // - macOS: วางไฟล์ .manifest.xml ใน // ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/ (Word สแกน *.xml ทุกไฟล์) // // ใช้: node scripts/install-wordaddin.mjs [options] // --url default https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml // (dev: https://localhost:3000/docedit/wordaddin/manifest.xml — ต้อง npm run dev:https ก่อน) // --remove ถอดการติดตั้ง (ไม่ต้องต่อเน็ต — ใช้ manifest ที่เก็บไว้ตอนลง) // --help | -h // // หลังลง/ถอดต้อง restart Word ทุกครั้ง · รายละเอียดเต็ม: docs/WORDADDIN-DEV.md import { spawnSync } from "node:child_process"; import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; const PREFIX = "[install-addin]"; const DEFAULT_MANIFEST_URL = "https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml"; /** ตรงคีย์ที่ office-addin-dev-settings ใช้ (Office 2016+ ทุกรุ่นอยู่ภายใต้ 16.0) */ const WIN_DEV_KEY = "HKCU\\Software\\Microsoft\\Office\\16.0\\Wef\\Developer"; /** โฟลเดอร์เก็บ manifest ถาวรบน Windows — path เดียวกับที่ docs/WORDADDIN-DEV.md สอน manual */ const winManifestDir = () => path.join(os.homedir(), "Documents", "wordaddin-manifest"); const winManifestPath = () => path.join(winManifestDir(), "manifest.xml"); /** โฟลเดอร์ที่ Word for Mac สแกนหา add-in ที่ sideload */ const macWefDir = () => path.join(os.homedir(), "Library", "Containers", "com.microsoft.Word", "Data", "Documents", "wef"); function usage(code = 1) { console.log(`ใช้: node scripts/install-wordaddin.mjs [options] --url default ${DEFAULT_MANIFEST_URL} --remove ถอดการติดตั้ง --help | -h`); process.exit(code); } function parseArgs(argv) { const options = { url: DEFAULT_MANIFEST_URL, remove: false }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; const next = () => { const value = argv[++i]; if (value === undefined) usage(); return value; }; switch (arg) { case "--url": options.url = next(); break; case "--remove": options.remove = true; break; case "--help": case "-h": usage(0); break; default: usage(); } } return options; } function fail(message) { console.error(`${PREFIX} ✗ ${message}`); process.exit(1); } /** โหลด manifest จาก server แล้วดึง GUID ของ add-in จาก — ไม่ hardcode เพราะเปลี่ยน environment ได้ */ async function downloadManifest(url) { let response; try { response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30_000) }); } catch (error) { fail(`โหลด manifest จาก ${url} ไม่ได้ (${error instanceof Error ? error.message : String(error)})`); } if (!response.ok) { fail(`server ตอบ HTTP ${response.status} จาก ${url} — เช็คว่า environment นั้นรันอยู่`); } const xml = await response.text(); if (!xml.includes("([0-9a-fA-F-]{36})<\/Id>/); if (!idMatch) { fail(`หา (GUID) ใน manifest ไม่เจอจาก ${url}`); } return { xml, guid: idMatch[1].toLowerCase() }; } /** อ่าน GUID จากไฟล์ manifest ที่เก็บไว้ตอนติดตั้ง (สำหรับ --remove ที่ไม่ต้องต่อเน็ต) */ function readStoredGuid(manifestPath) { try { const xml = readFileSync(manifestPath, "utf8"); const idMatch = xml.match(/([0-9a-fA-F-]{36})<\/Id>/); return idMatch ? idMatch[1].toLowerCase() : null; } catch { return null; } } /** reg.exe ผ่าน args array เสมอ (ไม่ใช่ shell string — path มี space ก็ไม่พัง) */ function reg(args) { return spawnSync("reg", args, { encoding: "utf8" }); } function installWindows(xml, guid) { const dest = winManifestPath(); mkdirSync(winManifestDir(), { recursive: true }); writeFileSync(dest, xml); console.log(`${PREFIX} ✓ เก็บ manifest ไว้ที่ ${dest}`); const add = reg(["add", WIN_DEV_KEY, "/v", guid, "/t", "REG_SZ", "/d", dest, "/f"]); if (add.status !== 0) { fail(`เขียน registry ไม่สำเร็จ (reg add exit ${add.status}): ${add.stderr?.trim() ?? "ไม่มี stderr"}`); } const query = reg(["query", WIN_DEV_KEY, "/v", guid]); if (query.status !== 0) { fail(`reg add สำเร็จแต่ยืนยันด้วย reg query ไม่ผ่าน (exit ${query.status})`); } console.log(`${PREFIX} ✓ จดทะเบียน add-in ใน registry (${WIN_DEV_KEY} → ${guid})`); return dest; } function removeWindows() { const stored = winManifestPath(); const guid = readStoredGuid(stored); if (!guid) { fail( `ไม่พบ manifest ที่เก็บไว้ (${stored}) จึงไม่รู้ GUID ของ add-in — ` + `ลบเองได้ที่ reg delete "${WIN_DEV_KEY}" /v /f`, ); } const del = reg(["delete", WIN_DEV_KEY, "/v", guid, "/f"]); if (del.status !== 0) { fail(`reg delete ไม่สำเร็จ (exit ${del.status}): ${del.stderr?.trim() ?? "ไม่มี stderr"}`); } try { unlinkSync(stored); } catch { // registry ถอดแล้ว — ไฟล์ค้างไม่ทำให้ Word เห็น add-in } console.log(`${PREFIX} ✓ ถอดการติดตั้งแล้ว (registry ${guid} + ไฟล์ ${stored})`); } /** ไฟล์ sideload ของ GUID นี้ทั้งหมดใน wef — ทั้งของเราและชื่อ basename เก่า ๆ ที่เคยใช้ */ function macSideloadFiles(guid) { const dir = macWefDir(); try { return readdirSync(dir) .filter((name) => name === `${guid}.manifest.xml` || name.startsWith(`${guid}.`)) .map((name) => path.join(dir, name)); } catch { return []; } } function installMac(xml, guid) { const dir = macWefDir(); mkdirSync(dir, { recursive: true }); // ลบของเก่าก่อนเขียนใหม่ — idempotent และไม่มีไฟล์ซ้ำ GUID ค้างในโฟลเดอร์ for (const stale of macSideloadFiles(guid)) { try { unlinkSync(stale); } catch { console.warn(`${PREFIX} ⚠ ลบไฟล์เก่าไม่ได้: ${stale}`); } } const dest = path.join(dir, `${guid}.manifest.xml`); writeFileSync(dest, xml); console.log(`${PREFIX} ✓ วาง manifest ไว้ที่ ${dest}`); return dest; } function removeMac() { const dir = macWefDir(); // บน Mac ไม่มี manifest เก็บแยกที่อื่น (ตัวไฟล์ใน wef คือของที่ลงไว้) — หาทุก *.xml // ในโฟลเดอร์ที่เป็น manifest ของ add-in นี้จากชื่อในไฟล์เอง จึงไม่ต้อง hardcode GUID let files; try { files = readdirSync(dir) .filter((name) => name.toLowerCase().endsWith(".xml")) .map((name) => path.join(dir, name)) .filter((file) => { try { const xml = readFileSync(file, "utf8"); return xml.includes(" My Add-ins > SHARED FOLDER" : "Insert > My Add-ins"; console.log(` ขั้นต่อไป: 1. restart Word ให้ครบทุกหน้าต่าง (จำเป็นทุกครั้งหลังลง/ถอด) 2. ${where} > เปิด "e-Court Documents Versioning" 3. เปิดไฟล์ .docx ที่มี ticket เพื่อทดสอบ เช่นสร้างใหม่ด้วย: node scripts/make-ticket-docx.mjs file.docx --fid DEMO_FID_001 --tid T12 รายละเอียด: docs/WORDADDIN-DEV.md`); } async function main() { const options = parseArgs(process.argv.slice(2)); console.log(`${PREFIX} platform: ${process.platform}${options.remove ? " · โหมดถอดการติดตั้ง" : ""}`); if (process.platform === "win32") { if (options.remove) { removeWindows(); } else { const { xml, guid } = await downloadManifest(options.url); installWindows(xml, guid); } } else if (process.platform === "darwin") { if (options.remove) { removeMac(); } else { const { xml, guid } = await downloadManifest(options.url); installMac(xml, guid); } } else { fail(`รองรับเฉพาะ Windows (win32) และ macOS (darwin) — platform นี้คือ ${process.platform}`); } console.log(`${PREFIX} ✓ เสร็จ`); printNextSteps(); } main().catch((error) => { fail(error instanceof Error ? error.stack ?? error.message : String(error)); });