# 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 ```sh 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: ```sh 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-.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--arm64.dmg`, `CodeBurn-.dmg` — installer images - `CodeBurn--arm64-mac.zip`, `CodeBurn--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--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 "` — 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: ```sh 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 # e.g. desktop-v0.9.15 ``` This mirrors the menubar's `mac-v` convention (see `../RELEASING.md`) and keeps the desktop app's tags in their own namespace, separate from the CLI (`v`) and the menubar (`mac-v`). Upload all of the artifacts above — the four macOS `.dmg`/`.zip` files, `CodeBurn-Setup-.exe`, and `CodeBurn-.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 ```sh 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: ```sh "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: ```sh # 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: ()"` 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.