import { spawn as spawnChild } from "node:child_process"; import { stat } from "node:fs/promises"; import path from "node:path"; export const WOW_EXPORT_AUTOMATION_VERSION = "0.2.19"; export const WOW_EXPORT_REQUIRED_FLAGS = Object.freeze([ "--disable-auto-update", "--discover-dungeon", "--export-dungeon", "--export-creatures", ]); export function defaultWowExportExecutable(projectRoot) { return path.resolve( projectRoot, `../wow335a/dungeon-export-work/tools/wow.export-${WOW_EXPORT_AUTOMATION_VERSION}/wow.export.exe`, ); } async function inspectCandidate(file, statFile) { try { const info = await statFile(file); return info.isFile() ? { usable: true, reason: null } : { usable: false, reason: "path exists but is not a file" }; } catch (error) { if (error?.code === "ENOENT") return { usable: false, reason: "file does not exist" }; return { usable: false, reason: `cannot inspect file (${error?.code ?? error?.message ?? "unknown error"})` }; } } function unavailableMessage(checks, configured) { const checked = checks.map(({ label, file, reason }) => ` - ${label}: ${file} (${reason})`).join("\n"); const selection = configured ? "WOW_EXPORT_BIN is set, so the default location was not used. Correct or unset that variable." : "Restore the patched portable bundle at the default location, or set WOW_EXPORT_BIN to its exact executable path."; return [ "HealerMan's patched wow.export automation bridge is unavailable.", `Required build: wow.export ${WOW_EXPORT_AUTOMATION_VERSION} with these automation flags: ${WOW_EXPORT_REQUIRED_FLAGS.join(", ")}.`, "Checked:", checked, selection, "A stock GUI-only wow.export executable is not a drop-in replacement unless it includes the HealerMan automation patch.", "No export was started and no placeholder assets were created.", ].join("\n"); } export class WowExportUnavailableError extends Error { constructor(checks, configured) { super(unavailableMessage(checks, configured)); this.name = "WowExportUnavailableError"; this.code = "WOW_EXPORT_AUTOMATION_UNAVAILABLE"; this.checks = checks; this.requiredVersion = WOW_EXPORT_AUTOMATION_VERSION; this.requiredFlags = [...WOW_EXPORT_REQUIRED_FLAGS]; } } export async function resolveWowExportExecutable({ projectRoot, environment = process.env, statFile = stat, } = {}) { if (!projectRoot) throw new TypeError("resolveWowExportExecutable requires projectRoot."); const configuredValue = environment.WOW_EXPORT_BIN?.trim(); const candidates = configuredValue ? [{ label: "WOW_EXPORT_BIN", file: path.resolve(projectRoot, configuredValue) }] : [{ label: "default portable path", file: defaultWowExportExecutable(projectRoot) }]; const checks = []; for (const candidate of candidates) { const inspection = await inspectCandidate(candidate.file, statFile); if (inspection.usable) return candidate.file; checks.push({ ...candidate, reason: inspection.reason }); } throw new WowExportUnavailableError(checks, Boolean(configuredValue)); } function exporterFailureMessage(executable, argumentsList, detail) { const operation = argumentsList.find((argument) => argument.startsWith("--")) ?? "unknown operation"; return [ `wow.export automation failed during ${operation}: ${detail}`, `Executable: ${executable}`, `Required automation flags: ${WOW_EXPORT_REQUIRED_FLAGS.join(", ")}.`, "Verify that WOW_EXPORT_BIN points to the HealerMan-patched build, not the stock GUI-only release.", "No placeholder assets were created.", ].join("\n"); } export async function runWowExporter(argumentsList, { projectRoot, environment = process.env, statFile = stat, spawn = spawnChild, } = {}) { const executable = await resolveWowExportExecutable({ projectRoot, environment, statFile }); await new Promise((resolve, reject) => { const child = spawn(executable, ["--disable-auto-update", ...argumentsList], { cwd: path.dirname(executable), stdio: "inherit", windowsHide: true, }); let settled = false; const fail = (error) => { if (settled) return; settled = true; reject(error); }; child.once("error", (error) => fail(new Error(exporterFailureMessage( executable, argumentsList, `process could not start (${error?.code ?? error?.message ?? "unknown error"})`, )))); child.once("exit", (code, signal) => { if (settled) return; settled = true; if (code === 0) resolve(); else reject(new Error(exporterFailureMessage( executable, argumentsList, signal ? `process exited after signal ${signal}` : `process exited with code ${code}`, ))); }); }); return executable; }