# electron-builder config — LearnHarness desktop (macOS arm64; Windows/Linux x64). # # Shape (fact-checked 2026-09-17): # - the app's real server (.next/standalone, curated by # scripts/build-server.mjs) ships via extraResources — OUTSIDE the asar, # because native .node/dylib can't dlopen from a read-only archive and # server.js process.chdir()s into its own dir; # - the asar holds only the shell (electron/main.cjs + package.json); # - signed + notarized since 2026-10-08 (Developer ID Application cert in # the login keychain; notary credentials in the LH_NOTARY keychain # profile; the profile + team env vars are baked into desktop:dist). appId: com.learnharness.LearnHarness productName: LearnHarness directories: output: dist # Icons (owner ruling, 2026-10-13) — per-platform sources in build/: # mac build/icon.icon — the Icon Composer document (layered Liquid # Glass); actool (Xcode 26+) compiles Assets.car (CFBundleIconName, # macOS 26+ renders the true layered icon) + an actool fallback icns # capped at 256px, which scripts/after-pack.cjs swaps for the # hand-packed full-res build/icon.icns (pre-Tahoe keeps 16→1024). # Requires actool ≥ 26: desktop:dist injects DEVELOPER_DIR locally; # the release workflow's mac job runs setup-xcode. # win build/icon.ico — generated (16→256) from the flat design by # scripts/make-icons.mjs. # linux build/icon.png — 512×512, same script. Rounded squircle with # baked transparency on both (owner ruling) — Windows/Linux don't # mask, the shape ships in the artwork. files: # Only the shell goes into the asar. ORDER IS LOAD-BEARING (payload # wave, 2026-09-24): electron-builder's dependency collector packs the # whole production dep tree into node_modules/ REGARDLESS of this # allow-list — normalizeFiles turns this array into one {filter: [...]} # fileSet, getNodeModuleFileMatcher feeds EVERY filter entry (positives # included) to the per-dep copier, and without a negation its implicit # '**/*' passes everything (the measured result: a 502MB asar holding # next/@ai-sdk/pdfjs/mermaid/puppeteer… while the launcher requires # nothing but electron + node builtins — the server ships via # extraResources). # # The fix is gitignore-style ordering (verified against the installed # app-builder-lib 26.15.3 + a DEBUG-mode rebuild): '!**/*' first excludes # EVERYTHING, the two positives after it re-include only the shell. # The per-dep collector then contributes ZERO files — app-builder-lib's # filter evaluates each dep file's DESTINATION path (the # stat.moduleFullFilePath quirk in util/filter.js getRelativePath), so # '!**/*' matches every 'node_modules//...' path and the bare # positives can never re-include one (DEBUG logs confirm filesCount=0 # for all 566 modules). If a future electron-builder fixes that quirk, # ~1MB of package.json stubs would start landing — harmless. The # two-package.json layout avoids the collector entirely but # restructures the repo. app.asar.unpacked dies with it: the natives # were only unpacked because their packages were packed. - "!**/*" - electron/**/* - package.json # Auto-update (Wave C, 2026-10-10): electron-updater + its runtime tree # re-included explicitly. The negation above excludes node_modules, and # the shell's bare positives can't re-include them — these explicit # node_modules positives do (gitignore order: the LAST matching pattern # wins; verified post-build by listing the asar — see desktop:dist notes). # Tree fact-checked via each package's declared dependencies: # electron-updater → fs-extra, js-yaml, lazy-val, lodash.escaperegexp, # lodash.isequal, semver, tiny-typed-emitter, builder-util-runtime; # builder-util-runtime → debug, sax; fs-extra → graceful-fs, jsonfile, # universalify; debug → ms; jsonfile → universalify (again); js-yaml → # argparse. Keep in sync when electron-updater bumps (npm ls --all). - node_modules/electron-updater/**/* - node_modules/fs-extra/**/* - node_modules/js-yaml/**/* - node_modules/lazy-val/**/* - node_modules/lodash.escaperegexp/**/* - node_modules/lodash.isequal/**/* - node_modules/semver/**/* - node_modules/tiny-typed-emitter/**/* - node_modules/builder-util-runtime/**/* - node_modules/debug/**/* - node_modules/sax/**/* - node_modules/graceful-fs/**/* - node_modules/jsonfile/**/* - node_modules/universalify/**/* - node_modules/ms/**/* - node_modules/argparse/**/* extraResources: # Copy from the PARENT dir (.next), not .next/standalone itself: # electron-builder hard-excludes a copy's root node_modules and the # default no-filter pattern is a literal path match (empirically both # node_modules/ and .next/ were silently dropped → server died on # "Cannot find module 'next'"). With `from: .next` the filter's # relative paths start with standalone/, dodging the root-node_modules # rule, and the explicit wildcard (minimatch dot:true) reaches # standalone/.next and standalone/node_modules. - from: .next to: server filter: - "standalone/**/*" mac: icon: build/icon.icon # zip rides next to dmg (flipped 2026-10-08): Squirrel.Mac — Electron's # macOS auto-updater — installs from ZIP and cannot read DMGs. DMG is # for humans; ZIP + blockmap are the updater's payload. target: - target: dmg arch: - arm64 - target: zip arch: - arm64 # Locale trim (payload wave, 2026-09-24): English-only app — cuts # every non-en .lproj from the frameworks (the STT/mic entitlements # don't depend on locales). Revisit only if localization ever lands. electronLanguages: - en category: public.app-category.education # Laid for the STT milestone (researcher-verified): the TCC mic prompt # reads this string; macOS asks regardless of Electron's own handlers. extendInfo: NSMicrophoneUsageDescription: "LearnHarness uses the microphone only for speech-to-text input into your sessions." hardenedRuntime: true entitlements: build/entitlements.mac.plist # Signing + notarization (flipped 2026-10-08): identity auto-detects the # Developer ID Application certificate from the login keychain; # notarize: true makes electron-builder sign → submit to Apple's notary # service → staple the ticket, all inside this one build (credentials # via the LH_NOTARY keychain profile, env baked into desktop:dist). # Adds minutes per build (Apple's scan queue). Verification battery: # codesign --verify --deep / spctl --assess / stapler validate. notarize: true dmg: title: "${productName} ${version}" afterPack: scripts/after-pack.cjs win: icon: build/icon.ico target: - target: nsis arch: - x64 artifactName: "${productName}-${version}-${arch}-setup.${ext}" # Unsigned Windows release (owner ruling): preserve icon/version metadata, # skip signing, and do not require a publisher signature for updates. signExecutable: false verifyUpdateCodeSignature: false nsis: oneClick: true perMachine: false linux: icon: build/icon.png target: - target: AppImage arch: - x64 - target: deb arch: - x64 artifactName: "${productName}-${version}-${arch}.${ext}" category: Education # Match Electron's app_id/WM_CLASS to the installed desktop launcher. syncDesktopName: true # Both targets update via electron-updater 6.8.9. AppImage replaces its # own file; deb uses dpkg/apt and may request administrator permission. publish: # Update feed = GitHub Releases (decided 2026-10-08): latest-mac.yml + # zip/blockmap live on releases; electron-updater reads them anonymously. # REQUIRES THE REPO TO BE PUBLIC before 0.3.0 ships — anonymous downloads # from a private repo are impossible, and a token baked into the app # would be a leak. desktop:dist passes no --publish flag, so builds never # upload; publishing happens at release time (GH_TOKEN + --publish). - provider: github owner: amosblomqvist repo: learnharness # All three CI jobs contribute assets; only the owner publishes it. releaseType: draft