From 4f7fdeca6bde016e04cb3ef1f227e65ca2dee8ea Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 17 Aug 2026 21:39:38 -0700 Subject: [PATCH] Build: ship launcher-only release zips by default IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the dist skill copies. Bundling every target into every provider copy put dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap that impeccable install downloads through. Prepared with AI assistance (Claude Code). --- CLAUDE.md | 2 +- scripts/build.js | 25 ++++++++++++++----------- tests/hook-build.test.mjs | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 00a629f6f..4dbb4dd2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ The skill has no runtime of its own. Every command the skill text runs is `{{scr The binary is built from a separate repo (`~/code/impeccable-engine`; do not edit it from here). Its verbs are the old script basenames (`context`, `doctor`, `pin`, `hook`, `hook-before-edit`, `live*`, `detect`, ...) with two aliases: `signals` for context-signals and `hooks` for hook-admin. Its observable behavior is specified in `docs/CLI-CONTRACT.md` and pinned by `tests/oracle/`. - **`ENGINE_VERSION`** (repo root) pins the engine release. The build copies it to `skill/scripts/VERSION`, which the launcher reads to name the download and the cache dir; `cli/bin/cli.js` reads the same version from `package.json`'s `optionalDependencies`. Bumping it is a release-time decision, like the other manifest versions. -- **Binaries are never tracked.** `skill/scripts/bin/` and `**/skills/impeccable/scripts/bin/` are gitignored, so the tracked provider dirs and `plugin/` ship launcher-only and users get the binary on first run. `bun run build:release` fetches every target (`scripts/fetch-engine.mjs --all --lenient`) and stages `bin//` into the dist skill copies **after** the root harness dirs and `plugin/` were synced, so `dist/universal.zip` is self-contained while git stays clean; `IMPECCABLE_BUNDLE_ENGINE=0` keeps the zip launcher-only. +- **Binaries are never tracked.** `skill/scripts/bin/` and `**/skills/impeccable/scripts/bin/` are gitignored, so the tracked provider dirs and `plugin/` ship launcher-only and users get the binary on first run. `bun run build:release` produces launcher-only zips by default; `IMPECCABLE_BUNDLE_ENGINE=1 bun run build:release` fetches every target (`scripts/fetch-engine.mjs --all --lenient`) and stages `bin//` into the dist skill copies **after** the root harness dirs and `plugin/` were synced, so `dist/universal.zip` is self-contained for offline installs while git stays clean. Bundling is opt-in because five targets in every provider copy put `universal.zip` near 340 MB, past the 25 MB Cloudflare Pages file cap that `impeccable install` downloads through. - **Tests get a binary** from `IMPECCABLE_BIN` or `skill/scripts/bin//` (`bun run fetch:engine`; `IMPECCABLE_BIN= bun run fetch:engine` copies a local build there). `tests/lib/engine-bin.mjs` is the one resolver; suites that need the binary skip cleanly without it. - **The oracle is the behavior gate.** `tests/oracle/` holds goldens recorded from the JS scripts before they left the tree, plus reviewed deltas in `DELTAS.md`; `tests/oracle.test.mjs` replays them against the binary in `bun run test`. New cases are recorded from the binary (`record.mjs --bin`) and reviewed by hand. `tests/oracle/vectors/calls/` is the frozen function-level snapshot; it cannot be regenerated. - **What stays JavaScript here:** the in-page live-mode JS (`skill/scripts/live-browser*.js`, `modern-screenshot.umd.js`), the build and test tooling, the extension shell, and the npm shim. diff --git a/scripts/build.js b/scripts/build.js index 55ea47043..825a6c287 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -514,10 +514,11 @@ function syncRootHookManifests(rootDir) { /** * Every skill copy in dist gets the engine binaries the launcher looks for * (`scripts/bin/-/impeccable[.exe]`), so the release zips are - * self-contained for installs without egress. Called only in release mode, - * and only after the root harness dirs and ./plugin have been synced from - * dist: those are git-delivered and must stay launcher-only (the binaries - * are gitignored and the launcher downloads them on first run). + * self-contained for installs without egress. Opt-in only + * (IMPECCABLE_BUNDLE_ENGINE=1 on a release build), and only after the root + * harness dirs and ./plugin have been synced from dist: those are + * git-delivered and must stay launcher-only (the binaries are gitignored and + * the launcher downloads them on first run). * * Source: skill/scripts/bin/, filled by scripts/fetch-engine.mjs --all. A * target that could not be fetched is reported and left out; the launcher @@ -854,13 +855,15 @@ async function build() { const openAiPluginRoot = stageOpenAIPlugin(ROOT_DIR, DIST_DIR); await createProviderZip(openAiPluginRoot, DIST_DIR, 'openai-plugin'); - // Release builds ship the engine binaries inside every dist skill copy so - // universal.zip is self-contained. This runs after the root harness dirs, - // ./plugin, and the OpenAI plugin were staged from dist: git-delivered - // trees stay launcher-only. - // IMPECCABLE_BUNDLE_ENGINE=0 keeps the release zip launcher-only (the size - // grows by roughly 4 MB per target per provider copy otherwise). - if (BUILD_OPTIONS.syncRootOutputs && process.env.IMPECCABLE_BUNDLE_ENGINE !== '0') { + // Release zips ship launcher-only by default: the launcher downloads the + // pinned engine on first run. IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging + // every fetched target into every dist skill copy for offline installs. + // That was the default once and put universal.zip at ~340 MB (five targets + // times every provider copy), past the 25 MB Cloudflare Pages file cap that + // `impeccable install` downloads through. Staging runs after the root + // harness dirs, ./plugin, and the OpenAI plugin were staged from dist, so + // git-delivered trees stay launcher-only either way. + if (BUILD_OPTIONS.syncRootOutputs && process.env.IMPECCABLE_BUNDLE_ENGINE === '1') { await stageEngineBinaries(ROOT_DIR, DIST_DIR); } diff --git a/tests/hook-build.test.mjs b/tests/hook-build.test.mjs index 05652586e..70a85bdc3 100644 --- a/tests/hook-build.test.mjs +++ b/tests/hook-build.test.mjs @@ -392,7 +392,7 @@ describe('generated hook artifacts in repo', { skip: SYNCED ? false : 'generated } assert.ok(fs.existsSync(path.join(abs, 'impeccable.cmd')), `impeccable.cmd missing in ${scriptDir}`); assert.ok(fs.existsSync(path.join(abs, 'VERSION')), `VERSION missing in ${scriptDir}`); - assert.equal(fs.existsSync(path.join(abs, 'bin')), false, `${scriptDir} must stay launcher-only in git; binaries ship only in release zips`); + assert.equal(fs.existsSync(path.join(abs, 'bin')), false, `${scriptDir} must stay launcher-only in git; binaries ship only in IMPECCABLE_BUNDLE_ENGINE=1 release zips`); const stray = fs.readdirSync(abs).filter((f) => f.endsWith('.mjs') || f === 'detector' || f === 'lib'); assert.deepEqual(stray, [], `Node-era files still in ${scriptDir}`); }