@burdenoff/desktop-shell

中文文档
2026.1107.1发布于 2026/11/07UNLICENSEDTypeScriptCommonJS近 30 天下载 8178

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).

原始使用文档
文档目录18 节
  1. The two bugs this fixes
  2. Install into a shell
  3. Renderer surface
  4. Desktop focus mode ( focusMode: true )
  5. Desktop idle detection ( idleDetection: true )
  6. Content Security Policy
  7. CLI reference
  8. stage
  9. launch
  10. smoke
  11. icons
  12. doctor
  13. Config contract
  14. Environment
  15. electron-builder config
  16. Release flow
  17. Migration checklist
  18. Developing this package

@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.

Renderer surface

The renderer is treated as untrusted: it runs shared MFE bundles and, in production, whatever web bundle CapGo OTA made active. What it can reach:

GlobalContents
window.__burdenoffDesktopthe desktop bridge (BurdenoffDesktopBridge in src/shared/types.ts)
window.<product>Electronlegacy subset; legacyGlobal: false removes it
window.electronUpdaternotifyAppReady() only

Every ipcMain entry point checks that the calling frame is on the app origin (or the dev server) before acting. OTA bundle selection — download, activation, reset, channel and endpoint changes — happens only in the main process; the CapGo package's own renderer API and IPC handlers are not installed. The renderer's single OTA input is the app-ready signal, which both __burdenoffDesktop.notifyAppReady() and electronUpdater.notifyAppReady() send over IPC.appReady, and which the main process forwards to CapGo so the running bundle is kept rather than rolled back.

Desktop focus mode (focusMode: true)

Products with focus sessions (TimeCampus) can turn on window.__burdenoffDesktop.focus. The renderer, which holds the user's session, calls focus.setState({ active, sessionId, endsAt, strict, blockedApps, doNotDisturb, trackAppSwitches }) every few seconds while a session runs; the main process then:

  • turns the OS do-not-disturb mode on and restores it afterwards, only if it was the one that turned it on. macOS has no public Focus API, so this runs two user-created Shortcuts (" Focus On" / " Focus Off"); GNOME uses gsettings (show-banners); Windows has no supported API, so the status is unsupported with instructions;
  • samples the foreground app (macOS osascript + System Events, which needs the Automation permission; Windows PowerShell + user32; Linux X11 xdotool, Wayland unsupported). A switch into a blocked app is hidden on strict sessions or warned about on standard ones and reported through focus.onBlockedApp; with trackAppSwitches every finished foreground stretch is reported through focus.onAppSegment;
  • switches everything off again if the renderer stops refreshing for five minutes, so a crashed or logged-out renderer never leaves apps hidden or notifications muted.

focus.getStatus() / focus.onStatus() report what actually works on this machine (on, needs-setup, unsupported, error) with a human-readable detail. All helpers run through execFile with argument arrays and a timeout; no renderer string reaches a shell or a script body.

Desktop idle detection (idleDetection: true)

Products with automatic capture (TimeCampus) can turn on window.__burdenoffDesktop.idle. The renderer calls idle.configure({ enabled, thresholdSeconds }) with the user's settings (the threshold is clamped to 60..3600 seconds); the main process then samples powerMonitor.getSystemIdleTime() (every quarter of the threshold, at most every 15 seconds) and listens for screen lock, unlock, suspend and resume.

Every finished stretch the person was away, from their last keyboard or mouse input to their first input after it (or to unlock / resume), is reported through idle.onIdlePeriod({ startedAt, endedAt, reason }) with reason inactive, locked or suspended. Stretches shorter than the threshold are never reported, and turning detection off drops an open stretch. idle.getStatus() / idle.onStatus() report whether the person is away right now and whether this OS can report input inactivity at all (supported). The monitor only reads the seconds since the last input; it never sees which keys were pressed or where the pointer went.

Content Security Policy

Every HTML document the <scheme>://app protocol serves (the builtin bundle, an OTA bundle, and the SPA fallback) carries two policies, built by src/main/csp.ts from the runtime config:

HeaderPolicy
Content-Security-Policyobject-src 'none'; base-uri 'none'; frame-ancestors 'self'
Content-Security-Policy-Report-Onlythe full target policy: 'self', the gateway origins (https + wss), the SSO origin, and the third parties the shared MFEs load (Stripe.js, Razorpay Checkout; package-CDN stylesheets and fonts). Images and media may come from any https host

Report-only violations do not block anything; the main process logs each distinct one ([burdenoff-desktop:<product>:csp]), so packaged builds without DevTools still show them. Package CDNs are deliberately absent from the target script-src: scripts loaded from them are reported and should be bundled with the app before script-src moves into the enforced policy. frame-ancestors is 'self', not 'none', so the app can still frame its own documents; the smoke command checks both the headers and a same-origin iframe.

CLI reference

burdenoff-desktop <command> [options]
  -h, --help       this help (or `<command> --help`)
  -v, --version    package version

stage

Builds electron/build/app — the folder electron-builder packages. Replaces scripts/prepare-electron.mjs.

