Revert "fix(code-mode): make packaged code mode work and drop ~460MB bundled engines (#614)"

This reverts commit 33c15cfbd9.
This commit is contained in:
Gagan 2026-06-16 23:29:46 -07:00
parent ed8f6f7246
commit 8ce24ebb33
8 changed files with 51 additions and 500 deletions

View file

@ -5,94 +5,6 @@
const path = require('path');
const pkg = require('./package.json');
// Stage the ACP coding-adapters (@agentclientprotocol/*-acp) and their full
// production dependency closure into the packaged app.
//
// Why this is needed: code mode spawns each adapter as a SEPARATE `node <entry>`
// process and locates it at runtime via require.resolve — so it must ship as a real
// on-disk file. esbuild can't inline it (dynamic resolve + spawn target), and Forge
// strips the workspace node_modules (see `ignore` below). Without this, packaged
// builds throw `Cannot find module '@agentclientprotocol/...'`.
//
// Why we reconstruct a nested tree instead of copying node_modules: pnpm's store is a
// symlink farm that legitimately holds multiple versions of the same package (e.g.
// @agentclientprotocol/sdk 0.21 for claude vs 0.22 for codex). We rebuild an npm-style
// nested node_modules — dereferencing symlinks and nesting on version conflict — which
// resolves correctly regardless of pnpm layout.
//
// What we DON'T bundle: the agents' native engines (claude.exe / codex.exe, ~230 MB
// each, shipped as platform-specific packages). Code mode drives each agent from the
// user's LOCAL install via CLAUDE_CODE_EXECUTABLE / CODEX_PATH (see
// packages/core/src/code-mode/acp/agents.ts), so the bundled engines would be dead
// weight. Skipping them keeps each OS installer ~470 MB smaller. (The adapters only
// fall back to a bundled engine when those env vars are unset, which agents.ts never
// leaves unset — it errors clearly if the local agent isn't installed.)
function stageAcpAdapters(mainDir, destNodeModules) {
const fs = require('fs');
const ADAPTERS = [
'@agentclientprotocol/claude-agent-acp',
'@agentclientprotocol/codex-acp',
];
// The bundled native engines, shipped as platform packages. Driven from the user's
// local install instead (see comment above), so they're excluded from staging.
const isNativeEngine = (key) =>
/^@anthropic-ai\/claude-agent-sdk-(win32|darwin|linux)/.test(key) || // bundled claude.exe
/^@openai\/codex-(win32|darwin|linux)/.test(key); // bundled codex.exe
// Resolve a dependency's real directory by walking node_modules the way Node does,
// looking for the package DIRECTORY. We deliberately do NOT use
// require.resolve(`${key}/package.json`): that throws for packages whose `exports`
// map doesn't expose package.json (e.g. @anthropic-ai/claude-agent-sdk), which would
// silently drop them and their subtrees. realpathSync dereferences pnpm's symlinks.
// Returns null for deps not installed for this OS (platform-optional binaries).
const realDirOf = (key, fromDir) => {
let dir = fromDir;
for (;;) {
const cand = path.join(dir, 'node_modules', ...key.split('/'));
if (fs.existsSync(path.join(cand, 'package.json'))) return fs.realpathSync(cand);
const parent = path.dirname(dir);
if (parent === dir) return null;
dir = parent;
}
};
let copied = 0;
const skippedEngines = new Set();
const install = (srcDir, key, destNM, chain) => {
const destDir = path.join(destNM, ...key.split('/'));
if (fs.existsSync(destDir)) return; // already placed at this exact location
if (chain.has(srcDir)) return; // dependency cycle — resolves to ancestor copy
fs.mkdirSync(path.dirname(destDir), { recursive: true });
fs.cpSync(srcDir, destDir, {
recursive: true,
dereference: true,
filter: (s) => path.basename(s) !== 'node_modules', // deps handled by recursion
});
copied++;
const pj = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf8'));
const deps = { ...pj.dependencies, ...pj.optionalDependencies };
const nextChain = new Set(chain).add(srcDir);
for (const depKey of Object.keys(deps)) {
if (isNativeEngine(depKey)) { skippedEngines.add(depKey); continue; }
const depDir = realDirOf(depKey, srcDir);
if (depDir) install(depDir, depKey, path.join(destDir, 'node_modules'), nextChain);
}
};
for (const key of ADAPTERS) {
const srcDir = realDirOf(key, mainDir);
if (!srcDir) {
throw new Error(`ACP adapter '${key}' is not installed in ${mainDir} — run pnpm install`);
}
install(srcDir, key, destNodeModules, new Set());
}
if (skippedEngines.size) {
console.log(` (skipped bundled native engines — driven from local install: ${[...skippedEngines].join(', ')})`);
}
return copied;
}
module.exports = {
packagerConfig: {
executableName: 'rowboat',
@ -105,41 +17,31 @@ module.exports = {
extendInfo: {
NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)',
},
// Sign/notarize only when release credentials are present (the release
// workflow sets APPLE_ID). Local mac dev builds and the CI smoke matrix
// have no signing identity — leaving these unconditional makes
// `npm run package` fail there.
...(process.env.APPLE_ID ? {
osxSign: {
batchCodesignCalls: true,
optionsForFile: () => ({
entitlements: path.join(__dirname, 'entitlements.plist'),
'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
}),
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
},
} : {}),
// Since we bundle the main process with esbuild, we don't need the workspace
// node_modules. These settings prevent Forge's dependency walker (flora-colossus)
// from trying to analyze/copy node_modules, which fails with pnpm's symlinked
// workspaces.
prune: false,
// Strip the workspace src/node_modules (regexes ANCHORED to the app root), BUT
// always keep everything under `.package/` — that's our staged output: the
// bundled main process, the ACP adapters + their dependency closure (staged by
// the generateAssets hook), and the native node-pty module (staged into
// .package/node_modules by bundle.mjs). Without the `.package` exemption the
// node_modules rule would strip those and code mode / the embedded terminal
// would break in packaged builds.
ignore: (p) => {
if (p === '/.package' || p.startsWith('/.package/')) return false;
return [/^\/src\//, /^\/node_modules\//, /\.gitignore/, /bundle\.mjs/, /tsconfig\.json/]
.some((re) => re.test(p));
osxSign: {
batchCodesignCalls: true,
optionsForFile: () => ({
entitlements: path.join(__dirname, 'entitlements.plist'),
'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
}),
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
},
// Since we bundle everything with esbuild, we don't need node_modules at all.
// These settings prevent Forge's dependency walker (flora-colossus) from trying
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
// Regexes are ANCHORED to the app root: .package/node_modules (where
// bundle.mjs stages the native node-pty module) must survive packaging.
prune: false,
ignore: [
/^\/src\//,
/^\/node_modules\//,
/.gitignore/,
/bundle\.mjs/,
/tsconfig.json/,
],
},
makers: [
{
@ -293,15 +195,6 @@ module.exports = {
fs.mkdirSync(rendererDest, { recursive: true });
fs.cpSync(rendererSrc, rendererDest, { recursive: true });
// Stage the ACP coding-adapters (+ their dependency closure) into .package/acp.
// They are spawned as separate node processes at runtime and Forge strips the
// workspace node_modules, so they must be copied in explicitly. See
// stageAcpAdapters() above for the why.
console.log('Staging ACP adapters...');
const acpDest = path.join(packageDir, 'acp', 'node_modules');
const staged = stageAcpAdapters(__dirname, acpDest);
console.log(`✅ Staged ${staged} ACP adapter packages into .package/acp/node_modules`);
console.log('✅ All assets staged in .package/');
},
}