forked from farhoodlabs/paperclip
868d08903e
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies, and its CLI import/export path is part of how operators move company state safely between environments. > - The `paperclipai company import/export` e2e test is supposed to validate that portability flow inside a hermetic harness, not against a developer's live Paperclip home. > - This regression showed nested CLI subprocesses could silently fall back to ambient `PAPERCLIP_*` state and mutate a real local instance by creating extra companies such as `CLI-1-Roundtrip-Test`. > - The first job was to pin the test subprocesses to isolated config, home, instance, auth, and context paths, and to add a regression assertion that proves the nested CLI writes stay inside the test-owned state. > - Once the PR was up, CI and Greptile exposed two follow-on issues that were blocking merge: plugin SDK typecheck bootstrap was racing across packages in fresh CI, and the new lock helper needed one more fix to release its lock on failure. > - This pull request therefore ends up doing two tightly related things: fixing the original CLI isolation leak, and hardening the supporting typecheck/bootstrap path enough for the fix to verify cleanly in CI. > - The benefit is that the portability e2e test is now actually isolated, and the PR verification path is stable enough to catch regressions instead of introducing its own nondeterministic failures. ## What Changed - Hardened `cli/src/__tests__/company-import-export-e2e.test.ts` so nested CLI subprocesses re-seed isolated `PAPERCLIP_CONFIG`, `PAPERCLIP_HOME`, `PAPERCLIP_INSTANCE_ID`, `PAPERCLIP_CONTEXT`, `PAPERCLIP_AUTH_STORE`, and throwaway `HOME` values instead of falling back to ambient machine state. - Added a regression assertion around `paperclipai context set --json`, then cleared the temporary `context.json` so the isolation check and the later export/import flow stay independent. - Passed the same isolated `HOME` into the server subprocess so both sides of the e2e harness are symmetric. - Introduced locking in `scripts/ensure-plugin-build-deps.mjs` and switched the server/plugin example `typecheck` scripts to use that helper instead of launching concurrent raw `@paperclipai/plugin-sdk` builds. - Fixed the helper failure path so it releases the lock before exiting non-zero, which prevents stale-lock timeouts during parallel typecheck runs. ## Verification - `pnpm vitest run cli/src/__tests__/company-import-export-e2e.test.ts --project paperclipai` - `pnpm --filter paperclipai typecheck` - `pnpm -r typecheck` - PR checks now pass on the current head, including `policy`, `verify`, `e2e`, `security/snyk`, and `Greptile Review`. ## Risks - Low risk. The product-facing behavior change is scoped to test harness code in the CLI e2e suite. - The CI stabilization changes only affect bootstrap/typecheck helper paths for the server and plugin/example packages, but they do touch shared verification plumbing; the main risk is changing how fresh build artifacts are prepared in local/CI typecheck runs. ## Model Used - Anthropic Claude via Paperclip `claude_local`, model `claude-opus-4-7`, high-effort local coding agent, used for the initial implementation and first peer-reviewed verification. - OpenAI Codex via Paperclip `codex_local`, model `gpt-5.4`, high reasoning-effort local coding agent with tool use, used for CI triage, Greptile follow-up fixes, verification, and PR maintenance. ## 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
106 lines
2.5 KiB
JavaScript
106 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const rootDir = path.resolve(scriptDir, "..");
|
|
const tscCliPath = path.join(rootDir, "node_modules", "typescript", "bin", "tsc");
|
|
const lockDir = path.join(rootDir, "node_modules", ".cache", "paperclip-plugin-build-deps.lock");
|
|
const lockTimeoutMs = 60_000;
|
|
const lockPollMs = 100;
|
|
|
|
const buildTargets = [
|
|
{
|
|
name: "@paperclipai/shared",
|
|
output: path.join(rootDir, "packages/shared/dist/index.js"),
|
|
tsconfig: path.join(rootDir, "packages/shared/tsconfig.json"),
|
|
},
|
|
{
|
|
name: "@paperclipai/plugin-sdk",
|
|
output: path.join(rootDir, "packages/plugins/sdk/dist/index.js"),
|
|
tsconfig: path.join(rootDir, "packages/plugins/sdk/tsconfig.json"),
|
|
},
|
|
];
|
|
|
|
if (!fs.existsSync(tscCliPath)) {
|
|
throw new Error(`TypeScript CLI not found at ${tscCliPath}`);
|
|
}
|
|
|
|
function allOutputsExist() {
|
|
return buildTargets.every((target) => fs.existsSync(target.output));
|
|
}
|
|
|
|
function sleep(ms) {
|
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
}
|
|
|
|
function waitForLockRelease() {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < lockTimeoutMs) {
|
|
if (!fs.existsSync(lockDir)) {
|
|
return;
|
|
}
|
|
if (allOutputsExist()) {
|
|
return;
|
|
}
|
|
sleep(lockPollMs);
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for plugin build dependency lock at ${lockDir}`);
|
|
}
|
|
|
|
if (allOutputsExist()) {
|
|
process.exit(0);
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
|
|
let holdsLock = false;
|
|
let exitCode = 0;
|
|
try {
|
|
try {
|
|
fs.mkdirSync(lockDir);
|
|
holdsLock = true;
|
|
} catch (error) {
|
|
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
waitForLockRelease();
|
|
if (!allOutputsExist()) {
|
|
throw new Error("Plugin build dependency lock released before all outputs were created");
|
|
}
|
|
process.exit(0);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
for (const target of buildTargets) {
|
|
if (fs.existsSync(target.output)) {
|
|
continue;
|
|
}
|
|
|
|
const result = spawnSync(process.execPath, [tscCliPath, "-p", target.tsconfig], {
|
|
cwd: rootDir,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
exitCode = result.status ?? 1;
|
|
break;
|
|
}
|
|
}
|
|
} finally {
|
|
if (holdsLock) {
|
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
if (exitCode !== 0) {
|
|
process.exit(exitCode);
|
|
}
|