From 8e84961d4af016fa5772bb91dbe1b5c717ebc843 Mon Sep 17 00:00:00 2001 From: Leigh Stillard Date: Tue, 24 Mar 2026 03:43:26 +0000 Subject: [PATCH] fix(standalone): fix standalone mode to run without Electron Three issues prevented standalone (non-Electron) mode from working: 1. sentry.ts used a top-level `import * from '@sentry/electron/main'` which crashes in plain Node.js. Changed to a try/catch require() so the module is safe to import in both environments. 2. vite.standalone.config.ts resolved all paths relative to __dirname (docker/) but is invoked from the repo root. Fixed to resolve relative to the repo root via a ROOT constant. 3. The electron stub was missing `safeStorage` and `screen` exports that newer code imports. Added them, and externalized agent-teams-controller (plain CJS with relative requires that break when bundled by Vite). --- docker/vite.standalone.config.ts | 19 +++++++++---- src/main/sentry.ts | 47 ++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/docker/vite.standalone.config.ts b/docker/vite.standalone.config.ts index ac24a4d7..8179e125 100644 --- a/docker/vite.standalone.config.ts +++ b/docker/vite.standalone.config.ts @@ -10,6 +10,11 @@ import { defineConfig } from 'vite' import type { Plugin } from 'vite' +// This config lives in docker/ but is invoked from the repo root via +// `vite build --config docker/vite.standalone.config.ts`, so __dirname +// is docker/. All paths must resolve relative to the repo root. +const ROOT = resolve(__dirname, '..') + // Node.js built-in modules that should be externalized const nodeBuiltins = new Set([ 'fs', 'path', 'os', 'events', 'stream', 'util', 'net', 'tls', @@ -21,7 +26,8 @@ const nodeBuiltins = new Set([ // Packages that must be externalized because they break when bundled // (fastify ecosystem uses internal file resolution that doesn't survive bundling) const externalPackages = [ - 'fastify', '@fastify/cors', '@fastify/static' + 'fastify', '@fastify/cors', '@fastify/static', + 'agent-teams-controller' ] // Stub native .node addons (ssh2/cpu-features have JS fallbacks) @@ -57,6 +63,8 @@ export const ipcMain = { handle: noop, on: noop, removeHandler: noop }; export const shell = { openPath: noop, openExternal: noop }; export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) }; export const Notification = class { show() {} }; +export const safeStorage = { isEncryptionAvailable: () => false, encryptString: noop, decryptString: () => '' }; +export const screen = proxyObj; export default proxyObj; ` return { @@ -75,12 +83,13 @@ export default proxyObj; } export default defineConfig({ + root: ROOT, plugins: [nativeModuleStub(), electronStub()], resolve: { alias: { - '@main': resolve(__dirname, 'src/main'), - '@shared': resolve(__dirname, 'src/shared'), - '@preload': resolve(__dirname, 'src/preload') + '@main': resolve(ROOT, 'src/main'), + '@shared': resolve(ROOT, 'src/shared'), + '@preload': resolve(ROOT, 'src/preload') } }, ssr: { @@ -94,7 +103,7 @@ export default defineConfig({ ssr: true, rollupOptions: { input: { - index: resolve(__dirname, 'src/main/standalone.ts') + index: resolve(ROOT, 'src/main/standalone.ts') }, output: { format: 'cjs', diff --git a/src/main/sentry.ts b/src/main/sentry.ts index 7c0fb2b7..e00ec248 100644 --- a/src/main/sentry.ts +++ b/src/main/sentry.ts @@ -5,9 +5,11 @@ * so that Sentry captures errors from the earliest point possible. * * When `SENTRY_DSN` is not set (dev / self-builds), everything is a no-op. + * + * The @sentry/electron/main import is lazy so this module can be safely + * loaded in standalone (non-Electron) mode without crashing. */ -import * as Sentry from '@sentry/electron/main'; import { isValidDsn, SENTRY_ENVIRONMENT, @@ -34,25 +36,40 @@ export function syncTelemetryFlag(enabled: boolean): void { } // --------------------------------------------------------------------------- -// Init +// Lazy Sentry import — safe in non-Electron environments // --------------------------------------------------------------------------- -const dsn = process.env.SENTRY_DSN; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let Sentry: any = null; let initialized = false; -if (isValidDsn(dsn)) { - Sentry.init({ - dsn, - release: SENTRY_RELEASE, - environment: SENTRY_ENVIRONMENT, - tracesSampleRate: TRACES_SAMPLE_RATE, - sendDefaultPii: false, +const dsn = process.env.SENTRY_DSN; - beforeSend(event) { - return telemetryAllowed ? event : null; - }, - }); - initialized = true; +if (isValidDsn(dsn)) { + try { + // Dynamic import would be cleaner but top-level await is not available + // in all contexts. require() is synchronous and works in both Electron + // and Node.js — it simply throws in standalone mode where the electron + // module is not resolvable. + // eslint-disable-next-line @typescript-eslint/no-require-imports + Sentry = require('@sentry/electron/main'); + Sentry.init({ + dsn, + release: SENTRY_RELEASE, + environment: SENTRY_ENVIRONMENT, + tracesSampleRate: TRACES_SAMPLE_RATE, + sendDefaultPii: false, + + beforeSend(event: unknown) { + return telemetryAllowed ? event : null; + }, + }); + initialized = true; + } catch { + // @sentry/electron/main requires Electron runtime — not available in + // standalone (pure Node.js) mode. All exported helpers are no-ops when + // initialized is false, so this is safe to swallow. + } } // ---------------------------------------------------------------------------