Files
PaperClipAI/scripts/prepare-bundled-package.mjs
DottaandPaperclip 6f9d0a56ba fix: resolve installed Codex and preserve npm host dependencies (#15555)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Installed agents need their own runtime dependencies.
> - Native Codex startup and browser login could depend on a global CLI.
> - npm bundles do not inherit workspace dependency overrides or
patches.
> - Codex native packages must come from npm for the consumer host.
> - This PR fixes executable resolution and the required npm dependency
layout.
> - Usable installed CLI versions can run without a numeric version
gate.

## Linked Issues or Issue Description

Refs: #15422. This is a prerequisite for the later Codex-default change.

**What happened?**

Packaged native Codex startup and browser login could require a global
Codex CLI. The server's vendored runner lacked its own declared Codex
bridge. A bundled wrapper could retain producer-host binaries or select
the legacy adapter's separate platform version.

**Expected behavior**

Prefer the installed Codex executable. Accept usable older or newer
versions. Use the selected host's PATH when the dependency is absent.
Keep the patched JavaScript graph. Let npm install official native
packages for the consumer platform. Grant sandbox reads only to the
selected vendor resources.

**Steps to reproduce**

1. Prepare a clean npm installation without a global Codex command.
2. Start native Codex or its browser login.
3. Inspect the selected executable, installed host package, and sandbox
resource paths.

**Paperclip version or commit**

Frozen master base: `1881894973a2b25838d8abed9bd8aeebc3af4441`.

**Deployment mode**

Source and packaged self-hosted installation.

## What Changed

- Share installed Codex command resolution across native startup, direct
evals, and browser login. Accept usable version differences. Preserve
explicit commands, recorded sessions, remote host boundaries, and the
Linux ARM64 legacy login path. Return safe errors for missing
executables and failed login terminals.
- Declare the server's Codex bridge. Retain the patched JavaScript
dependency graph in npm packages. Strip Codex native payloads. Declare
official optional host packages on the published server manifest so
native and legacy versions remain separate. Preserve consumer esbuild
platform dependencies.
- Adjust resource lookup for npm's separate platform packages. Retain
package identity, path containment, resolver ownership, and narrow
sandbox reads. Keep exact version and digest checks for explicit ACPX
artifact qualification.
- Extend existing packaging, installed-consumer, login, selection,
recovery, and integrity tests. Document the retained behavior.

The diff is now 23 files, 1,709 additions, and 53 deletions. The prior
diff had 35 files and about 3,700 changed lines. Removed work is
preserved on `codex/runner-packaging-full-snapshot` at
`6f3060beaa842ffcee21af058370d3cab5f571e9`.

Removed from this PR: release workflows and assembly, provider-pack
changes, Docker materialization, Git installer changes, extra login
HOME/working-directory isolation, and unrelated CI fixture repairs. This
PR does not change agent defaults, stored runner choices, UI, schema, or
provider qualification.

## Verification

- Final candidate: `dde37d7ed0121c10b60b8801eda80f8dc17909ad`. [All 47
ordinary CI jobs
passed](https://github.com/paperclipai/paperclip/actions/runs/37842288835)
on attempt 1, including typecheck, tests, build, E2E, runner checks, and
the installed-consumer canary. [Fresh Greptile
review](https://github.com/paperclipai/paperclip/pull/15555#issuecomment-6059581546)
is 5/5 on this head; no unresolved threads remain. Human approval is
still outstanding.
- Reused focused controls passed with Node 24: 71 initial narrowed
checks, then 56 affected Codex/selection/eval checks after the relative
PATH correction. Runner TypeScript no-emit, syntax, and diff checks
passed. The existing fixture reproduces the original relative PATH
failure and verifies working-directory selection, empty entries,
ordering, and absolute launch. One CLI entrypoint test was initially
blocked by sandbox IPC and passed with its existing local socket
allowed. Login HOME, config-directory assignments, and working directory
match master.
- [The actual clean Linux npm
consumer](https://github.com/paperclipai/paperclip/actions/runs/37842288835/job/113535523044)
passed on the final candidate's CI integration. All 17 Paperclip
tarballs omit native Codex payloads. Official npm host packages retain
their own integrity and `inBundle=false`. Native Codex selects 0.160.0;
the legacy closure retains 0.156.1. Package admission, command leases,
narrow native sandbox resources, consumer hooks, preserved lock, and
offline lifecycle controls passed. Provider calls were zero. No new
workflow is added.
- Actual official Codex 0.156.1 passed on the final committed source on
macOS ARM64: installed dependency preference, absolute and relative PATH
fallback, narrow resource lookup, and app-server initialize/initialized.
Executed module hashes match the candidate. One scripts-disabled
install, three version probes, and one handshake completed in 26
seconds; owned files and process group were removed. No login,
account/model request, or provider task ran.
- CI checked out `dfda8e708e87306d22ed735d78bdfcbe770e99b7` on base
`65b558180533039a891ee0cd1ccab9988aa79adc`. Its 13 upstream paths do not
overlap this PR's 23 paths or alter packaging/Codex inputs. The consumer
report records producer `7b8e94c08657b5ddc265946459762340f125726c`,
after the existing canary staged a generated-lock-only commit (one file,
three insertions). These identities are kept separate; raw
generated-lock bytes were not retained.
- No local Docker or Rust build, paid provider turn, merge, or
deployment. Later PRs must prove live onboarding and production cloud
packaging before changing defaults.

## Risks

- npm must install optional host dependencies. Missing dependencies
still return errors. Paperclip tarballs do not pre-bundle Codex
executables.
- The repository requires CI-owned lockfile updates. PR CI resolves the
changed manifest and stages its own producer lockfile. A raw source
Docker build with `--frozen-lockfile` must wait for the existing master
lockfile bot to merge its refresh, or use a disposable resolved
checkout. No Docker build or deployment is qualified by this PR.
- Ordinary Codex startup accepts version differences. Actual protocol or
login failures remain errors. Explicit ACPX artifact checks retain their
release pins.
- The published server delegates platform installation outside its
bundled JavaScript graph. Focused negative controls reject unsafe
package metadata and paths. The hosted consumer test passed on this
candidate’s CI integration.
- Existing agents retain their stored runner choices. There is no data
migration or automatic upgrade. Release pipeline and platform
qualification work remain separate prerequisites for later defaults.

## Model Used

OpenAI Codex, GPT-6, with reasoning, tool use, code execution, and
parallel agents. The exact serving snapshot and context window are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-09 08:19:51 -05:00

449 lines
22 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
const nativeTargets = ["linux-x64", "darwin-arm64", "darwin-x64"];
export function materializePublishManifest(pkg) {
const publishConfig = pkg.publishConfig ?? {};
const publishManifest = { ...pkg };
for (const key of ["main", "types", "exports", "bin"]) {
if (publishConfig[key] !== undefined) publishManifest[key] = publishConfig[key];
}
for (const section of ["dependencies", "optionalDependencies", "peerDependencies"]) {
if (!publishManifest[section]) continue;
publishManifest[section] = Object.fromEntries(
Object.entries(publishManifest[section]).map(([name, specifier]) => {
if (typeof specifier !== "string" || !specifier.startsWith("workspace:")) return [name, specifier];
const range = specifier.slice("workspace:".length);
const prefix = range === "^" || range === "~" ? range : "";
return [name, `${prefix}${pkg.version}`];
}),
);
}
delete publishManifest.publishConfig;
return publishManifest;
}
export function createBundledInstallManifest(publishManifest, bundledDependencies) {
const bundledDependencyNames = new Set(bundledDependencies);
const installManifest = structuredClone(publishManifest);
delete installManifest.devDependencies;
for (const section of ["dependencies", "optionalDependencies", "peerDependencies"]) {
if (!installManifest[section]) continue;
installManifest[section] = Object.fromEntries(
Object.entries(installManifest[section]).filter(([name]) => bundledDependencyNames.has(name)),
);
if (Object.keys(installManifest[section]).length === 0) delete installManifest[section];
}
return installManifest;
}
// npm consumers cannot inherit the workspace's pnpm overrides or patches.
// Materialize the already-qualified Codex bridge closure in the tarball,
// with overrides confined to the temporary, scripts-disabled producer graph.
export function configureBundledProviderOverrides(installManifest, bundledDependencies, rootPackage, profileData) {
const result = structuredClone(installManifest);
const selected = [];
for (const [agent, serverPackage, runtimePackage] of [
["codex", "@agentclientprotocol/codex-acp", "@openai/codex"],
]) {
if (!bundledDependencies.includes(serverPackage)) continue;
const profile = profileData?.profiles?.[agent];
if (profileData?.schema !== "paperclip.acpx-profiles.v1" || profile?.agentServerPackage !== serverPackage
|| profile.agentRuntimePackage !== runtimePackage || !/^\d+\.\d+\.\d+$/.test(profile.agentServerVersion ?? "")
|| !/^\d+\.\d+\.\d+$/.test(profile.agentRuntimeVersion ?? "")
|| result.dependencies?.[serverPackage] !== profile.agentServerVersion) {
throw new Error(`Bundled ${agent} bridge must match its exact qualified profile`);
}
const selector = `${serverPackage}@${profile.agentServerVersion}>${runtimePackage}`;
if (rootPackage.pnpm?.overrides?.[selector] !== profile.agentRuntimeVersion) {
throw new Error(`Bundled ${agent} runtime override must match its qualified profile`);
}
const npmSelector = `${serverPackage}@${profile.agentServerVersion}`;
if (result.overrides?.[npmSelector] !== undefined) {
throw new Error(`Bundled ${agent} runtime has a conflicting producer override`);
}
const overrides = { [runtimePackage]: profile.agentRuntimeVersion };
result.overrides = { ...result.overrides, [npmSelector]: overrides };
selected.push({ agent, ...profile });
}
return { installManifest: result, profiles: selected };
}
const inside = (root, candidate) => {
const value = relative(root, candidate);
return value !== "" && value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
};
// Bundle the pinned JavaScript closure, never the producer's Codex executable.
// npm installs the official optional package for the consumer's own platform.
export function stageBundledProviderOptionalDependencies(destinationDir, publishManifest, profiles) {
const graphRoot = resolve(destinationDir, "node_modules");
if (realpathSync(graphRoot) !== graphRoot) throw new Error("Bundled Codex graph must be a canonical owned directory");
const result = structuredClone(publishManifest), remove = new Set(), optional = {}, normalize = new Map();
const profile = profiles.find(value => value.agent === "codex");
if (profiles.length !== 1 || !profile) throw new Error("Bundled provider graph must contain its qualified Codex profile");
const targets = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64"];
const declarations = Object.fromEntries(targets.map(target => [`${profile.agentRuntimePackage}-${target}`,
`npm:${profile.agentRuntimePackage}@${profile.agentRuntimeVersion}-${target}`]));
const ownedDirectory = directory => {
const stat = lstatSync(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || !inside(graphRoot, realpathSync(directory))) {
throw new Error("Bundled Codex dependency escapes its producer graph");
}
};
const manifestAt = directory => {
const path = resolve(directory, "package.json"), stat = lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) {
throw new Error("Bundled Codex dependency manifest must be a bounded regular file");
}
return JSON.parse(readFileSync(path, "utf8"));
};
const bridgeDirectory = resolve(graphRoot, profile.agentServerPackage);
ownedDirectory(bridgeDirectory);
const bridge = manifestAt(bridgeDirectory);
if (bridge.name !== profile.agentServerPackage || bridge.version !== profile.agentServerVersion) throw new Error("Bundled Codex bridge version mismatch");
const issuer = createRequire(resolve(bridgeDirectory, "package.json"));
const runtimeManifest = realpathSync(issuer.resolve(`${profile.agentRuntimePackage}/package.json`));
if (!inside(graphRoot, runtimeManifest)) throw new Error("Bundled Codex runtime escapes its producer graph");
let runtimeCount = 0, packageCount = 0;
const pending = [{ directory: graphRoot, depth: 0 }];
while (pending.length) {
const { directory, depth } = pending.pop();
if (depth > 64) throw new Error("Bundled Codex graph exceeds its depth limit");
const packages = [];
for (const entry of readdirSync(directory)) {
if (entry.startsWith(".")) continue;
const candidate = resolve(directory, entry); ownedDirectory(candidate);
if (entry.startsWith("@")) {
for (const child of readdirSync(candidate)) {
const scoped = resolve(candidate, child); ownedDirectory(scoped); packages.push(scoped);
}
} else packages.push(candidate);
}
for (const packageDirectory of packages) {
if (++packageCount > 20_000) throw new Error("Bundled Codex graph exceeds its package limit");
const metadata = manifestAt(packageDirectory);
const binding = basename(dirname(packageDirectory)) === "@openai" ? `@openai/${basename(packageDirectory)}` : null;
if (binding?.startsWith("@openai/codex-")) {
const target = binding.slice("@openai/codex-".length), [os, cpu] = target.split("-");
if (!Object.hasOwn(declarations, binding) || ![binding, profile.agentRuntimePackage].includes(metadata.name)
|| metadata.version !== `${profile.agentRuntimeVersion}-${target}` || !metadata.os?.includes(os) || !metadata.cpu?.includes(cpu)) {
throw new Error("Bundled Codex installed platform identity mismatch");
}
remove.add(packageDirectory);
continue;
}
const nested = resolve(packageDirectory, "node_modules");
if (lstatExists(nested)) { ownedDirectory(nested); pending.push({ directory: nested, depth: depth + 1 }); }
if (binding !== profile.agentRuntimePackage) continue;
if (metadata.name !== profile.agentRuntimePackage || metadata.version !== profile.agentRuntimeVersion) throw new Error("Bundled Codex runtime version mismatch");
runtimeCount++;
const delegated = metadata.optionalDependencies === undefined
&& Object.entries(declarations).every(([name, specifier]) => result.optionalDependencies?.[name] === specifier);
if (!delegated && (Object.keys(metadata.optionalDependencies ?? {}).length !== targets.length
|| targets.some(target => metadata.optionalDependencies?.[`${profile.agentRuntimePackage}-${target}`] !== declarations[`${profile.agentRuntimePackage}-${target}`]))) {
throw new Error("Bundled Codex platform declaration is not qualified");
}
if (!delegated) {
const normalized = { ...metadata }; delete normalized.optionalDependencies;
normalize.set(resolve(packageDirectory, "package.json"), normalized);
}
const vendor = resolve(packageDirectory, "vendor");
if (lstatExists(vendor)) { ownedDirectory(vendor); remove.add(vendor); }
}
}
if (!runtimeCount) throw new Error("Bundled Codex runtime omitted its platform declarations");
for (const [name, specifier] of Object.entries(declarations)) {
if (result.optionalDependencies?.[name] !== undefined && result.optionalDependencies[name] !== specifier) throw new Error(`Bundled Codex platform conflicts with published dependency: ${name}`);
optional[name] = specifier;
}
// A bundled wrapper's optional edges make npm treat missing platform slots
// as bundled too. Delegate those exact declarations to the unbundled server
// root so normal npm can install the correct host version beside legacy deps.
// Validate the complete graph before changing metadata or deleting payloads.
for (const [path, metadata] of normalize) writeFileSync(path, `${JSON.stringify(metadata, null, 2)}\n`);
for (const directory of remove) rmSync(directory, { recursive: true, force: true });
result.optionalDependencies = { ...result.optionalDependencies, ...optional };
for (const field of ["bundleDependencies", "bundledDependencies"]) {
if (Array.isArray(result[field])) result[field] = result[field].filter(name => !Object.hasOwn(declarations, name));
}
delete result.paperclipProviderArtifacts;
return result;
}
function lstatExists(path) {
try { lstatSync(path); return true; }
catch (error) { if (error.code === "ENOENT") return false; throw error; }
}
// A Linux-produced bundle contains esbuild's JavaScript but only the producer's
// optional executable. Expose its exact platform declarations to the consumer,
// just as embedded-postgres does below, before retaining the original hooks.
export function stageBundledEsbuildOptionalDependencies(destinationDir, publishManifest) {
const graphRoot = resolve(destinationDir, "node_modules");
if (realpathSync(graphRoot) !== graphRoot) throw new Error("Bundled esbuild graph must be a canonical owned directory");
const result = structuredClone(publishManifest), pending = [{ directory: graphRoot, depth: 0 }];
const optional = {}, remove = new Set();
let packageCount = 0;
const ownedDirectory = directory => {
const stat = lstatSync(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || !inside(graphRoot, realpathSync(directory))) {
throw new Error("Bundled esbuild dependency escapes its producer graph");
}
};
const manifestAt = directory => {
const file = resolve(directory, "package.json"), stat = lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) {
throw new Error("Bundled esbuild dependency manifest must be a bounded regular file");
}
return JSON.parse(readFileSync(file, "utf8"));
};
while (pending.length) {
const { directory, depth } = pending.pop();
if (depth > 64) throw new Error("Bundled esbuild dependency graph exceeds its depth limit");
const packages = [];
for (const entry of readdirSync(directory)) {
if (entry.startsWith(".")) continue;
const candidate = resolve(directory, entry);
ownedDirectory(candidate);
if (entry.startsWith("@")) {
for (const child of readdirSync(candidate)) {
const scoped = resolve(candidate, child); ownedDirectory(scoped); packages.push(scoped);
}
} else packages.push(candidate);
}
for (const packageDirectory of packages) {
if (++packageCount > 20_000) throw new Error("Bundled esbuild dependency graph exceeds its package limit");
const metadata = manifestAt(packageDirectory);
const nested = resolve(packageDirectory, "node_modules");
if (lstatExists(nested)) { ownedDirectory(nested); pending.push({ directory: nested, depth: depth + 1 }); }
if (basename(packageDirectory) !== "esbuild") continue;
if (metadata.name !== "esbuild" || !/^\d+\.\d+\.\d+$/.test(metadata.version ?? "")) {
throw new Error("Bundled esbuild package identity mismatch");
}
const declarations = metadata.optionalDependencies ?? {};
if (nativeTargets.some(target => declarations[`@esbuild/${target}`] !== metadata.version)) {
throw new Error("Bundled esbuild omitted a supported platform declaration");
}
const issuer = createRequire(resolve(packageDirectory, "package.json"));
for (const [name, version] of Object.entries(declarations)) {
if (!/^@esbuild\/[a-z0-9]+-[a-z0-9]+$/.test(name) || version !== metadata.version) {
throw new Error("Bundled esbuild platform declaration must match its exact package version");
}
if ((optional[name] !== undefined && optional[name] !== version)
|| (result.optionalDependencies?.[name] !== undefined && result.optionalDependencies[name] !== version)) {
throw new Error(`Bundled esbuild platform version conflicts with published dependency: ${name}`);
}
optional[name] = version;
for (const lookup of issuer.resolve.paths(name) ?? []) {
const candidate = resolve(lookup, name);
if (!inside(graphRoot, candidate) || !lstatExists(candidate)) continue;
ownedDirectory(candidate);
const installed = manifestAt(candidate);
if (installed.name !== name || installed.version !== version) {
throw new Error(`Bundled esbuild installed platform identity mismatch: ${name}`);
}
remove.add(candidate);
}
}
}
}
for (const directory of remove) rmSync(directory, { recursive: true, force: true });
if (Object.keys(optional).length) result.optionalDependencies = { ...result.optionalDependencies, ...optional };
return result;
}
function patchedDependencyPackageName(specifier) {
const versionSeparator = specifier.lastIndexOf("@");
const packageNameEnd = specifier.startsWith("@") ? specifier.indexOf("/") : 0;
if (packageNameEnd < 0) return specifier;
return versionSeparator > packageNameEnd ? specifier.slice(0, versionSeparator) : specifier;
}
export function selectBundledDependencyPatches(
destinationDir,
bundledDependencies,
patchedDependencies,
) {
const patchesByPackageName = new Map();
for (const [specifier, patchPath] of Object.entries(patchedDependencies)) {
const packageName = patchedDependencyPackageName(specifier);
const packagePatches = patchesByPackageName.get(packageName) ?? new Map();
packagePatches.set(specifier, patchPath);
patchesByPackageName.set(packageName, packagePatches);
}
const selectedPatches = [];
for (const packageName of new Set(bundledDependencies)) {
const packagePatches = patchesByPackageName.get(packageName);
if (!packagePatches) continue;
const installedManifestPath = resolve(
destinationDir,
"node_modules",
packageName,
"package.json",
);
let installedManifest;
try {
installedManifest = JSON.parse(readFileSync(installedManifestPath, "utf8"));
} catch (cause) {
throw new Error(
`Cannot select a patch for bundled dependency ${packageName}: failed to read ${installedManifestPath}`,
{ cause },
);
}
if (
installedManifest.name !== packageName ||
typeof installedManifest.version !== "string" ||
installedManifest.version.length === 0
) {
throw new Error(
`Cannot select a patch for bundled dependency ${packageName}: installed package manifest must declare the expected name and a version`,
);
}
const installedSpecifier = `${packageName}@${installedManifest.version}`;
const patchPath = packagePatches.get(installedSpecifier);
if (patchPath === undefined) {
const configuredSpecifiers = [...packagePatches.keys()].sort().join(", ");
throw new Error(
`Cannot select a patch for bundled dependency ${packageName}: installed ${installedSpecifier}, but configured patches are ${configuredSpecifiers}`,
);
}
if (typeof patchPath !== "string" || patchPath.length === 0) {
throw new Error(`Patch path for ${installedSpecifier} must be a non-empty string`);
}
selectedPatches.push({ packageName, specifier: installedSpecifier, patchPath });
}
return selectedPatches;
}
export function applyBundledDependencyPatches(destinationDir, bundledDependencies, sourceRoot = repoRoot) {
const rootPackage = JSON.parse(readFileSync(resolve(sourceRoot, "package.json"), "utf8"));
const patchedDependencies = rootPackage.pnpm?.patchedDependencies ?? {};
for (const { packageName, patchPath } of selectBundledDependencyPatches(
destinationDir,
bundledDependencies,
patchedDependencies,
)) {
execFileSync(
"patch",
["-p1", "--forward", "-d", resolve(destinationDir, "node_modules", packageName)],
{
input: readFileSync(resolve(sourceRoot, patchPath)),
stdio: ["pipe", "inherit", "inherit"],
},
);
}
}
export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot = repoRoot } = {}) {
const sourcePackagePath = resolve(sourceDir, "package.json");
const sourcePackage = JSON.parse(readFileSync(sourcePackagePath, "utf8"));
const bundledDependencies = sourcePackage.bundleDependencies ?? sourcePackage.bundledDependencies ?? [];
if (bundledDependencies.length === 0) {
throw new Error(`${sourcePackage.name} does not declare bundled dependencies`);
}
rmSync(destinationDir, { recursive: true, force: true });
mkdirSync(destinationDir, { recursive: true });
for (const entry of sourcePackage.files ?? []) {
cpSync(resolve(sourceDir, entry), resolve(destinationDir, entry), { recursive: true });
}
for (const entry of ["README.md", "LICENSE", "LICENSE.md"]) {
const sourcePath = resolve(sourceDir, entry);
if (existsSync(sourcePath)) cpSync(sourcePath, resolve(destinationDir, entry));
}
const deployedPackagePath = resolve(destinationDir, "package.json");
const publishManifest = materializePublishManifest(sourcePackage);
const rootPackage = JSON.parse(readFileSync(resolve(sourceRoot, "package.json"), "utf8"));
const profileData = bundledDependencies.includes("@agentclientprotocol/codex-acp")
? JSON.parse(readFileSync(resolve(sourceRoot, "packages/paperclip-runner/acpx-profiles.json"), "utf8")) : undefined;
const { installManifest, profiles } = configureBundledProviderOverrides(
createBundledInstallManifest(publishManifest, bundledDependencies), bundledDependencies, rootPackage, profileData,
);
writeFileSync(deployedPackagePath, `${JSON.stringify(installManifest, null, 2)}\n`);
execFileSync(
"npm",
["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
{ cwd: destinationDir, stdio: "inherit" },
);
writeFileSync(deployedPackagePath, `${JSON.stringify(publishManifest, null, 2)}\n`);
applyBundledDependencyPatches(destinationDir, bundledDependencies, sourceRoot);
if (profiles.length) writeFileSync(deployedPackagePath, `${JSON.stringify(stageBundledProviderOptionalDependencies(destinationDir, publishManifest, profiles), null, 2)}\n`);
if (bundledDependencies.includes("acpx")) {
const acpxPackage = JSON.parse(
readFileSync(resolve(destinationDir, "node_modules/acpx/package.json"), "utf8"),
);
const expectedPatchMarker = {
"0.12.0": "onAgentStderr",
"0.13.1": "spawnEnvironment",
}[acpxPackage.version];
const acpxRuntime = readFileSync(
resolve(destinationDir, "node_modules/acpx/dist/runtime.js"),
"utf8",
);
if (!expectedPatchMarker || !acpxRuntime.includes(expectedPatchMarker)) {
throw new Error(
`staged acpx@${acpxPackage.version} runtime is missing the repository patch`,
);
}
}
if (bundledDependencies.includes("embedded-postgres")) {
const embeddedPostgresSource = readFileSync(
resolve(destinationDir, "node_modules/embedded-postgres/dist/index.js"),
"utf8",
);
if (
!embeddedPostgresSource.includes("const LC_MESSAGES_LOCALE = 'C';") ||
!embeddedPostgresSource.includes("globalThis.process.env")
) {
throw new Error("staged embedded-postgres runtime is missing the repository patch");
}
const embeddedPostgresPackage = JSON.parse(
readFileSync(resolve(destinationDir, "node_modules/embedded-postgres/package.json"), "utf8"),
);
const stagedPackage = JSON.parse(readFileSync(deployedPackagePath, "utf8"));
stagedPackage.optionalDependencies = {
...(stagedPackage.optionalDependencies ?? {}),
...(embeddedPostgresPackage.optionalDependencies ?? {}),
};
writeFileSync(deployedPackagePath, `${JSON.stringify(stagedPackage, null, 2)}\n`);
rmSync(resolve(destinationDir, "node_modules/@embedded-postgres"), { recursive: true, force: true });
}
writeFileSync(deployedPackagePath, `${JSON.stringify(stageBundledEsbuildOptionalDependencies(destinationDir,
JSON.parse(readFileSync(deployedPackagePath, "utf8"))), null, 2)}\n`);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const [sourceDir, destinationDir] = process.argv.slice(2);
if (!sourceDir || !destinationDir) {
console.error("Usage: prepare-bundled-package.mjs <source-dir> <destination-dir>");
process.exit(1);
}
prepareBundledPackage(resolve(sourceDir), resolve(destinationDir));
}