OptionDefaultMeaning
--root <dir>cwdShell root
--env <name>$BURDENOFF_DESKTOP_ENVlocal | alpha | prod (staging/production accepted)
--web-dist <dir>distWeb build to copy in
--deps-mode <mode>bundlebundle 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)
--launchoffRun 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_PREFIX is the main → preload channel (URI-encoded JSON via additionalArguments, with cfAccess stripped). Main reads the staged runtime-config.json itself, and BURDENOFF_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:

CheckAssertion
windowa first window opens
titleequals productName
bridgewindow.__burdenoffDesktop.product.id equals product
diagnosticsglobalThis.__burdenoffDesktopDiagnostics is published
protocolprotocolRegistered
menumenu
traytray (skipped with --no-tray — headless CI has no tray host)
deep-linkapp.emit('open-url', …, '<scheme>://app/smoke') reaches the renderer
updater-surfacewindow.electronUpdater exposes notifyAppReady and nothing else
cspthe app document carries the enforced baseline and a report-only policy (skipped off the app protocol)
csp-framea same-origin iframe of an app document still loads
updater / capgo / webRootreported, 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,
    "focusMode": false,                     // true exposes __burdenoffDesktop.focus (see below)
    "idleDetection": false                  // true exposes __burdenoffDesktop.idle (see below)
  }
}

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:

VariableEffect
ENVlocal | alpha | prod
VERSIONCalVer baked into the staged package.json
START_URLdev-server origin the window loads
UPDATE_FEED_URLoverrides the derived electron-updater feed
GLOBAL_BASE_URL / WORKSPACE_BASE_URL / PUBLIC_STORE_BASE_URLgateway overrides
CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRETalpha only — baked in so electron-updater can reach the Access-gated alpha downloads host
DEVTOOLSopen 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.buildResources is electron/build/icons, not build/ — several shells generate build/i18n-bundle.json there;
  • Linux .desktop gets MimeType=x-scheme-handler/<scheme>; — without it deep links silently do nothing on Linux;
  • npmRebuild: false replaces the old beforeBuild: () => false hack;
  • macOS: hardened runtime, gatekeeperAssess: false, entitlements, and notarize: false — notarization is an explicit xcrun notarytool CI step so a build never blocks on Apple credentials;
  • no publish key: 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 validate-inputs job every other job depends on (CalVer version, known choice values, no secrets), a capgo-ota job (production only, in electron-production, CLI from the integrity-locked .github/tools/capgo-cli), linux / macos (macos-15) / windows build jobs, and a publish job uploading downloads/<ver>/<os>/, mirroring downloads/latest/<os>/ with stable un-versioned aliases, publishing the auto-updater feeds (latest-mac.yml, latest.yml, latest-linux.yml plus the installers they name) flat at downloads/latest/ where electron-updater's generic provider looks for them, writing manifest.json (built with jq) and invalidating CloudFront. macOS signs with CSC_LINK and notarizes/staples via notarytool; Windows signs with azure/artifact-signing-action (v2.0.0) 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. Installs are --frozen-lockfile, third-party actions are SHA-pinned, and dispatch inputs reach shell steps only through env: — see templates/README.md.

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

  1. Add the desktop slice to native.config.json.
  2. Stamp electron/main.cjs, electron/preload.cjs, electron/desktop.config.cjs, electron-builder.config.cjs.
  3. Apply templates/package-scripts.json (merge scripts/deps, apply remove).
  4. Delete scripts/prepare-electron.mjs, scripts/electron-launch.mjs, scripts/electron-before-build.cjs, src/electronRuntime.ts, and the "build" key.
  5. Ensure a square PNG ≥512px exists (public/icons/icon-512x512.png).
  6. Stamp both workflows.
  7. bun install && bunx burdenoff-desktop doctor → must exit 0.
  8. bun run electron:run:local — window, tray, menu, deep link.
  9. bunx burdenoff-desktop smoke (add --no-tray on headless Linux).
  10. Release dry run: Release Electron with dry_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.ts

test/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.

版本与发布记录

@burdenoff/desktop-shell 共发布 22 个版本,首次发布于 2026/08/23;过去 12 个月发布了 22 个版本。

版本发布日期距今
2026.1107.1latest2026/11/071 天前
2026.1102.12026/11/035 天前
2026.1031.12026/11/018 天前
2026.1028.32026/10/2811 天前
2026.1028.22026/10/2811 天前
2026.1028.12026/10/2811 天前

关于 @burdenoff/desktop-shell 的常见问题

@burdenoff/desktop-shell 是什么?

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).

如何安装 @burdenoff/desktop-shell?

在项目目录运行 npm install @burdenoff/desktop-shell,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:electron(>=39 <45)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 burdenoff-desktop,可以用 npx burdenoff-desktop 直接运行。

@burdenoff/desktop-shell 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@burdenoff/desktop-shell 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@burdenoff/desktop-shell 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@burdenoff/desktop-shell 可以商用吗?

@burdenoff/desktop-shell 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@burdenoff/desktop-shell 的下载量有多少?

根据 npm 官方下载统计,@burdenoff/desktop-shell 在 2026/10/09 – 2026/11/07 这 30 天内被下载 8,178 次,日均约 273 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@burdenoff/desktop-shell 还在维护吗?

最新版本 2026.1107.1 发布于 2026/11/07(1 天前);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/08/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。