Skip to content

desktop: fix packaged app crash on startup (bundle the bearer helpers, not the server) - #1018

Merged
RhysSullivan merged 1 commit into
mainfrom
claude/packaged-desktop-ws-fix
Jun 14, 2026
Merged

desktop: fix packaged app crash on startup (bundle the bearer helpers, not the server)#1018
RhysSullivan merged 1 commit into
mainfrom
claude/packaged-desktop-ws-fix

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

The bug

The packaged desktop app crashed on startup:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws' imported from
.../resources/app.asar/out/main/index.js

The Electron main process imports loadOrMintLocalAuthToken /
rotateLocalAuthToken from the @executor-js/local barrel (./src/index.ts),
which also exports the full local server (startServer). That pulls the entire
server module graph — including ws — into the main bundle. electron-vite's
externalizeDepsPlugin externalizes ws, but ws isn't a declared desktop
dependency, so electron-builder never bundles it into the asar. Dev electron
hides this because it resolves ws from the monorepo node_modules; only the
packaged artifact hits it.

The fix

The token helpers in apps/local/src/auth.ts are pure node:fs / node:crypto
— no server, no ws, no Effect. Expose them via a dedicated
@executor-js/local/auth subpath and import from it in the desktop main.
The sidecar process (src/sidecar/server.ts) keeps the full barrel because it
actually runs the server.

ws references in the built main bundle drop 547 → 0.

Proof

The packaged Linux bundle, launched headless under Xvfb in a VM, now boots
and logs attaching to supervised daemon at http://127.0.0.1:4789 (pid …), and
the server manifest stays kind: cli-daemon with the daemon's pid — i.e. the
packaged app attached to the supervised daemon rather than spawning its own
sidecar. The harness is e2e/scripts/verify-linux-desktop-attach.ts (manual;
Xvfb sidesteps the macOS Aqua-session limitation that gates the
desktop-packaged vitest project).

…fix packaged crash)

The Electron main process imported `loadOrMintLocalAuthToken` /
`rotateLocalAuthToken` from the `@executor-js/local` barrel, which also exports
the full local server (`startServer`). That dragged the entire server module
graph — including `ws` — into the main bundle. electron-vite's
externalizeDepsPlugin then externalized `ws`, but `ws` is not a declared
desktop dependency, so electron-builder never bundled it. The result: the
PACKAGED app crashed on startup with `ERR_MODULE_NOT_FOUND: Cannot find package
'ws'`. Dev electron hid this — it resolves `ws` from the monorepo node_modules.

The auth helpers in apps/local/src/auth.ts are pure `node:fs`/`node:crypto`
with no server or Effect imports. Expose them via a dedicated
`@executor-js/local/auth` subpath and import from it in the desktop main; the
sidecar process (src/sidecar/server.ts) keeps the full barrel since it runs the
server. `ws` references in the built main bundle drop from 547 to 0.

Proven end-to-end: the packaged Linux bundle, launched headless under Xvfb in a
VM, now boots and logs "attaching to supervised daemon …" and the manifest
stays kind=cli-daemon with the daemon's pid (attached, not a spawned sidecar) —
e2e/scripts/verify-linux-desktop-attach.ts.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing b1211f4 Commit Preview URL

Branch Preview URL
Jun 14 2026, 05:08 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud b1211f4 Jun 14 2026, 05:08 PM

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a packaged-app crash by introducing a @executor-js/local/auth subpath export that exposes only the pure Node built-in bearer-token helpers (loadOrMintLocalAuthToken, rotateLocalAuthToken), and updates the Electron main-process imports to use it instead of the full barrel that dragged in ws (and the entire server module graph) into the bundled main.

  • apps/local/package.json adds "./auth": "./src/auth.ts" to the export map; auth.ts has zero non-built-in dependencies, so electron-vite's externalize plugin no longer touches any of its imports.
  • settings.ts / sidecar.ts (Electron main) are each updated with a single import-path change; a grep confirms no remaining @executor-js/local barrel imports exist in the main-process bundle.
  • e2e/scripts/verify-linux-desktop-attach.ts is a new manual Linux/Xvfb harness (not in the vitest matrix) that proves the packaged app attaches to a supervised daemon end-to-end.

