codeburn/app/DISTRIBUTION.md

17 KiB

Distributing CodeBurn Desktop

This document describes how to produce distributable macOS, Windows, and Linux builds of the Electron desktop app. The macOS build is ad-hoc-signed and not notarized (no paid Apple Developer account); the Windows and Linux builds are unsigned. There is no CI automation for any of this yet (unlike the CLI and menubar release processes in ../RELEASING.md) — packaging is run by hand on a maintainer's machine. All three targets are produced by electron-builder and can be cross-built from a single macOS host.

The bundled CLI (no install prerequisite)

The packaged app ships its own copy of the codeburn CLI and needs nothing installed on the target machine. Packaging stages a self-contained copy of the root repo's CLI into app/build/cli (see scripts/stage-cli.mjs) — the tsup bundle (dist/main.js), its Node-version launcher (dist/cli.js), the root package.json (the bundle reads its version), plus the CLI's production node_modules closure. An afterPack hook (scripts/after-pack.cjs) copies that tree into the app at Contents/Resources/cli/ after packaging and before signing, so it lands inside the code signature.

At runtime a packaged build spawns the bundled CLI with Electron's own binary acting as Node (ELECTRON_RUN_AS_NODE=1), so no Node install is required — the app is version-matched to itself. main.ts sets CODEBURN_BUNDLED_CLI to Resources/cli/dist/launch.js (a small shim that corrects argv for commander under Electron, then hands off to main.js), and electron/cli.ts resolves it ahead of any persisted path or PATH lookup.

A user-installed CLI is only consulted outside a packaged build (dev via the Vite dev server) or when explicitly overridden — CODEBURN_BIN=/abs/path, or a persisted path file (Application Support/CodeBurn/codeburn-cli-path.v1). The full resolution order in electron/cli.ts is: CODEBURN_BIN → dev repo CLI (Vite) → bundled CLI → persisted path → PATH/Homebrew/nvm/volta/asdf.

Freshness

Packaging always rebuilds the root CLI before staging: the app stage-cli script runs the root build:cli (tsup only — no dashboard build, no network bundle-litellm fetch), so dist/main.js and dist/cli.js are regenerated from current src/ on every package* run. A stale global codeburn can never ship, and the app can never be older than the JSON surfaces it calls. The bundled CLI's deps are pure JS (no native bindings), so the same staged tree is valid for every arch.

Why afterPack, not extraResources

electron-builder routes every node_modules directory it copies through its production-dependency filter, which keeps only the app's own deps — so the bundled CLI's dependency tree gets stripped out of an extraResources copy (filter: ["**/*"] does not defeat this). afterPack copies the staged tree in verbatim, which is the electron-builder-recommended mechanism for adding unpacked files that must not be ASAR-archived.

Versioning

app/package.json's version tracks the CLI's version (root package.json) — one CodeBurn version across CLI, menubar, and desktop. Bump it in the same change that bumps the root version; the splash, the About dialog, and the artifact filenames all read it from there.

Build

npm --prefix app install
npm --prefix app run package          # macOS, both arm64 and x64
npm --prefix app run package:arm64    # macOS arm64 only (faster on Apple Silicon)
npm --prefix app run package:x64      # macOS x64 only
npm --prefix app run package:win      # Windows NSIS installer, x64
npm --prefix app run package:store    # Microsoft Store AppX, x64 (Windows host only)
npm --prefix app run package:linux    # Linux AppImage, x64

The Linux targets and arches can be widened by calling electron-builder directly, all from the same macOS host:

cd app
npx electron-builder --linux AppImage deb        # x64 AppImage + deb
npx electron-builder --linux AppImage deb --x64  # force x64 on an arm64 mac
npx electron-builder --linux AppImage deb --arm64
npx electron-builder --linux rpm --x64           # rpm; needs `brew install rpm` (rpmbuild)

The rpm target is the only one with an extra prerequisite — fpm (bundled by electron-builder) shells out to rpmbuild, so brew install rpm must be present or the build fails with "executable rpmbuild is required". It emits codeburn-desktop-<version>.x86_64.rpm, the name the website's Fedora/RHEL download links.

