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).
This commit is contained in:
Leigh Stillard 2026-03-24 03:43:26 +00:00
parent d6ee7bc320
commit 8e84961d4a
2 changed files with 46 additions and 20 deletions

View file

@ -10,6 +10,11 @@ import { defineConfig } from 'vite'
import type { Plugin } 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 // Node.js built-in modules that should be externalized
const nodeBuiltins = new Set([ const nodeBuiltins = new Set([
'fs', 'path', 'os', 'events', 'stream', 'util', 'net', 'tls', '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 // Packages that must be externalized because they break when bundled
// (fastify ecosystem uses internal file resolution that doesn't survive bundling) // (fastify ecosystem uses internal file resolution that doesn't survive bundling)
const externalPackages = [ 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) // 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 shell = { openPath: noop, openExternal: noop };
export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) }; export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) };
export const Notification = class { show() {} }; export const Notification = class { show() {} };
export const safeStorage = { isEncryptionAvailable: () => false, encryptString: noop, decryptString: () => '' };
export const screen = proxyObj;
export default proxyObj; export default proxyObj;
` `
return { return {
@ -75,12 +83,13 @@ export default proxyObj;
} }
export default defineConfig({ export default defineConfig({
root: ROOT,
plugins: [nativeModuleStub(), electronStub()], plugins: [nativeModuleStub(), electronStub()],
resolve: { resolve: {
alias: { alias: {
'@main': resolve(__dirname, 'src/main'), '@main': resolve(ROOT, 'src/main'),
'@shared': resolve(__dirname, 'src/shared'), '@shared': resolve(ROOT, 'src/shared'),
'@preload': resolve(__dirname, 'src/preload') '@preload': resolve(ROOT, 'src/preload')
} }
}, },
ssr: { ssr: {
@ -94,7 +103,7 @@ export default defineConfig({
ssr: true, ssr: true,
rollupOptions: { rollupOptions: {
input: { input: {
index: resolve(__dirname, 'src/main/standalone.ts') index: resolve(ROOT, 'src/main/standalone.ts')
}, },
output: { output: {
format: 'cjs', format: 'cjs',

View file

@ -5,9 +5,11 @@
* so that Sentry captures errors from the earliest point possible. * so that Sentry captures errors from the earliest point possible.
* *
* When `SENTRY_DSN` is not set (dev / self-builds), everything is a no-op. * 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 { import {
isValidDsn, isValidDsn,
SENTRY_ENVIRONMENT, 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; let initialized = false;
if (isValidDsn(dsn)) { const dsn = process.env.SENTRY_DSN;
Sentry.init({
dsn,
release: SENTRY_RELEASE,
environment: SENTRY_ENVIRONMENT,
tracesSampleRate: TRACES_SAMPLE_RATE,
sendDefaultPii: false,
beforeSend(event) { if (isValidDsn(dsn)) {
return telemetryAllowed ? event : null; try {
}, // Dynamic import would be cleaner but top-level await is not available
}); // in all contexts. require() is synchronous and works in both Electron
initialized = true; // 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.
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------