Confidence Score: 4/5

Safe to merge; the fix is minimal and targeted, the desktop main no longer pulls in any server-side dependencies.

The three production-code changes are each a single import-path swap, and auth.ts is demonstrably dependency-free (only Node built-ins). The one rough edge is in the manual e2e harness: the daemon-manifest polling loop exits silently on timeout, which would surface as a confusing SyntaxError rather than a clear diagnostic — but this is a test-script quality issue, not a regression in the shipped code.

e2e/scripts/verify-linux-desktop-attach.ts — the daemon-readiness polling loop could be more defensive on timeout.

Important Files Changed

Filename Overview
apps/local/package.json Adds a new ./auth subpath export pointing directly to src/auth.ts (pure Node built-ins only), cleanly severing the ws/Effect dependency chain for consumers that only need the bearer-token helpers.
apps/desktop/src/main/settings.ts Single import swap: rotateLocalAuthToken now comes from @executor-js/local/auth instead of the full barrel. No @executor-js/local imports remain in the Electron main bundle after this change.
apps/desktop/src/main/sidecar.ts Single import swap: loadOrMintLocalAuthToken now comes from @executor-js/local/auth. All remaining logic is unchanged; the full @executor-js/local barrel is still used by the separate sidecar binary (correct).
e2e/scripts/verify-linux-desktop-attach.ts New manual Linux/Xvfb verification harness that proves the packaged app attaches to a supervised daemon. The daemon-manifest polling loop exits silently on timeout, surfacing later as a cryptic JSON.parse error rather than an explicit failure message.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Electron main process] -->|was: import from @executor-js/local| B[Full barrel index.ts]
    B -->|re-exports| C[startServer imports ws]
    C -->|electron-vite externalizes ws| D[ws missing from asar - crash]

    A2[Electron main process] -->|now: import from @executor-js/local/auth| E[auth.ts subpath]
    E -->|only node:fs and node:crypto| F[no ws, no Effect, no crash]

    G[Sidecar binary - separate process] -->|still imports from @executor-js/local| B
    B -->|ws is a dependency of sidecar binary| H[ws bundled in sidecar binary]
Loading

Reviews (1): Last reviewed commit: "desktop: import the local bearer helpers..." | Re-trigger Greptile

Comment on lines +83 to +90

log("starting bundled sidecar as supervised daemon...");
await vm.ssh(
`rm -rf ${home}; mkdir -p ${home}; nohup env HOME=${home} EXECUTOR_SUPERVISED=1 EXECUTOR_DATA_DIR=${home}/.executor EXECUTOR_PORT=4789 EXECUTOR_HOST=127.0.0.1 EXECUTOR_AUTH_TOKEN=linux-attach EXECUTOR_CLIENT_DIR=${webui} ${sidecar} > /tmp/daemon.log 2>&1 &`,
);
// Wait for the daemon to publish its manifest + serve.
for (let i = 0; i < 30; i++) {
const r = await vm.ssh(`cat ${home}/.executor/server-control/server.json 2>/dev/null`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent timeout before manifest parse

If the daemon never writes server.json within the 30-second poll window the loop exits silently, and the subsequent JSON.parse(before) on line 92 will throw a SyntaxError (empty string) rather than a clear diagnostic. The intent check on line 95 (if (beforeManifest.kind !== "cli-daemon") throw …) only helps if the file exists with wrong content. Adding a throw after the loop on timeout would surface the actual failure condition immediately.

@pkg-pr-new

pkg-pr-new Bot commented Jun 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1018

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1018

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1018

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1018

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1018

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1018

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1018

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1018

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1018

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1018

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1018

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1018

executor

npm i https://pkg.pr.new/executor@1018

commit: b1211f4

@RhysSullivan
RhysSullivan merged commit 1e1567e into main Jun 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant