@burdenoff/desktop-shell
v2026.825.3
Published
Shared Electron main-process + preload runtime for Burdenoff product app shells (tray, native menus, window state, notifications, badges, theme sync, auto-update UX, deep links).
Readme
@burdenoff/desktop-shell
The shared Electron runtime and build tooling behind every Burdenoff desktop app
shell — tray, native menus, window state, notifications, badges, theme sync,
auto-update UX and deep links in one package, plus the burdenoff-desktop CLI
that stages, launches, smoke-tests and verifies a shell.
Before this package each of the ~40 app shells carried its own 30 kB
electron/main.cjs, its own scripts/prepare-electron.mjs, and its own copy of
a "build" block in package.json. They drifted, and the drift shipped.
The two bugs this fixes
1. The packaged app shipped no node_modules.
directories.app pointed at electron/build/app, a folder the stage script
filled with the web build and two .cjs files — and nothing else. Inside the
packaged asar, require('electron-updater') and
require('@capgo/electron-updater') therefore threw at startup. Both were
wrapped in try/catch, so every released installer looked healthy while
auto-update and CapGo OTA were silently dead.
burdenoff-desktop stage now bundles electron/main.cjs with
bun build --target=node --format=cjs --external electron, inlining
@burdenoff/desktop-shell and electron-updater, and then asserts the output
contains no bare updater imports. CapGo is already embedded in the published
desktop-shell bundle with a streaming extractor that rejects symlinks, path
escapes and oversized archives. A staged app that still needs node_modules
fails the build.
2. The sandboxed preload could not require CapGo.
Sandboxed preloads resolve only electron and a handful of builtins. Any npm
require from the preload is a hard failure, so the renderer bridge never
appeared. The preload is bundled the same way and asserted against an explicit
allow-list (electron, events, timers, url).
Both assertions run in stage, in this repo's scripts/verify-bundle.ts, and
again as an "assert staged bundle" step in the release workflow.
A third, quieter bug: 38 of 40 shells shipped the default Electron icon,
because the old stage script copied build/icon.png only if it happened to
exist. stage now resolves an icon explicitly and fails when there isn't a
square PNG of at least 512px.
Install into a shell
Four wrapper files, one config slice, one package.json delta:
electron/main.cjs require('@burdenoff/desktop-shell/main').createDesktopApp(require('./desktop.config.cjs'));
electron/preload.cjs require('@burdenoff/desktop-shell/preload').installPreload(require('./desktop.config.cjs'));
electron/desktop.config.cjs module.exports = require('../native.config.json').desktop ?? require('../native.config.json');
electron-builder.config.cjs module.exports = require('@burdenoff/desktop-shell/builder').createElectronBuilderConfig(require('./electron/desktop.config.cjs'));{
"main": "electron/main.cjs",
"dependencies": { "@burdenoff/desktop-shell": "<CalVer>" },
"devDependencies": {
"electron": "^43.4.1",
"electron-builder": "^26.0.12",
"playwright-core": "^1.50.0" // drives `burdenoff-desktop smoke`
}
}Removed from the shell: the "build" key, electron-updater,
@capgo/electron-updater, @capacitor-community/electron,
scripts/prepare-electron.mjs, scripts/electron-launch.mjs,
scripts/electron-before-build.cjs, src/electronRuntime.ts.
Canonical scripts, templates and every {{TOKEN}} the fleet generator
substitutes: templates/README.md.
Verify with bunx burdenoff-desktop doctor — it must exit 0.
CLI reference
burdenoff-desktop <command> [options]
-h, --help this help (or `<command> --help`)
-v, --version package versionstage
Builds electron/build/app — the folder electron-builder packages. Replaces
scripts/prepare-electron.mjs.
| Option | Default | Meaning |
|---|---|---|
| --root <dir> | cwd | Shell root |
| --env <name> | $BURDENOFF_DESKTOP_ENV | local | alpha | prod (staging/production accepted) |
| --web-dist <dir> | dist | Web build to copy in |
| --deps-mode <mode> | bundle | bundle inlines every dependency; install writes dependencies into the staged package.json and runs bun install --production --ignore-scripts inside it (documented fallback for a genuinely un-bundleable native module) |
| --launch | off | Run launch afterwards |
It: reads the config → resolves the icon (fails if missing) → wipes
electron/build/app → copies the web build → bundles main.cjs + preload.cjs
→ asserts self-containment → writes electron/runtime-config.json and a
dependency-free package.json → stages icons + entitlements → on macOS
rebrands a dev copy of Electron.app (PlistBuddy + ad-hoc codesign) so the dock
icon and menu title are the product's.
launch
burdenoff-desktop launch [--root <dir>] [--env <name>] [-- <electron args>]
Spawns the local Electron on the staged app with stdio inherited, forwarding
every BURDENOFF_DESKTOP_* variable (legacy <PREFIX>_ELECTRON_* values are
normalised to the canonical names first). On macOS it prefers the rebranded
electron/build/<ProductName>.app. Replaces scripts/electron-launch.mjs.
No runtime-config argument is passed on the command line:
RUNTIME_ARG_PREFIXis the main → preload channel (URI-encoded JSON viaadditionalArguments, withcfAccessstripped). Main reads the stagedruntime-config.jsonitself, andBURDENOFF_DESKTOP_*overrides it.
smoke
burdenoff-desktop smoke [--root <dir>] [--packaged <dir>] [--timeout <ms>] [--screenshot <path>] [--no-tray] [--allow-missing-driver]
Launches the staged app (or a packaged build, e.g.
electron/dist/linux-unpacked) through playwright-core's Electron driver with
BURDENOFF_DESKTOP_SMOKE=1 and asserts:
| Check | Assertion |
|---|---|
| window | a first window opens |
| title | equals productName |
| bridge | window.__burdenoffDesktop.product.id equals product |
| diagnostics | globalThis.__burdenoffDesktopDiagnostics is published |
| protocol | protocolRegistered |
| menu | menu |
| tray | tray (skipped with --no-tray — headless CI has no tray host) |
| deep-link | app.emit('open-url', …, '<scheme>://app/smoke') reaches the renderer |
| updater / capgo / webRoot | reported, not asserted |
Writes electron/dist/smoke/window.png and prints a table; exits non-zero with
a readable report on failure. Run under xvfb-run on Linux. Without
playwright-core it exits 1, or 0 with --allow-missing-driver.
icons
burdenoff-desktop icons [--root <dir>] [--check]
Materialises electron/build/icons/ (electron-builder's buildResources):
icon.png from iconSource → public/icons/icon-512x512.png →
public/icons/icon-512.png → electron/icon.png → build/icon.png, plus
entitlements.mac.plist from this package. --check reports only.
electron-builder renders .icns/.ico from the PNG, so no image library is
involved.
doctor
burdenoff-desktop doctor [--root <dir>] [--json]
Read-only verification of a shell — config validity (every problem reported, not
just the first), thin wrappers, electron-builder.config.cjs, resolvable icons,
required/forbidden dependencies, canonical scripts, leftover legacy files, and
the removed "build" key. Prints a table; --json emits
{ root, product, ok, checks: [{ id, status, summary, problems }] } for the
fleet verifier. Exit 1 on any failure.
The expected package.json delta is read from this package's
templates/package-scripts.json, so the migration template and the verifier
can never drift.
Config contract
src/shared/types.ts is authoritative. The desktop slice of a shell's
native.config.json:
{
"desktop": {
"product": "vibecontrols", // slug: Linux executableName, staged package name
"productName": "VibeControls", // window title, tray, installer, artifact names
"appId": "com.vibecontrols.www", // NEVER rewrite — updater identity + Windows AUMID
"capgoAppId": "com.vibecontrols.desktop", // omit to disable CapGo OTA
"scheme": "vibecontrols", // deep links + Linux MimeType
"domain": "vibecontrols.com", // app origins + update feeds
"devServerPort": 5200,
"envPrefix": "VIBE", // legacy VIBE_ELECTRON_* fallback ("VIBE_ELECTRON" also accepted)
"appOrigins": { "prod": "https://admin.burdenoff.com" }, // optional override
"category": { "mac": "public.app-category.developer-tools", "linux": "Development" },
"links": { "docs": "…", "support": "…", "status": "…" },
"window": { "width": 1440, "height": 900, "minWidth": 960, "minHeight": 600 },
"closeToTray": { "win32": true, "linux": false },
"iconSource": "public/icons/icon-512x512.png",
"maintainer": "Name <email>",
"legacyGlobal": true
}
}electron/desktop.config.cjs is also accepted as the source and takes priority.
Environment
Every variable is BURDENOFF_DESKTOP_<KEY>, with a fallback to the legacy
<envPrefix>_ELECTRON_<KEY> so un-migrated shells and workflows keep working:
| Variable | Effect |
|---|---|
| ENV | local | alpha | prod |
| VERSION | CalVer baked into the staged package.json |
| START_URL | dev-server origin the window loads |
| UPDATE_FEED_URL | overrides the derived electron-updater feed |
| GLOBAL_BASE_URL / WORKSPACE_BASE_URL / PUBLIC_STORE_BASE_URL | gateway overrides |
| CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET | alpha only — baked in so electron-updater can reach the Access-gated alpha downloads host |
| DEVTOOLS | open devtools on start |
Defaults derived from domain: prod feed https://<domain>/downloads/latest,
alpha feed https://alphadownloads.<domain>/downloads/latest, app origins
https://app.<domain> / https://alphaapp.<domain>.
CF Access credentials are baked into alpha builds only — possessing an alpha binary means being on the team. They are never written into a prod or local build, and the CLI redacts them in every log line.
electron-builder config
createElectronBuilderConfig(config, overrides?) derives the whole
configuration. Same artifacts as the old hand-copied "build" blocks
(AppImage + deb x64, universal dmg + zip, NSIS x64, asar,
${productName}-${version}-${arch}.${ext}), with the fleet fixes:
directories.buildResourcesiselectron/build/icons, notbuild/— several shells generatebuild/i18n-bundle.jsonthere;- Linux
.desktopgetsMimeType=x-scheme-handler/<scheme>;— without it deep links silently do nothing on Linux; npmRebuild: falsereplaces the oldbeforeBuild: () => falsehack;- macOS: hardened runtime,
gatekeeperAssess: false, entitlements, andnotarize: false— notarization is an explicitxcrun notarytoolCI step so a build never blocks on Apple credentials; - no
publishkey: the release workflow syncs to S3 itself.
appId is passed through verbatim. Shells differ (com.burdenoff.<p> vs
com.<p>.www) and rewriting one orphans every installed user's app data and
breaks the updater.
Release flow
bun run electron:build:<env> → burdenoff-desktop stage → assert staged
bundle → electron-builder --config electron-builder.config.cjs → sign →
smoke → S3 → CloudFront invalidation.
templates/workflows/release-electron.yml is the canonical pipeline
(workflow_dispatch with platforms/channel/version/dry_run/mark_latest):
a capgo-ota job (production only), linux / macos (macos-15) / windows
build jobs, and a publish job uploading downloads/<ver>/<os>/, mirroring
downloads/latest/<os>/ with stable un-versioned aliases, writing
manifest.json and invalidating CloudFront. macOS signs with CSC_LINK and
notarizes/staples via notarytool; Windows signs with
azure/[email protected] and then re-computes the latest.yml
sha512/size, because signing rewrites the binary after electron-builder
hashed it and electron-updater would otherwise reject the download.
templates/workflows/native-smoke.yml is the ~6-minute path-filtered PR guard:
install → doctor → build → stage → electron-builder --dir --linux →
xvfb-run burdenoff-desktop smoke --packaged → screenshot artifact.
Migration checklist
- Add the
desktopslice tonative.config.json. - Stamp
electron/main.cjs,electron/preload.cjs,electron/desktop.config.cjs,electron-builder.config.cjs. - Apply
templates/package-scripts.json(mergescripts/deps, applyremove). - Delete
scripts/prepare-electron.mjs,scripts/electron-launch.mjs,scripts/electron-before-build.cjs,src/electronRuntime.ts, and the"build"key. - Ensure a square PNG ≥512px exists (
public/icons/icon-512x512.png). - Stamp both workflows.
bun install && bunx burdenoff-desktop doctor→ must exit 0.bun run electron:run:local— window, tray, menu, deep link.bunx burdenoff-desktop smoke(add--no-trayon headless Linux).- Release dry run:
Release Electronwithdry_run=true.
Developing this package
bun install
bun run sanity # lint + format + type:check + test + build
bun run build # dist/{main,preload,builder,config,cli}.cjs + d.ts
bun run scripts/verify-bundle.tstest/fixture-shell/ is a minimal shell used by .github/workflows/smoke.yml
to exercise doctor, icons, stage and smoke end to end without depending
on a product repo.
License: proprietary — Burdenoff Consultancy Services Pvt. Ltd.
