diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index c168a479844..7032e635d93 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -210,6 +210,8 @@ module.exports = { // it is gitignored, but exclude it defensively so a stray local capture at // package time never bloats app.asar. '!pr-evidence{,/**/*}', + // Local build logs and rollback copies are never application resources. + '!notes{,/**/*}', // Why: local agent/tooling directories may contain worktree symlink loops; // they are never runtime inputs and must not be traversed by electron-builder. '!{.claude,.grok,.agents,.codex}{,/**/*}', diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index 430a5fae4c8..72ffc80ec2c 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -1,8 +1,8 @@ import { existsSync } from 'node:fs' -import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { chmod, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { writeMobileWebBundleFixtureTree } from './mobile-web-bundle-fixture-tree.mjs' @@ -11,7 +11,7 @@ const SRC_MAIN_DIR = join(REPO_ROOT, 'src', 'main') const require = createRequire(import.meta.url) const electronBuilderConfig = require('../electron-builder.config.cjs') -const { FileMatcher } = require('app-builder-lib/out/fileMatcher') +const { copyFiles, FileMatcher } = require('app-builder-lib/out/fileMatcher') const FpmTarget = require('app-builder-lib/out/targets/FpmTarget').default const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs') @@ -37,6 +37,7 @@ describe('electron-builder config', () => { '!tests{,/**/*}', '!examples{,/**/*}', '!pr-evidence{,/**/*}', + '!notes{,/**/*}', '!{.claude,.grok,.agents,.codex}{,/**/*}', '!Casks{,/**/*}', '!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md,ORCHESTRATION_IMPLEMENTATION_CHECKLIST.md,ORCHESTRATION_STRUCTURED_OUTPUT_DESIGN.md}', @@ -63,6 +64,62 @@ describe('electron-builder config', () => { expect(packs('out/main/index.js')).toBe(true) }) + it.each(['file', 'directory'])('keeps a root notes %s out of app.asar', async (kind) => { + const root = await mkdtemp(join(tmpdir(), 'orca-packaging-notes-')) + const source = join(root, 'app') + const destination = join(root, 'selected') + const runtimePaths = [ + 'package.json', + 'out/main/index.js', + 'out/renderer/index.html', + 'out/cli/index.js', + 'out/shared/index.js', + 'out/main/notes/index.js', + 'out/renderer/assets/notes/help.md', + 'resources/notes/help.md', + 'notes.txt' + ] + const notesPaths = + kind === 'file' + ? ['notes'] + : [ + 'notes/build.log', + 'notes/installed-orca-backup/Orca.exe', + 'notes/installed-orca-backup/resources/app.asar', + 'notes/orca-windows-setup.exe', + 'notes/.recovery/state.json' + ] + try { + for (const fixturePath of [...runtimePaths, ...notesPaths]) { + const file = join(source, fixturePath) + await mkdir(dirname(file), { recursive: true }) + await writeFile(file, 'synthetic fixture\n') + } + if (kind === 'directory') { + await mkdir(join(source, 'notes', 'empty')) + } + const matcher = new FileMatcher( + source, + destination, + (value) => value, + electronBuilderConfig.files + ) + // copyFiles adds the default include and prunes excluded directories during traversal. + await copyFiles([matcher]) + for (const runtimePath of runtimePaths) { + expect(await readFile(join(destination, runtimePath), 'utf8')).toBe('synthetic fixture\n') + } + const isPacked = matcher.createFilter() + for (const notesPath of new Set(['notes', ...notesPaths])) { + const file = join(source, notesPath) + expect(isPacked(file, await lstat(file)), notesPath).toBe(false) + } + expect(existsSync(join(destination, 'notes'))).toBe(false) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + // Why: `files` is an all-negation list, so electron-builder's default `**/*` packs // anything without an explicit `!` entry — examples/ landed without one and shipped // hostile-panel, the adversarial containment fixture, into 1.4.160-rc.3's app.asar.