codeburn/app/DISTRIBUTION.md

352 lines
17 KiB
Markdown

# 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-<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:
```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<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
```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: <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.