From 40782f703d1f4a13f4ceadbe84c9b92be0bfacaf Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sat, 25 Apr 2026 12:16:23 -0700 Subject: [PATCH] Fix release packaging for standalone public packages (#4494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies, and the sandbox-provider work just moved E2B into a standalone publishable plugin package. > - That plugin is intentionally excluded from the root pnpm workspace so it can model third-party install behavior without forcing lockfile churn in the main repo. > - The merged architecture change exposed a follow-up release problem: the canary publish workflow tried to publish `@paperclipai/plugin-e2b`, but the tarball had no `dist/` payload because standalone public packages were not being built in the release path. > - That means the release pipeline needed a packaging fix in core release tooling, not another architectural change in the sandbox provider itself. > - This pull request adds a generic release step for public packages that live outside the pnpm workspace, instead of hardcoding E2B-specific behavior into the release script. > - The benefit is that standalone publishable packages can be built and packed correctly during release, including future sandbox-provider plugins that follow the same pattern. ## What Changed - Added `scripts/build-standalone-public-packages.mjs` to discover public packages outside the pnpm workspace, run a clean package-local install, and build them before publish. - Updated `scripts/release.sh` to invoke that helper immediately after the normal workspace build step. - Kept the behavior generic by driving off the existing public package map and pnpm workspace patterns rather than special-casing `@paperclipai/plugin-e2b`. ## Verification - `rm -rf packages/plugins/sandbox-providers/e2b/dist` - `node ./scripts/build-standalone-public-packages.mjs` - `cd packages/plugins/sandbox-providers/e2b && npm pack --dry-run` - Confirm the tarball now includes the rebuilt `dist/` files instead of only `README.md` / `package.json` ## Risks - Low risk: this only changes the release build path for public packages outside the pnpm workspace. - The helper performs a clean package-local install for each standalone public package, so release time may increase slightly as more such packages are added. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex via `codex_local` - Model ID: `gpt-5.4` - Reasoning effort: `high` - Context window observed in runtime session metadata: `258400` tokens - Capabilities used: terminal tool execution, git, GitHub CLI, and local build/test inspection ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --- scripts/build-standalone-public-packages.mjs | 144 +++++++++++++++++++ scripts/release.sh | 1 + 2 files changed, 145 insertions(+) create mode 100644 scripts/build-standalone-public-packages.mjs diff --git a/scripts/build-standalone-public-packages.mjs b/scripts/build-standalone-public-packages.mjs new file mode 100644 index 00000000..3a199219 --- /dev/null +++ b/scripts/build-standalone-public-packages.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, rmSync } from "node:fs"; +import path, { dirname } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, ".."); +const workspacePath = path.join(repoRoot, "pnpm-workspace.yaml"); +const releasePackageMapPath = path.join(repoRoot, "scripts", "release-package-map.mjs"); + +function parseWorkspaceEntries(workspaceText) { + // Keep this aligned with the repo's block-sequence `packages:` format in + // pnpm-workspace.yaml. If that file moves to a more complex YAML shape, + // switch this parser to a real YAML parser instead of line matching. + return workspaceText + .split("\n") + .map((line) => line.match(/^\s*-\s+(.+)\s*$/)?.[1]?.trim() ?? null) + .map((entry) => { + if (!entry) return entry; + return entry.replace(/^(['"])(.*)\1$/, "$2"); + }) + .filter(Boolean) + .map((entry) => ({ + pattern: entry.startsWith("!") ? entry.slice(1) : entry, + negated: entry.startsWith("!"), + })); +} + +function globToRegExp(pattern) { + let regex = ""; + + for (let index = 0; index < pattern.length; index += 1) { + const char = pattern[index]; + const next = pattern[index + 1]; + + if (char === "*" && next === "*") { + regex += ".*"; + index += 1; + continue; + } + if (char === "*") { + regex += "[^/]*"; + continue; + } + if (char === "?") { + regex += "[^/]"; + continue; + } + regex += /[|\\{}()[\]^$+?.]/.test(char) ? `\\${char}` : char; + } + + return new RegExp(`^${regex}$`); +} + +function isWorkspacePackage(pkgDir, workspaceEntries) { + let included = false; + + for (const entry of workspaceEntries) { + if (globToRegExp(entry.pattern).test(pkgDir)) { + included = !entry.negated; + } + } + + return included; +} + +function listPublicPackages() { + const output = execFileSync( + process.execPath, + [releasePackageMapPath, "list"], + { cwd: repoRoot, encoding: "utf8" }, + ); + + return output + .trim() + .split("\n") + .filter(Boolean) + .map((line) => { + const [dir, name] = line.split("\t"); + return { dir, name }; + }); +} + +function readPackageJson(pkgDir) { + return JSON.parse( + readFileSync(path.join(repoRoot, pkgDir, "package.json"), "utf8"), + ); +} + +function run(command, args, cwd) { + execFileSync(command, args, { + cwd, + env: { + ...process.env, + CI: "true", + }, + stdio: "inherit", + }); +} + +function main() { + const workspaceEntries = parseWorkspaceEntries(readFileSync(workspacePath, "utf8")); + const standalonePackages = listPublicPackages() + .filter(({ dir }) => !isWorkspacePackage(dir, workspaceEntries)); + + if (standalonePackages.length === 0) { + console.log(" i No standalone public packages detected outside the pnpm workspace"); + return; + } + + for (const pkg of standalonePackages) { + const pkgDir = path.join(repoRoot, pkg.dir); + const pkgJson = readPackageJson(pkg.dir); + const nodeModulesDir = path.join(pkgDir, "node_modules"); + const packageLockfilePath = path.join(pkgDir, "pnpm-lock.yaml"); + + console.log(` Preparing standalone package ${pkg.name} (${pkg.dir})`); + if (existsSync(nodeModulesDir)) { + rmSync(nodeModulesDir, { force: true, recursive: true }); + } + + const installArgs = existsSync(packageLockfilePath) + ? ["install", "--ignore-workspace", "--frozen-lockfile"] + : [ + "install", + "--ignore-workspace", + "--no-lockfile", + // Standalone packages intentionally avoid committed lockfile churn in the repo. + ]; + + run("pnpm", installArgs, pkgDir); + + if (pkgJson.scripts?.build) { + run("pnpm", ["run", "build"], pkgDir); + } else { + console.log(" i No build script; skipped build"); + } + } +} + +main(); diff --git a/scripts/release.sh b/scripts/release.sh index 6a726896..74cdaa8f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -214,6 +214,7 @@ release_info "" release_info "==> Step 2/7: Building workspace artifacts..." cd "$REPO_ROOT" pnpm build +node "$REPO_ROOT/scripts/build-standalone-public-packages.mjs" bash "$REPO_ROOT/scripts/prepare-server-ui-dist.sh" for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do rm -rf "$REPO_ROOT/$pkg_dir/skills"