diff --git a/apps/desktop/after-pack.cjs b/apps/desktop/after-pack.cjs
new file mode 100644
index 0000000..69e0143
--- /dev/null
+++ b/apps/desktop/after-pack.cjs
@@ -0,0 +1,32 @@
+/**
+ * electron-builder applies the repo .gitignore to extraResources, which would
+ * drop `node_modules/`. We stage deps as `_node_modules` and rename here after
+ * the app directory is assembled (before NSIS/DMG/AppImage wrapping).
+ */
+const fs = require("node:fs");
+const path = require("node:path");
+
+exports.default = async function afterPack(context) {
+ const runtime = path.join(context.appOutDir, "resources", "runtime");
+ const staged = path.join(runtime, "_node_modules");
+ const target = path.join(runtime, "node_modules");
+
+ if (fs.existsSync(staged)) {
+ if (fs.existsSync(target)) {
+ fs.rmSync(target, { recursive: true, force: true });
+ }
+ fs.renameSync(staged, target);
+ }
+
+ const cors = path.join(target, "cors");
+ const bridge = path.join(runtime, "apps", "bridge", "dist", "index.js");
+ if (!fs.existsSync(bridge)) {
+ throw new Error(`Desktop afterPack: missing Bridge at ${bridge}`);
+ }
+ if (!fs.existsSync(cors)) {
+ throw new Error(
+ `Desktop afterPack: runtime node_modules incomplete (missing cors at ${cors}). ` +
+ "electron-builder likely stripped dependencies."
+ );
+ }
+};
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 9ca0d48..aa5f846 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -36,6 +36,7 @@
},
"files": [
"dist/**/*",
+ "preload.cjs",
"package.json"
],
"extraResources": [
@@ -63,8 +64,10 @@
"oneClick": true,
"perMachine": false,
"allowToChangeInstallationDirectory": false,
- "deleteAppDataOnUninstall": false
+ "deleteAppDataOnUninstall": false,
+ "shortcutName": "GodMode"
},
+ "afterPack": "./after-pack.cjs",
"mac": {
"target": [
"dmg"
diff --git a/apps/desktop/src/preload.ts b/apps/desktop/preload.cjs
similarity index 77%
rename from apps/desktop/src/preload.ts
rename to apps/desktop/preload.cjs
index ac5efda..93fdbb9 100644
--- a/apps/desktop/src/preload.ts
+++ b/apps/desktop/preload.cjs
@@ -1,4 +1,4 @@
-import { contextBridge } from "electron";
+const { contextBridge } = require("electron");
/** Intentional empty preload — UI talks to Bridge over localhost HTTP only. */
contextBridge.exposeInMainWorld("godmodeDesktop", {
diff --git a/apps/desktop/resources/runtime/.gitkeep b/apps/desktop/resources/runtime/.gitkeep
index ebe8c6a..8b13789 100644
--- a/apps/desktop/resources/runtime/.gitkeep
+++ b/apps/desktop/resources/runtime/.gitkeep
@@ -1,3 +1 @@
-# Staged by scripts/release/package-desktop.mjs — do not commit runtime contents.
-*
-!.gitkeep
+
diff --git a/apps/desktop/resources/update/.gitkeep b/apps/desktop/resources/update/.gitkeep
index 5375402..8b13789 100644
--- a/apps/desktop/resources/update/.gitkeep
+++ b/apps/desktop/resources/update/.gitkeep
@@ -1,3 +1 @@
-# Staged by scripts/release/package-desktop.mjs — do not commit update scripts copies.
-*
-!.gitkeep
+
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index 832b144..f84b231 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1,30 +1,33 @@
-import {
- app,
- BrowserWindow,
- dialog,
-} from "electron";
+import { app, BrowserWindow, dialog } from "electron";
import path from "node:path";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import type { ChildProcess } from "node:child_process";
import {
+ appendLog,
+ ensureRuntimeDependencies,
freePort,
+ nodeBinary,
+ openLogFile,
runtimeRoot,
spawnHost,
spawnSupervisor,
supervisorToken,
updateScriptsRoot,
waitForUrl,
- nodeBinary,
} from "./runtime.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const appRoot = path.resolve(__dirname, "..");
let mainWindow: BrowserWindow | null = null;
+let splashWindow: BrowserWindow | null = null;
let hostProcess: ChildProcess | null = null;
let supervisorProcess: ChildProcess | null = null;
let shuttingDown = false;
+let logFile: string | null = null;
+let bootFailed = false;
+let booting = true;
function readReleaseMeta(runtime: string): { version?: string; commit?: string } {
try {
@@ -36,9 +39,65 @@ function readReleaseMeta(runtime: string): { version?: string; commit?: string }
}
}
+function showSplash(message: string): void {
+ if (splashWindow && !splashWindow.isDestroyed()) {
+ splashWindow.webContents
+ .executeJavaScript(
+ `document.getElementById("msg").textContent = ${JSON.stringify(message)}`
+ )
+ .catch(() => undefined);
+ return;
+ }
+ splashWindow = new BrowserWindow({
+ width: 420,
+ height: 180,
+ resizable: false,
+ maximizable: false,
+ minimizable: false,
+ fullscreenable: false,
+ autoHideMenuBar: true,
+ title: "GodMode",
+ show: true,
+ webPreferences: {
+ contextIsolation: true,
+ nodeIntegration: false,
+ sandbox: true,
+ },
+ });
+ const html = `
+
+
GodMode
+
${message.replaceAll("<", "<")}
+
`;
+ void splashWindow.loadURL(
+ `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
+ );
+ splashWindow.on("closed", () => {
+ splashWindow = null;
+ });
+}
+
+function closeSplash(): void {
+ if (splashWindow && !splashWindow.isDestroyed()) {
+ splashWindow.close();
+ }
+ splashWindow = null;
+}
+
+function failStartup(title: string, detail: string): void {
+ bootFailed = true;
+ appendLog(logFile, `${title}: ${detail}`);
+ const logHint = logFile ? `\n\nDetails were written to:\n${logFile}` : "";
+ dialog.showErrorBox(title, `${detail}${logHint}`);
+ stopChildren();
+ closeSplash();
+ app.quit();
+}
+
async function boot(): Promise {
const runtime = runtimeRoot(app.isPackaged, appRoot);
const updateRoot = updateScriptsRoot(app.isPackaged, appRoot);
+ ensureRuntimeDependencies(runtime);
const release = readReleaseMeta(runtime);
const [publicPort, bridgePort, supervisorPort] = await Promise.all([
freePort(),
@@ -81,26 +140,32 @@ async function boot(): Promise {
if (release.version) env.GODMODE_VERSION = String(release.version);
if (release.commit) env.GODMODE_COMMIT = String(release.commit);
- hostProcess = spawnHost({ runtime, env });
+ appendLog(
+ logFile,
+ `Starting host runtime=${runtime} public=${publicUrl} data=${dataDir}`
+ );
+ showSplash("Starting local services…");
+
+ hostProcess = spawnHost({ runtime, env, logFile });
hostProcess.on("exit", (code, signal) => {
- if (shuttingDown) return;
- console.error(`GodMode host exited (code=${code} signal=${signal})`);
- app.quit();
+ if (shuttingDown || bootFailed) return;
+ failStartup(
+ "GodMode failed to start",
+ `The local host process exited unexpectedly (code=${code ?? "?"} signal=${signal ?? "none"}).`
+ );
});
- supervisorProcess = spawnSupervisor({ updateRoot, env });
+ supervisorProcess = spawnSupervisor({ updateRoot, env, logFile });
supervisorProcess.on("exit", (code, signal) => {
if (shuttingDown) return;
- console.warn(
+ appendLog(
+ logFile,
`Update supervisor exited (code=${code} signal=${signal}); one-click apply unavailable`
);
});
- await waitForUrl(`${publicUrl}/api/health`).catch(async () => {
- // health may be unauthenticated or named differently — fall back to root
- await waitForUrl(publicUrl);
- });
-
+ showSplash("Waiting for GodMode to become ready…");
+ await waitForUrl(`${publicUrl}/api/health`);
return publicUrl;
}
@@ -113,13 +178,26 @@ function createWindow(url: string): void {
show: false,
title: "GodMode",
webPreferences: {
- preload: path.join(__dirname, "preload.js"),
+ preload: path.join(appRoot, "preload.cjs"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
- mainWindow.once("ready-to-show", () => mainWindow?.show());
+ const showMain = () => {
+ booting = false;
+ closeSplash();
+ if (mainWindow && !mainWindow.isDestroyed()) mainWindow.show();
+ };
+ mainWindow.once("ready-to-show", showMain);
+ // Fallback if ready-to-show never fires (some Windows GPU drivers).
+ setTimeout(showMain, 8_000);
+ mainWindow.webContents.on("did-fail-load", (_e, code, desc, validatedURL) => {
+ failStartup(
+ "GodMode failed to load",
+ `Could not load ${validatedURL} (code ${code}): ${desc}`
+ );
+ });
void mainWindow.loadURL(url);
mainWindow.on("closed", () => {
mainWindow = null;
@@ -131,11 +209,7 @@ function stopChildren(): void {
for (const child of [supervisorProcess, hostProcess]) {
if (!child || child.killed) continue;
try {
- if (process.platform === "win32") {
- child.kill();
- } else {
- child.kill("SIGTERM");
- }
+ child.kill();
} catch {
// ignore
}
@@ -152,25 +226,50 @@ if (!gotLock) {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
+ return;
+ }
+ if (splashWindow && !splashWindow.isDestroyed()) {
+ splashWindow.focus();
}
});
app.whenReady().then(async () => {
+ logFile = openLogFile(app.getPath("userData"));
+ process.on("uncaughtException", (error) => {
+ failStartup(
+ "GodMode crashed",
+ error instanceof Error ? error.stack ?? error.message : String(error)
+ );
+ });
+ process.on("unhandledRejection", (reason) => {
+ failStartup(
+ "GodMode crashed",
+ reason instanceof Error ? reason.stack ?? reason.message : String(reason)
+ );
+ });
+
try {
const devUrl = process.env.GODMODE_DEV_URL?.trim();
- const url = devUrl || (await boot());
+ if (devUrl) {
+ createWindow(devUrl);
+ return;
+ }
+ showSplash("Preparing GodMode…");
+ const url = await boot();
+ if (bootFailed) return;
createWindow(url);
} catch (error) {
- dialog.showErrorBox(
+ if (bootFailed) return;
+ failStartup(
"GodMode failed to start",
- error instanceof Error ? error.message : String(error)
+ error instanceof Error ? error.stack ?? error.message : String(error)
);
- stopChildren();
- app.quit();
}
});
app.on("window-all-closed", () => {
+ // Splash closes before the main window is shown; do not quit mid-boot.
+ if (booting || bootFailed) return;
stopChildren();
app.quit();
});
diff --git a/apps/desktop/src/runtime.ts b/apps/desktop/src/runtime.ts
index 2fa6e7f..6195498 100644
--- a/apps/desktop/src/runtime.ts
+++ b/apps/desktop/src/runtime.ts
@@ -1,6 +1,6 @@
import { createServer } from "node:net";
import { randomBytes } from "node:crypto";
-import { spawn, type ChildProcess } from "node:child_process";
+import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
@@ -34,7 +34,6 @@ export function runtimeRoot(isPackaged: boolean, appPath: string): string {
}
const staged = path.join(appPath, "resources", "runtime");
if (fs.existsSync(path.join(staged, "bin", "host.mjs"))) return staged;
- // Dev fallback: monorepo root when resources/runtime is not staged yet.
return path.resolve(appPath, "..", "..");
}
@@ -49,7 +48,64 @@ export function nodeBinary(runtime: string): string {
const name = process.platform === "win32" ? "node.exe" : "node";
const bundled = path.join(runtime, "bin", name);
if (fs.existsSync(bundled)) return bundled;
- return process.execPath;
+ throw new Error(
+ `Bundled Node runtime missing at ${bundled}. Reinstall GodMode.`
+ );
+}
+
+export function ensureRuntimeDependencies(runtime: string): void {
+ const modules = path.join(runtime, "node_modules");
+ const staged = path.join(runtime, "_node_modules");
+ if (!fs.existsSync(modules) && fs.existsSync(staged)) {
+ try {
+ fs.renameSync(staged, modules);
+ } catch (error) {
+ // Windows Defender / indexer sometimes blocks rename; junction is enough.
+ const linked = spawnSync(
+ "cmd.exe",
+ ["/c", `mklink /J "${modules}" "${staged}"`],
+ { encoding: "utf8" }
+ );
+ if (linked.status !== 0 || !fs.existsSync(modules)) {
+ throw new Error(
+ `Unable to expose runtime dependencies at ${modules}: ${
+ error instanceof Error ? error.message : String(error)
+ }`
+ );
+ }
+ }
+ }
+ const cors = path.join(modules, "cors");
+ if (!fs.existsSync(cors)) {
+ throw new Error(
+ `Desktop runtime is missing dependencies (cors). ` +
+ `Expected packages under ${modules}. Reinstall from a newer GitHub release.`
+ );
+ }
+}
+
+export function openLogFile(userData: string): string {
+ const dir = path.join(userData, "logs");
+ fs.mkdirSync(dir, { recursive: true });
+ const file = path.join(dir, "desktop.log");
+ fs.writeFileSync(
+ file,
+ `\n---- GodMode desktop ${new Date().toISOString()} ----\n`,
+ { flag: "a" }
+ );
+ return file;
+}
+
+export function appendLog(logFile: string | null, message: string): void {
+ if (!logFile) {
+ console.error(message);
+ return;
+ }
+ try {
+ fs.appendFileSync(logFile, `${message}\n`);
+ } catch {
+ console.error(message);
+ }
}
export async function waitForUrl(
@@ -63,9 +119,11 @@ export async function waitForUrl(
const response = await fetch(url, {
signal: AbortSignal.timeout(2_000),
});
- if (response.ok || response.status === 401 || response.status === 404) {
+ // Require a real success — 404 means static assets are missing.
+ if (response.ok || response.status === 401) {
return;
}
+ lastError = new Error(`HTTP ${response.status}`);
} catch (error) {
lastError = error;
}
@@ -78,37 +136,68 @@ export async function waitForUrl(
);
}
+function spawnLogged(
+ command: string,
+ args: string[],
+ options: {
+ cwd: string;
+ env: NodeJS.ProcessEnv;
+ logFile: string | null;
+ label: string;
+ }
+): ChildProcess {
+ const child = spawn(command, args, {
+ cwd: options.cwd,
+ env: options.env,
+ stdio: ["ignore", "pipe", "pipe"],
+ windowsHide: true,
+ });
+ const forward = (chunk: Buffer, stream: "stdout" | "stderr") => {
+ const text = chunk.toString("utf8").trimEnd();
+ if (!text) return;
+ for (const line of text.split(/\r?\n/)) {
+ appendLog(options.logFile, `[${options.label}:${stream}] ${line}`);
+ }
+ };
+ child.stdout?.on("data", (chunk: Buffer) => forward(chunk, "stdout"));
+ child.stderr?.on("data", (chunk: Buffer) => forward(chunk, "stderr"));
+ return child;
+}
+
export function spawnHost(options: {
runtime: string;
env: NodeJS.ProcessEnv;
+ logFile: string | null;
}): ChildProcess {
const node = nodeBinary(options.runtime);
const hostScript = path.join(options.runtime, "bin", "host.mjs");
if (!fs.existsSync(hostScript)) {
throw new Error(`Missing host runtime at ${hostScript}`);
}
- return spawn(node, [hostScript], {
+ return spawnLogged(node, [hostScript], {
cwd: options.runtime,
env: options.env,
- stdio: "inherit",
- windowsHide: true,
+ logFile: options.logFile,
+ label: "host",
});
}
export function spawnSupervisor(options: {
updateRoot: string;
env: NodeJS.ProcessEnv;
+ logFile: string | null;
}): ChildProcess {
- const node = options.env.GODMODE_NODE_BIN || process.execPath;
+ const node = options.env.GODMODE_NODE_BIN;
+ if (!node) throw new Error("GODMODE_NODE_BIN is required for the supervisor");
const script = path.join(options.updateRoot, "supervisor.mjs");
if (!fs.existsSync(script)) {
throw new Error(`Missing update supervisor at ${script}`);
}
- return spawn(node, [script], {
+ return spawnLogged(node, [script], {
cwd: options.updateRoot,
env: options.env,
- stdio: "inherit",
- windowsHide: true,
+ logFile: options.logFile,
+ label: "supervisor",
});
}
diff --git a/docs/RELEASES.md b/docs/RELEASES.md
index 9cde420..3c3dc5a 100644
--- a/docs/RELEASES.md
+++ b/docs/RELEASES.md
@@ -104,9 +104,13 @@ Non-technical users should install the signed desktop app from GitHub Releases:
| Bare-metal Windows | `godmode-windows-bare-metal-.zip` |
The Electron shell boots the same Bridge + web runtime as bare-metal, binds only
-on loopback, stores SQLite under the OS app data directory, and starts a local
-update supervisor so **Admin → Updates** can download, Sigstore-verify, and
-apply the matching `installer` artifact.
+on loopback, stores SQLite under the OS app data directory
+(`%APPDATA%\\GodMode\\data` on Windows), and starts a local update supervisor so
+**Admin → Updates** can download, Sigstore-verify, and apply the matching
+`installer` artifact.
+
+If the desktop app fails to start, check `%APPDATA%\\GodMode\\logs\\desktop.log`
+(macOS: `~/Library/Application Support/GodMode/logs/desktop.log`).
Set these GitHub Actions secrets for **stable** signed desktop builds:
diff --git a/scripts/release/__tests__/contract.test.mjs b/scripts/release/__tests__/contract.test.mjs
index 8487b2d..96c54f7 100644
--- a/scripts/release/__tests__/contract.test.mjs
+++ b/scripts/release/__tests__/contract.test.mjs
@@ -161,14 +161,26 @@ test("bare-metal updater selects signed bundle artifacts", async () => {
assert.match(updater, /artifact\.sha256/);
});
-test("desktop updater selects signed installer artifacts", async () => {
- const updater = await readFile("scripts/update/desktop-update.mjs", "utf8");
- assert.match(updater, /kind === "installer"/);
- assert.match(updater, /AppImage/);
- assert.match(updater, /NSIS|\/S/);
- assert.match(updater, /\$\{manifestUrl\}\.bundle/);
- assert.doesNotMatch(updater, /\$\{artifactUrl\}\.bundle/);
- assert.match(updater, /artifact\.sha256/);
+test("desktop packaging dodges gitignore stripping of node_modules", async () => {
+ const packager = await readFile("scripts/release/package-desktop.mjs", "utf8");
+ const afterPack = await readFile("apps/desktop/after-pack.cjs", "utf8");
+ const desktopPkg = await readFile("apps/desktop/package.json", "utf8");
+ assert.match(packager, /_node_modules/);
+ assert.match(packager, /extraMetadata\.name=GodMode/);
+ assert.match(afterPack, /_node_modules/);
+ assert.match(afterPack, /cors/);
+ assert.match(desktopPkg, /after-pack\.cjs/);
+});
+
+test("desktop shell surfaces boot failures instead of quitting silently", async () => {
+ const main = await readFile("apps/desktop/src/main.ts", "utf8");
+ const runtime = await readFile("apps/desktop/src/runtime.ts", "utf8");
+ assert.match(main, /showSplash|Preparing GodMode/);
+ assert.match(main, /failStartup/);
+ assert.match(main, /desktop\.log|openLogFile/);
+ assert.match(runtime, /ensureRuntimeDependencies/);
+ assert.match(runtime, /stdio: \["ignore", "pipe", "pipe"\]/);
+ assert.doesNotMatch(runtime, /return process\.execPath/);
});
test("supervisor routes electron surface to desktop-update", async () => {
diff --git a/scripts/release/package-desktop.mjs b/scripts/release/package-desktop.mjs
index 8148cac..5da7da9 100644
--- a/scripts/release/package-desktop.mjs
+++ b/scripts/release/package-desktop.mjs
@@ -1,4 +1,4 @@
-import { cp, mkdir, readdir, rm } from "node:fs/promises";
+import { cp, mkdir, readdir, rename, rm, access } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import path from "node:path";
import {
@@ -7,6 +7,15 @@ import {
} from "./artifact-names.mjs";
import { hostPlatformLabel, stageRuntime } from "./stage-runtime.mjs";
+async function exists(target) {
+ try {
+ await access(target);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
const outputDirectory = process.argv[2] ?? "release-out";
const version = process.env.RELEASE_VERSION;
const commit = process.env.RELEASE_COMMIT;
@@ -22,7 +31,13 @@ const root = process.cwd();
const desktopRoot = path.join(root, "apps", "desktop");
const runtimeDest = path.join(desktopRoot, "resources", "runtime");
const updateDest = path.join(desktopRoot, "resources", "update");
-const stage = path.join(desktopRoot, ".stage-runtime");
+// Prefer a fresh temp stage so leftover locked files under apps/desktop/.stage-runtime
+// (Windows AV / prior electron copies) cannot block packaging.
+const stage = path.join(
+ root,
+ "release-out",
+ `.desktop-stage-${process.pid}-${Date.now()}`
+);
await stageRuntime({
platform,
@@ -34,10 +49,20 @@ await stageRuntime({
});
await rm(runtimeDest, { recursive: true, force: true });
-await mkdir(runtimeDest, { recursive: true });
+await mkdir(path.dirname(runtimeDest), { recursive: true });
+// Copy stage directory onto runtimeDest path (replace), not into an existing folder.
await cp(stage, runtimeDest, { recursive: true });
await rm(stage, { recursive: true, force: true });
+// electron-builder honors .gitignore and would omit node_modules from extraResources.
+// Stage deps under _node_modules; after-pack.cjs renames them back inside the app.
+const runtimeNodeModules = path.join(runtimeDest, "node_modules");
+const runtimeBundledModules = path.join(runtimeDest, "_node_modules");
+if (await exists(runtimeNodeModules)) {
+ await rm(runtimeBundledModules, { recursive: true, force: true });
+ await rename(runtimeNodeModules, runtimeBundledModules);
+}
+
await rm(updateDest, { recursive: true, force: true });
await mkdir(updateDest, { recursive: true });
for (const name of ["supervisor.mjs", "desktop-update.mjs", "bare-metal-update.mjs"]) {
@@ -45,13 +70,16 @@ for (const name of ["supervisor.mjs", "desktop-update.mjs", "bare-metal-update.m
}
const electronVersion = version.replace(/^v/, "");
+const packDirOnly = process.env.DESKTOP_PACK_DIR === "1";
const builderArgs = [
"run",
- "dist",
+ packDirOnly ? "pack" : "dist",
"-w",
"@godmode/desktop",
"--",
`-c.extraMetadata.version=${electronVersion}`,
+ // Avoid NSIS install dir "@godmodedesktop" from the scoped package name.
+ "-c.extraMetadata.name=GodMode",
"--publish",
"never",
];
diff --git a/scripts/release/stage-runtime.mjs b/scripts/release/stage-runtime.mjs
index ef1c101..72124a5 100644
--- a/scripts/release/stage-runtime.mjs
+++ b/scripts/release/stage-runtime.mjs
@@ -41,13 +41,38 @@ export async function stageRuntime({
const copy = (source, destination) =>
cp(path.join(root, source), path.join(stageDir, destination), {
recursive: true,
+ // Keep links/junctions as-is for speed; @godmode workspaces are replaced below.
verbatimSymlinks: true,
});
+ const skipPackagingTooling = !includeServices;
+ const nodeModulesSource = path.join(root, "node_modules");
+ const nodeModulesDest = path.join(stageDir, "node_modules");
+ await mkdir(nodeModulesDest, { recursive: true });
+ const nmEntries = await import("node:fs/promises").then(({ readdir }) =>
+ readdir(nodeModulesSource)
+ );
+ const skipNames = new Set([
+ "electron",
+ "electron-builder",
+ "electron-publish",
+ "electron-winstaller",
+ "app-builder-bin",
+ "app-builder-lib",
+ "@electron",
+ ]);
+
await Promise.all([
copy("package.json", "package.json"),
copy("package-lock.json", "package-lock.json"),
- copy("node_modules", "node_modules"),
+ ...nmEntries
+ .filter((entry) => !(skipPackagingTooling && (skipNames.has(entry) || /^electron/i.test(entry))))
+ .map((entry) =>
+ cp(path.join(nodeModulesSource, entry), path.join(nodeModulesDest, entry), {
+ recursive: true,
+ verbatimSymlinks: true,
+ })
+ ),
copy("apps/bridge/package.json", "apps/bridge/package.json"),
copy("apps/bridge/dist", "apps/bridge/dist"),
copy("apps/web/dist", "apps/web/dist"),