Developer-focused setup, build, and debugging instructions for Skiller.
- Bun 1.3+ (used for install + script running; Electron itself runs on Node 20 bundled inside Electron)
- macOS 14+ for local macOS release builds (signing, notarization, DMG repacking)
- Windows 10+ for local Windows release builds (or use the GitHub Actions matrix)
create-dmg(brew install create-dmg) for macOS DMG repackaging
bun install
# Rebuild better-sqlite3 against Electron's ABI (only needed if native modules change):
bunx electron-builder install-app-deps# Full dev: main + preload watch, renderer on Vite HMR port 5180
bun run dev
# Same, but auto-open renderer DevTools on launch
bun run dev:debug
# Build the static bundle into out/ (no packaging)
bun run build
# Preview the built bundle in a packaged-like Electron process
bun run previewbun run typecheck
# = tsc --noEmit && tsc -p tsconfig.node.json --noEmitRun this before opening a pull request when touching TypeScript, preload, or main-process code.
The two configs own disjoint parts of the tree:
tsconfig.json→src/mainview/**(renderer) + shared typestsconfig.node.json→src/electron-main/**+src/preload/**+src/main/**+ shared types
They share src/shared/**; both type-check with noEmit: true so overlapping includes are fine.
electron-builder packages + signs + notarizes. Native modules (better-sqlite3) are rebuilt against Electron's ABI on the target OS, so cross-compilation is not supported — run the appropriate script on the appropriate OS, or use the GitHub Actions matrix.
| Host OS | Command | Output in artifacts/ |
|---|---|---|
| macOS (Apple Silicon) | bun run dist:mac |
Skiller-<version>-macos-arm64.dmg (signed + notarized, drag-to-Applications layout) |
| Windows x64 | bun run dist:win |
Skiller-<version>-win-x64.exe (NSIS installer) |
| Linux x64 | bun run dist:linux |
Skiller-<version>-linux-x86_64.{AppImage,deb} |
There are two ways to produce all three without owning three machines.
.github/workflows/release.yml fans out to macos-14, windows-latest, and ubuntu-latest runners. Tag a release and GitHub builds and publishes everything:
git tag v0.2.0
git push origin v0.2.0Required repo secrets (Settings → Secrets and variables → Actions):
macOS signing + notarization:
MACOS_CERT_P12— base64-encoded.p12export ofDeveloper ID Application: … (TEAM_ID)MACOS_CERT_P12_PASSWORD— password used to export the.p12MACOS_KEYCHAIN_PASSWORD— arbitrary temp-keychain password (generate any string)CSC_NAME— full identity string (e.g.Developer ID Application: Your Name (XXXXXXXXXX))APPLE_API_ISSUER— App Store Connect → Users and Access → Integrations → App Store Connect API → Issuer IDAPPLE_API_KEY_ID— 10-char Key IDAPPLE_API_KEY_P8— full contents of theAuthKey_XXXXXXXXXX.p8file (multi-line secret)
Windows/Linux jobs don't need secrets — Windows ships as an unsigned NSIS installer (SmartScreen shows a one-time warning on first install).
.github/workflows/pages.yml publishes the static files in docs/ to GitHub Pages on pushes to main that touch docs/**. First-time setup: Settings → Pages → Build and deployment → Source: GitHub Actions.
Inside a Windows 10/11 VM:
# Prereqs: Git, Bun, Visual Studio Build Tools (Desktop development with C++), Python 3
git clone https://github.com/beautyfree/skiller
cd skiller
bun install
bunx electron-builder install-app-deps
bun run dist:winElectron-builder reads these from .env or the shell when you run bun run dist:mac. Without them, the build succeeds unsigned — useful for quick local testing, not shippable.
| Variable | Purpose |
|---|---|
CSC_NAME |
Full Developer ID identity string |
APPLE_API_KEY_ID |
10-char Key ID from App Store Connect |
APPLE_API_ISSUER |
Issuer UUID |
APPLE_API_KEY |
Absolute path to the .p8 file (preferred for CI) |
| — or Apple ID fallback — | |
APPLE_ID |
Developer Apple ID email |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password |
APPLE_TEAM_ID |
10-char team ID |
electron-vite build— emits main + preload + renderer intoout/.electron-builder --mac— creates a signed.appinartifacts/mac-arm64/, then notarizes + staples it using the API-key env vars (handled natively by electron-builder 26+; no custom afterSign hook needed).scripts/repack-dmg.sh— wraps the signed app in a styled drag-to-Applications DMG, re-signs the DMG wrapper, notarizes + staples the DMG.
spctl -a -vvv -t install artifacts/Skiller-*.dmg # expect: "source=Notarized Developer ID"
xcrun stapler validate artifacts/Skiller-*.dmg # expect: "The validate action worked!"Electron-builder's built-in DMG writer produces an unstyled window. scripts/repack-dmg.sh uses create-dmg to get a 128px app icon, a dark background featuring two light "cards" under the app and Applications icons, a bright arrow, and the volume named "Skiller Installer" — the same layout Chrome/Slack use to visually force drag-to-Applications. We produce the styled DMG ourselves because the built-in is cosmetically insufficient (tiny default icon, no background).
Relevant config:
electron-builder.yml→mac.target: dirdisables electron-builder's own DMGdmg.writeUpdateInfo: falsesuppresses the blockmap (we generate that separately from the final DMG)
Apple returns a JSON log listing unsigned binaries or missing secure timestamps when notarization fails. Common causes:
- A native addon (e.g.
better-sqlite3.node) wasn't codesigned with--timestamp.electron-builderhandles this automatically; if you add new native modules,bunx electron-builder install-app-depson the target OS before dist. - Missing one of the three API-key vars. All three (
APPLE_API_ISSUER,APPLE_API_KEY_ID,APPLE_API_KEY) must be present, and the.p8file must exist at the path inAPPLE_API_KEY.
- Auto-open:
bun run dev:debug(AGENTSKILLS_DEVTOOLS=1 electron-vite dev). - Right-click → Inspect Element works in dev.
- On macOS Cmd+Alt+I and on Windows/Linux F12 toggle DevTools (see
@electron-toolkit/utils.optimizer.watchWindowShortcuts).
If UI renders in browser but not in Electron:
- Open
http://127.0.0.1:5180in Chrome — if the page renders, the main process crashed and the preload couldn't attach. Check main logs. - If renderer works but tRPC calls hang, the main process didn't bind a port — check the
tRPC: http://127.0.0.1:NNNN/trpclog line.
electron-updater is wired in src/main/app-updater.ts:
- Config —
electron-builder.ymlsetspublish.provider: githubpointing atbeautyfree/skiller. When youbun run dist:mac/win/linux, electron-builder writeslatest-mac.yml/latest-linux.yml/latest.yml(for Windows) alongside the DMG/AppImage/EXE. GitHub Actions uploads all of them to the tagged Release. - Runtime —
initAppUpdater()runsautoUpdater.checkForUpdates()on launch and every 6 hours.autoDownload = false, so the user clicks Download update in Settings → App Updates after an update is announced. Progress + ready state stream to the renderer through theapp_update_status_changedpush channel; the Settings page renders the current state and offers Restart & install once the update is downloaded. - Delta patches — electron-builder generates
.blockmapfiles automatically whenwriteUpdateInfo: falseis not set. We currently ship full DMGs — blockmap-based deltas can be added later when update volume justifies the extra artifacts. - Dev —
app.isPackaged === falsein dev, so the updater short-circuits and stays in theidlestate. No spam.
# Build version N with the real publish URL (GitHub Releases)
bun run dist:mac
open artifacts/Skiller-*.dmg # install into /Applications, launch
# Bump package.json version and tag + push — CI builds version N+1
git tag v0.2.1
git push origin v0.2.1The installed app should detect the new release within 6h (or via Check for updates in Settings → App Updates) and offer Restart & install after download completes.
Primary project configuration:
electron.vite.config.ts— bundler config (main + preload + renderer all via Vite 7)electron-builder.yml— packaging, signing, publish configbuild-resources/entitlements.mac.plist— Hardened Runtime entitlementspackage.json— scripts +main: out/main/index.js