inital commit

This commit is contained in:
KingKDot
2026-03-05 00:04:26 -06:00
commit f8515a2883
269 changed files with 47081 additions and 0 deletions
@@ -0,0 +1,237 @@
import fs from "fs/promises";
import path from "path";
import AdmZip from "adm-zip";
import { v4 as uuidv4 } from "uuid";
import type { ClientInfo } from "../types";
import { logger } from "../logger";
import { encodeMessage, type PluginManifest } from "../protocol";
export type PluginState = {
enabled: Record<string, boolean>;
lastError: Record<string, string>;
};
export async function loadPluginStateFromDisk(pluginStatePath: string): Promise<PluginState> {
try {
const raw = await fs.readFile(pluginStatePath, "utf-8");
const parsed = JSON.parse(raw) as { enabled?: Record<string, boolean>; lastError?: Record<string, string> };
return {
enabled: parsed.enabled || {},
lastError: parsed.lastError || {},
};
} catch {
return { enabled: {}, lastError: {} };
}
}
export async function savePluginStateToDisk(
pluginRoot: string,
pluginStatePath: string,
pluginState: PluginState,
): Promise<void> {
await fs.mkdir(pluginRoot, { recursive: true });
await fs.writeFile(pluginStatePath, JSON.stringify(pluginState, null, 2));
}
export async function ensurePluginExtracted(
pluginRoot: string,
pluginId: string,
sanitizePluginId: (name: string) => string,
): Promise<void> {
const safeId = sanitizePluginId(pluginId);
const zipPath = path.join(pluginRoot, `${safeId}.zip`);
const pluginDir = path.join(pluginRoot, safeId);
const manifestPath = path.join(pluginDir, "manifest.json");
let zipStat: any = null;
try {
zipStat = await fs.stat(zipPath);
} catch {
zipStat = null;
}
let manifestStat: any = null;
try {
manifestStat = await fs.stat(manifestPath);
} catch {
manifestStat = null;
}
if (!zipStat) {
if (manifestStat) return;
throw new Error(`Plugin bundle not found: ${safeId}`);
}
if (manifestStat && manifestStat.mtimeMs >= zipStat.mtimeMs) {
return;
}
await fs.mkdir(pluginDir, { recursive: true });
const assetsDir = path.join(pluginDir, "assets");
await fs.mkdir(assetsDir, { recursive: true });
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
let wasmEntry: Buffer | null = null;
let htmlEntry: Buffer | null = null;
let cssEntry: Buffer | null = null;
let jsEntry: Buffer | null = null;
for (const entry of entries) {
if (entry.isDirectory) continue;
const base = path.basename(entry.entryName);
if (base.toLowerCase().endsWith(".wasm")) {
wasmEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".html")) {
htmlEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".css")) {
cssEntry = entry.getData();
} else if (base.toLowerCase().endsWith(".js")) {
jsEntry = entry.getData();
}
}
if (!wasmEntry || !htmlEntry || !cssEntry || !jsEntry) {
throw new Error(`Invalid plugin bundle: ${safeId} (missing required files)`);
}
await fs.writeFile(path.join(pluginDir, `${safeId}.wasm`), wasmEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.html`), htmlEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.css`), cssEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.js`), jsEntry);
const manifest: PluginManifest = {
id: safeId,
name: safeId,
version: "1.0.0",
binary: `${safeId}.wasm`,
entry: `${safeId}.html`,
assets: {
html: `${safeId}.html`,
css: `${safeId}.css`,
js: `${safeId}.js`,
},
};
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
}
export async function syncPluginBundles(
pluginRoot: string,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<void> {
await fs.mkdir(pluginRoot, { recursive: true });
const entries = await fs.readdir(pluginRoot, { withFileTypes: true });
for (const ent of entries) {
if (ent.isFile() && ent.name.toLowerCase().endsWith(".zip")) {
const pluginId = ent.name.slice(0, -4);
try {
await ensureExtracted(pluginId);
} catch (err) {
logger.warn(`[plugin] failed to extract ${pluginId}: ${(err as Error).message}`);
}
}
}
}
export async function listPluginManifests(
pluginRoot: string,
pluginState: PluginState,
saveState: () => Promise<void>,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<PluginManifest[]> {
try {
await syncPluginBundles(pluginRoot, ensureExtracted);
const entries = await fs.readdir(pluginRoot, { withFileTypes: true });
const manifests: PluginManifest[] = [];
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const manifestPath = path.join(pluginRoot, ent.name, "manifest.json");
try {
const raw = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(raw) as PluginManifest;
const id = manifest.id || ent.name;
const name = manifest.name || ent.name;
if (pluginState.enabled[id] === undefined) {
pluginState.enabled[id] = true;
}
manifests.push({ ...manifest, id, name });
} catch {}
}
await saveState();
return manifests;
} catch {
return [];
}
}
export async function loadPluginBundle(
pluginRoot: string,
pluginId: string,
ensureExtracted: (pluginId: string) => Promise<void>,
): Promise<{ manifest: PluginManifest; wasm: Uint8Array }> {
await ensureExtracted(pluginId);
const dir = path.join(pluginRoot, pluginId);
const manifestPath = path.join(dir, "manifest.json");
const rawManifest = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(rawManifest) as PluginManifest;
manifest.id = manifest.id || pluginId;
manifest.name = manifest.name || pluginId;
const binaryName = manifest.binary || manifest.entry || `${pluginId}.wasm`;
let wasmPath = path.join(dir, binaryName);
try {
await fs.access(wasmPath);
} catch {
const files = await fs.readdir(dir);
const firstWasm = files.find((f) => f.toLowerCase().endsWith(".wasm"));
if (!firstWasm) throw new Error("No .wasm found for plugin " + pluginId);
wasmPath = path.join(dir, firstWasm);
}
const wasm = new Uint8Array(await fs.readFile(wasmPath));
return { manifest, wasm };
}
export function sendPluginBundle(
target: ClientInfo,
bundle: { manifest: PluginManifest; wasm: Uint8Array },
): void {
const chunkSize = 16 * 1024;
const wasm = bundle.wasm;
const totalChunks = Math.ceil(wasm.length / chunkSize);
const initPayload = {
manifest: bundle.manifest,
size: wasm.length,
chunks: totalChunks,
};
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_init",
id: uuidv4(),
payload: initPayload,
}),
);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, wasm.length);
const chunk = wasm.slice(start, end);
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_chunk",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id, index: i, data: chunk },
}),
);
}
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_finish",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id },
}),
);
}