#!/usr/bin/env node const fs = require("fs"); const os = require("os"); const path = require("path"); const { spawnSync } = require("child_process"); const root = path.resolve(__dirname, ".."); const sourceImagePath = path.join(root, "chatlab-web", "frontend", "public", "company-logo.jpg"); const outputDir = path.join(root, "electron-launcher", "build"); const outputIco = path.join(outputDir, "icon.ico"); const outputPng = path.join(outputDir, "icon.png"); const sizes = [16, 24, 32, 48, 64, 128, 256]; const electronNodeModules = path.join(root, "electron-launcher", "node_modules"); const electronUserData = process.env.CHATLAB_ICON_USER_DATA || path.join(os.tmpdir(), "chatlab-icon-renderer-user-data"); function electronApi() { if (process.versions.electron) { return require("electron"); } return require(path.join(electronNodeModules, "electron")); } function resolveElectronBinary() { return electronApi(); } function writeIco(entries, destination) { const headerSize = 6; const entrySize = 16; let offset = headerSize + entries.length * entrySize; const header = Buffer.alloc(offset); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(entries.length, 4); for (const [index, entry] of entries.entries()) { const pos = headerSize + index * entrySize; header.writeUInt8(entry.size === 256 ? 0 : entry.size, pos); header.writeUInt8(entry.size === 256 ? 0 : entry.size, pos + 1); header.writeUInt8(0, pos + 2); header.writeUInt8(0, pos + 3); header.writeUInt16LE(1, pos + 4); header.writeUInt16LE(32, pos + 6); header.writeUInt32LE(entry.png.length, pos + 8); header.writeUInt32LE(offset, pos + 12); offset += entry.png.length; } fs.writeFileSync(destination, Buffer.concat([header, ...entries.map((entry) => entry.png)])); } async function renderSourcePng() { const { app, BrowserWindow, nativeImage } = electronApi(); await app.whenReady(); const imageBytes = fs.readFileSync(sourceImagePath); const dataUri = `data:image/jpeg;base64,${imageBytes.toString("base64")}`; const html = ` `; const win = new BrowserWindow({ show: false, width: 256, height: 256, transparent: true, backgroundColor: "#00000000", resizable: false, webPreferences: { backgroundThrottling: false, contextIsolation: true, offscreen: true, sandbox: true, }, }); await win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); await win.webContents.executeJavaScript(` new Promise((resolve, reject) => { const image = document.querySelector("img"); if (!image) { reject(new Error("Icon image element was not created")); return; } if (image.complete && image.naturalWidth > 0) { setTimeout(resolve, 80); return; } image.onload = () => setTimeout(resolve, 80); image.onerror = () => reject(new Error("Icon SVG failed to render")); }) `); const captured = await win.webContents.capturePage({ x: 0, y: 0, width: 256, height: 256 }); win.destroy(); const normalized = nativeImage .createFromBuffer(captured.toPNG()) .resize({ width: 256, height: 256, quality: "best" }); return normalized.toPNG(); } async function mainElectron() { const { app } = electronApi(); app.disableHardwareAcceleration(); app.setPath("userData", electronUserData); app.commandLine.appendSwitch("disable-gpu"); app.commandLine.appendSwitch("disable-gpu-compositing"); app.commandLine.appendSwitch("disable-software-rasterizer"); app.commandLine.appendSwitch("disk-cache-dir", path.join(electronUserData, "cache")); if (!fs.existsSync(sourceImagePath)) { throw new Error(`Missing icon source: ${sourceImagePath}`); } fs.mkdirSync(outputDir, { recursive: true }); const { nativeImage } = electronApi(); const sourcePng = await renderSourcePng(); const sourceImage = nativeImage.createFromBuffer(sourcePng); const pngEntries = sizes.map((size) => ({ size, png: sourceImage.resize({ width: size, height: size, quality: "best" }).toPNG(), })); fs.writeFileSync(outputPng, sourcePng); writeIco(pngEntries, outputIco); console.log(`Generated ${path.relative(root, outputIco)} (${sizes.join(", ")} px)`); console.log(`Generated ${path.relative(root, outputPng)} (256 px)`); app.quit(); } function mainNode() { const rendererDir = fs.mkdtempSync(path.join(os.tmpdir(), "chatlab-icon-renderer-")); const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chatlab-icon-user-data-")); fs.mkdirSync(rendererDir, { recursive: true }); fs.writeFileSync( path.join(rendererDir, "package.json"), JSON.stringify({ name: "chatlab-icon-renderer", main: "main.js" }, null, 2), ); fs.writeFileSync( path.join(rendererDir, "main.js"), `process.env.CHATLAB_ICON_RENDER = "1";\nrequire(${JSON.stringify(__filename)});\n`, ); const electronBinary = resolveElectronBinary(); const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE; env.NODE_PATH = [electronNodeModules, process.env.NODE_PATH].filter(Boolean).join(path.delimiter); env.ELECTRON_DISABLE_CRASHPAD = "1"; env.ELECTRON_ENABLE_LOGGING = "0"; env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true"; env.CHATLAB_ICON_USER_DATA = userDataDir; const result = spawnSync( electronBinary, [ rendererDir, "--disable-crash-reporter", "--disable-gpu", "--disable-gpu-compositing", `--user-data-dir=${userDataDir}`, `--disk-cache-dir=${path.join(userDataDir, "cache")}`, ], { cwd: root, stdio: "inherit", windowsHide: true, env, }, ); fs.rmSync(rendererDir, { recursive: true, force: true }); fs.rmSync(userDataDir, { recursive: true, force: true }); if (result.error) { throw result.error; } process.exit(result.status ?? 1); } if (process.versions.electron && process.env.CHATLAB_ICON_RENDER === "1") { mainElectron().catch((error) => { console.error(error); process.exitCode = 1; try { electronApi().app.quit(); } catch { process.exit(1); } }); } else { mainNode(); }