package runs npm run stage-cli (rebuilds the root CLI and stages the self-contained bundle into app/build/cli; see "The bundled CLI" above), then npm run build (compiles electron/ with tsc, builds the renderer with vite), then electron-builder --mac (whose afterPack hook copies the staged CLI into the app). package:win and package:linux mirror it exactly, swapping the final flag for electron-builder --win and electron-builder --linux. All three can run on the same macOS host — electron-builder downloads the NSIS and AppImage tooling on first use.

Artifacts

electron-builder writes to app/release/ (gitignored, like dist/):

  • CodeBurn-<version>-arm64.dmg, CodeBurn-<version>.dmg — installer images
  • CodeBurn-<version>-arm64-mac.zip, CodeBurn-<version>-mac.zip — zipped .app bundles
  • release/mac-arm64/CodeBurn.app, release/mac/CodeBurn.app — the raw unpacked bundles (arm64 and x64 respectively)
  • .blockmap files alongside each zip/dmg (used by electron-builder's differential-update mechanism; unused since this app has no auto-updater yet)

Both dmg and zip targets are built for both arm64 and x64 — four artifacts total, not a universal binary. This keeps each download roughly half the size of a universal build. Pick the zip if you just want to unpack and drag to /Applications; the dmg gives users the familiar drag-to-Applications installer window.

Build configuration

The build block lives in app/package.json (small enough not to warrant a separate electron-builder.yml):

  • appId: "org.agentseal.codeburn-desktop" — reuses the org.agentseal.* prefix from the menubar app's bundle id (org.agentseal.codeburn-menubar, see mac/Scripts/package-app.sh); there is no com.codeburn.* bundle id anywhere in the codebase, so org.agentseal.* is the actual house convention.
  • productName: "CodeBurn".
  • files: only dist/electron/**/*, dist/renderer/**/*, and package.json. The Electron main process has no npm runtime dependencies (only Node/Electron builtins — see app/electron/cli.ts and app/electron/quota/*.ts), and the renderer is a single Vite bundle, so the app's own node_modules does not need to ship at all. (The bundled CLI has its own node_modules, added to Resources/cli/ by the afterPack hook — see "The bundled CLI" above.)
  • afterPack: "./scripts/after-pack.cjs" — copies the staged CLI bundle (app/build/cli) into Contents/Resources/cli after packaging and before signing.
  • mac.identity: "-" — forces ad-hoc signing. identity: null does NOT ad-hoc sign — it skips signing entirely, which produces a bundle with a broken/absent seal (codesign --verify --deep --strict fails with code has no resources but signature indicates they must be present, and Apple Silicon refuses to run it at all). "-" is the same ad-hoc identity mac/Scripts/package-app.sh uses for the menubar app's local/CI builds.
  • mac.hardenedRuntime: false — hardened runtime is for notarized builds; leaving it on for an ad-hoc signature with no entitlements can prevent the app from launching.
  • mac.gatekeeperAssess: false — skips electron-builder's post-sign spctl check, which would always fail for an unnotarized app.
  • icon: build/icon.png — a pre-existing 1024x1024 PNG at app/build/icon.png. No .icns exists in the repo; electron-builder generates one from the PNG at build time. This is the same source PNG used for the app icon; the menubar app has its own separate icon (assets/menubar-logo.png, converted to .icns in package-app.sh).
  • directories.output: "release" — electron-builder's default output dir is dist, which collides with this app's existing tsc/vite build output (app/dist/electron, app/dist/renderer) that files reads from. Using a separate release/ directory keeps build inputs and packaging outputs apart.

Windows and Linux builds

Both are cross-built from the same macOS host used for the mac build — no Windows or Linux machine, and no wine, is required. electron-builder 26 embeds the Windows executable's icon/version resources natively and downloads the NSIS and AppImage tooling on first run.

Windows (package:win)

electron-builder --win produces a single artifact in app/release/:

  • CodeBurn Setup 0.9.15.exe — the NSIS installer (the version number tracks package.json; note the spaces in the filename). A .exe.blockmap is written alongside it (differential-update metadata, unused — no auto-updater yet).

Config (build.win + build.nsis):

  • win.target: nsis, arch: x64.
  • win.icon: build/icon.png — electron-builder converts the 1024x1024 PNG to a multi-resolution .ico at build time (same source PNG as the mac icon).
  • nsis.oneClick: false — an assisted installer with a wizard, so users get an install-directory choice instead of a silent one-click install.
  • nsis.perMachine: false — installs per-user (into the user's AppData), so no administrator/UAC elevation is needed.

The build is UNSIGNED (no Authenticode certificate). electron-builder logs signing with signtool.exe, but with no certificate configured that step is a no-op — the .exe ships without a signature. On first run, Windows SmartScreen shows "Windows protected your PC". Users click "More info" → "Run anyway" to launch it. This is expected for an unsigned build; the only fix is a purchased code-signing (Authenticode/EV) certificate.

Microsoft Store (package:store)

The Store build is a separate AppX target so the GitHub NSIS installer remains unchanged. AppX packaging requires Windows 10 or newer and is built by the manual Build Windows Store package GitHub Actions workflow on windows-latest. Download its CodeBurn-Microsoft-Store workflow artifact and upload the contained CodeBurn-Store-<version>-x64.appx file in Partner Center.

The manifest identity must exactly match the reserved Partner Center product:

  • Identity name: Codeburn.CodeBurn
  • Publisher: CN=3EFA3336-87E1-46F2-9DFA-2EB5A7693F89
  • Publisher display name: Codeburn
  • Store ID: 9P0R4ZL5XMB8

The Store package is intentionally unsigned: Microsoft signs it during Store submission. Direct sideloading requires a separate trusted or development certificate. The AppX declares runFullTrust (electron-builder's required default for Electron apps), so CodeBurn retains access to the user's local provider session files rather than running in a UWP application sandbox.

Linux (package:linux)

electron-builder --linux produces a single artifact in app/release/:

  • CodeBurn-0.9.15.AppImage — a self-contained AppImage (no install step, no package manager).

Config (build.linux):

  • linux.target: AppImage, arch: x64.
  • linux.category: "Development" — the freedesktop menu category.
  • linux.icon: build/icon.png — reuses the same source PNG.
  • linux.maintainer: "AgentSeal <hello@agentseal.org>" — matches the root package.json author.
  • linux.executableName: "codeburn" — the binary name inside the AppImage (lowercase, no spaces), distinct from the CodeBurn product name.

After downloading, the AppImage must be made executable before it will run:

chmod +x CodeBurn-0.9.15.AppImage
./CodeBurn-0.9.15.AppImage

The build logs one benign warning — desktopName is not set — which only affects how some desktop environments group the app's windows in the taskbar/dock; it does not affect packaging or launch.

Releases

There is no release CI for the desktop app yet (see the note at the top). When a maintainer cuts a desktop release by hand, the GitHub tag convention is:

desktop-v<version>      # e.g. desktop-v0.9.15

This mirrors the menubar's mac-v<version> convention (see ../RELEASING.md) and keeps the desktop app's tags in their own namespace, separate from the CLI (v<version>) and the menubar (mac-v<version>). Upload all of the artifacts above — the four macOS .dmg/.zip files, CodeBurn-Setup-<version>.exe, and CodeBurn-<version>.AppImage — to the GitHub Release created at that tag. The website's download links pin that tag in their URLs, so the release name and the artifact filenames must match exactly. (The Windows installer uses an explicit nsis.artifactName of CodeBurn-Setup-${version}.${ext} — electron-builder's default contains spaces, which make ugly percent-encoded URLs.)

Verifying a build

codesign -dv --verbose=2 app/release/mac-arm64/CodeBurn.app
codesign --verify --deep --strict app/release/mac-arm64/CodeBurn.app

Expect Signature=adhoc, a real Identifier=org.agentseal.codeburn-desktop, and Sealed Resources present. The deep-verify command should exit 0.

To smoke-test that the packaged renderer actually loads (the classic failure is a white screen from a wrong loadFile path once assets are behind app.asar), launch the built binary directly and confirm the process tree stays up and the main process logs no did-fail-load errors:

"app/release/mac-arm64/CodeBurn.app/Contents/MacOS/CodeBurn" --user-data-dir=/tmp/codeburn-smoke

A healthy launch spawns CodeBurn, CodeBurn Helper (gpu-process), CodeBurn Helper (utility/network), and CodeBurn Helper (Renderer) processes and keeps running with no stderr output. main.ts's did-fail-load handler (console.error('Renderer failed to load ...')) prints to that same stderr if the packaged loadFile(path.join(__dirname, '..', 'renderer', 'index.html')) path is ever wrong.

The Gatekeeper story (no paid Apple Developer account)

Ad-hoc signing satisfies the kernel's code-signing requirement (Apple Silicon refuses to execute anything with no signature at all), but it is not a Developer ID signature and the app is not notarized. Concretely:

  • spctl --assess --type execute on the built app returns rejected, ad-hoc-signed or not, quarantined or not. spctl's static assessment checks for a Developer ID + notarization ticket, which this build does not have and cannot have without a paid account.
  • Any file downloaded through a browser (or unzipped by Finder's Archive Utility from a browser download) gets a com.apple.quarantine extended attribute. The first time a quarantined, non-notarized app is opened, Gatekeeper blocks a plain double-click with "Apple could not verify that [CodeBurn] is free of malware."
  • This is expected and correct for an unpaid, unnotarized build. Being a known GitHub author, signing the repo's commits, or ad-hoc signing the binary does not change this — none of that is a substitute for an Apple-issued Developer ID certificate plus notarization.

First-open instructions for users

Field-verified on macOS 15+ (Sequoia/Tahoe): an ad-hoc-signed, quarantined app gets the harsher "[CodeBurn] is damaged and can't be opened. You should move it to the Trash." dialog, and the classic right-click → Open bypass does NOT work for it (that trick only helps Developer-ID-signed, unnotarized apps). The reliable path is stripping the quarantine attribute:

# after dragging CodeBurn.app from the dmg into /Applications
xattr -cr /Applications/CodeBurn.app

One time only; subsequent launches work normally. System Settings → Privacy & Security → "Open Anyway" may also appear after a blocked attempt and works when offered, but is not shown in all cases for ad-hoc builds — document the xattr path as primary anywhere user-facing.

None of these steps are needed for a dmg/zip built and opened locally on the same machine (no quarantine attribute is applied to files that were never downloaded) — they only apply to a build distributed to someone else, e.g. via a GitHub Release.

Folder-access prompts re-appear on every update

CodeBurn requests access to folders like Documents, Desktop, and Downloads (via mac.extendInfo in app/package.json) to read local AI coding tool session logs. Because each ad-hoc/unsigned build has no stable Developer ID, macOS TCC treats every rebuild as a new app identity, so users get re-prompted for folder access after each update even though nothing else changed. Signing with a stable Developer ID certificate (see "Upgrade path" below) fixes this — TCC grants persist across updates once the app's identity is stable.

Upgrade path: paid account + notarization

When a paid Apple Developer Program membership is available, the same electron-builder config takes the upgrade with a few changes, no new tooling:

  • Set mac.identity to the real "Developer ID Application: <Name> (<TEAMID>)" certificate name (or let electron-builder auto-discover it from the keychain by removing identity entirely), and set mac.hardenedRuntime: true with an entitlements file.
  • Add a notarize block (or the afterSign hook electron-builder's @electron/notarize integration expects) with an app-specific password or API key, and remove gatekeeperAssess: false so electron-builder verifies the notarized result itself.
  • Everything else — appId, files, mac.target (dmg/zip, arm64+x64), icon, directories.output — stays as-is.