mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The server package ships runtime asset trees used by built-in agents and onboarding templates. > - A prior server build omitted those source asset trees from `dist`, allowing a built artifact to differ from runtime expectations. > - The copy step is now present, but the existing build-gap gate only checked TypeScript coverage for packages whose build skips `tsc`. > - This pull request extends that standing gate so source asset files under the server runtime asset trees must exist at the matching `dist` paths after build. > - The benefit is that future server asset additions fail loudly in CI instead of silently shipping an incomplete `dist`. ## Linked Issues or Issue Description ### What happened? After a server build, runtime asset files under `server/src/built-ins/**` and `server/src/onboarding-assets/**` could be missing from `dist/` with no build failure. The existing build-gap gate only checked TypeScript coverage for packages that skip `tsc`; it did not verify that non-TypeScript source assets were copied to `dist`. A server build that forgot the `cp -R` step, or that added a new asset tree without updating the copy command, would produce an incomplete `dist` without any CI signal. ### Expected behavior After `pnpm --filter @paperclipai/server build`, every non-TypeScript/non-JavaScript source file under `server/src/built-ins/` and `server/src/onboarding-assets/` must exist at the matching path under `server/dist/`. If any file is missing, the build-gap gate must exit non-zero with a diagnostic listing the missing files and the command to fix them. ### Steps to reproduce 1. Remove a copied runtime asset: `rm server/dist/built-ins/agents/reflection-coach/AGENTS.md` 2. Run the guard: `node scripts/run-typecheck-build-gaps.mjs --runtime-assets-only` 3. Before this fix: the command exits 0 and the missing file goes undetected. ### Paperclip version or commit Reproduced on `master` at `c36f1a4af` (`@paperclipai/server` 0.3.1). ### Deployment mode Not deployment-specific — the build-gap check runs in CI on any checkout. ## What Changed - Extended `scripts/run-typecheck-build-gaps.mjs` with a source-derived server runtime asset parity check for non-`.ts`/non-`.js` files under `server/src/built-ins/**` and `server/src/onboarding-assets/**`. - Added a guard-only mode, `--runtime-assets-only`, for focused pass/fail verification after a server build. - Wired `pnpm run typecheck:build-gaps` to prepare plugin SDK build deps, build the server package, then run the existing build-gap gate plus the new asset check. ## Verification Pass path: ```text $ pnpm --filter @paperclipai/plugin-sdk ensure-build-deps > @paperclipai/[email protected] ensure-build-deps .../packages/plugins/sdk > node ../../../scripts/ensure-plugin-build-deps.mjs $ pnpm --filter @paperclipai/server build > @paperclipai/[email protected] build .../server > tsc && mkdir -p dist/onboarding-assets dist/built-ins && cp -R src/onboarding-assets/. dist/onboarding-assets/ && cp -R src/built-ins/. dist/built-ins/ $ node scripts/run-typecheck-build-gaps.mjs --runtime-assets-only [typecheck:build-gaps] server runtime assets present in dist: 7 file(s) ``` Regression simulation (guard catches the missing file): ```text $ rm server/dist/built-ins/agents/reflection-coach/AGENTS.md $ node scripts/run-typecheck-build-gaps.mjs --runtime-assets-only [typecheck:build-gaps] Missing server runtime asset(s) in dist: - source: server/src/built-ins/agents/reflection-coach/AGENTS.md expected dist: server/dist/built-ins/agents/reflection-coach/AGENTS.md Run pnpm --filter @paperclipai/server build and ensure source runtime asset trees are copied into dist. ``` Standing gate (full end-to-end): ```text $ pnpm run typecheck:build-gaps [typecheck:build-gaps] typechecking 4 workspace(s): paperclipai, @paperclipai/plugin-authoring-smoke-example, @paperclipai/plugin-llm-wiki, @paperclipai/ui [typecheck:build-gaps] server runtime assets present in dist: 7 file(s) ``` ## Risks Low risk. The check only reads source and dist files during the build-gap gate. The main tradeoff is that the gate now builds `@paperclipai/server` so a clean checkout has generated `dist` content to validate. ## Model Used OpenAI Codex, GPT-5 based coding agent with repository tool use and shell execution. ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <[email protected]>
187 lines
5.1 KiB
JavaScript
187 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const repoRoot = process.cwd();
|
|
const args = new Set(process.argv.slice(2));
|
|
const allowedArgs = new Set(["--runtime-assets-only"]);
|
|
|
|
for (const arg of args) {
|
|
if (!allowedArgs.has(arg)) {
|
|
fail(`Unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
|
|
function fail(message) {
|
|
console.error(`[typecheck:build-gaps] ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function run(command, args) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: repoRoot,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
if (result.error) {
|
|
console.error(`[typecheck:build-gaps] Failed to spawn ${command}: ${result.error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function formatPath(filePath) {
|
|
return path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
}
|
|
|
|
function listFilesRecursive(rootDir) {
|
|
if (!existsSync(rootDir)) {
|
|
return [];
|
|
}
|
|
|
|
const files = [];
|
|
const entries = readdirSync(rootDir, { withFileTypes: true }).sort((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
);
|
|
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(rootDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...listFilesRecursive(entryPath));
|
|
continue;
|
|
}
|
|
|
|
if (entry.isFile()) {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function isRuntimeAsset(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
return !new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]).has(ext);
|
|
}
|
|
|
|
function checkServerRuntimeAssets() {
|
|
const runtimeAssetTrees = [
|
|
{
|
|
sourceDir: path.join(repoRoot, "server", "src", "built-ins"),
|
|
distDir: path.join(repoRoot, "server", "dist", "built-ins"),
|
|
},
|
|
{
|
|
sourceDir: path.join(repoRoot, "server", "src", "onboarding-assets"),
|
|
distDir: path.join(repoRoot, "server", "dist", "onboarding-assets"),
|
|
},
|
|
];
|
|
const missingAssets = [];
|
|
let checkedCount = 0;
|
|
|
|
for (const assetTree of runtimeAssetTrees) {
|
|
const sourceAssets = listFilesRecursive(assetTree.sourceDir).filter(isRuntimeAsset);
|
|
checkedCount += sourceAssets.length;
|
|
|
|
for (const sourcePath of sourceAssets) {
|
|
const relativeAssetPath = path.relative(assetTree.sourceDir, sourcePath);
|
|
const distPath = path.join(assetTree.distDir, relativeAssetPath);
|
|
|
|
if (!existsSync(distPath) || !statSync(distPath).isFile()) {
|
|
missingAssets.push({ sourcePath, distPath });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (missingAssets.length > 0) {
|
|
const missingList = missingAssets
|
|
.map(
|
|
({ sourcePath, distPath }) =>
|
|
` - source: ${formatPath(sourcePath)}\n expected dist: ${formatPath(distPath)}`,
|
|
)
|
|
.join("\n");
|
|
|
|
fail(
|
|
`Missing server runtime asset(s) in dist:\n${missingList}\nRun pnpm --filter @paperclipai/server build and ensure source runtime asset trees are copied into dist.`,
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`[typecheck:build-gaps] server runtime assets present in dist: ${checkedCount} file(s)`,
|
|
);
|
|
}
|
|
|
|
if (args.has("--runtime-assets-only")) {
|
|
checkServerRuntimeAssets();
|
|
process.exit(0);
|
|
}
|
|
|
|
function listWorkspacePackages() {
|
|
const result = spawnSync("pnpm", ["ls", "-r", "--depth", "-1", "--json"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
});
|
|
|
|
if (result.error) {
|
|
fail(`Unable to spawn pnpm to list workspace packages: ${result.error.message}`);
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
fail("Unable to list pnpm workspace packages.");
|
|
}
|
|
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
function buildSkipsTypeScript(pkg) {
|
|
const buildScript = pkg.scripts?.build;
|
|
if (typeof buildScript !== "string") {
|
|
return false;
|
|
}
|
|
|
|
return !/\btsc\b/.test(buildScript);
|
|
}
|
|
|
|
const workspacePackages = listWorkspacePackages();
|
|
const buildGapCandidates = workspacePackages
|
|
.filter((workspacePkg) => workspacePkg.path !== repoRoot)
|
|
.map((workspacePkg) => ({
|
|
name: workspacePkg.name,
|
|
path: workspacePkg.path,
|
|
pkg: readJson(path.join(workspacePkg.path, "package.json")),
|
|
}))
|
|
.filter(({ pkg }) => buildSkipsTypeScript(pkg));
|
|
const packagesMissingTypecheck = buildGapCandidates.filter(
|
|
({ pkg }) => typeof pkg.scripts?.typecheck !== "string",
|
|
);
|
|
if (packagesMissingTypecheck.length > 0) {
|
|
const missingNames = packagesMissingTypecheck.map((workspacePkg) => workspacePkg.name).join(", ");
|
|
fail(
|
|
`Workspace packages with build scripts that skip tsc must define a typecheck script. Missing: ${missingNames}`,
|
|
);
|
|
}
|
|
const buildGapPackages = buildGapCandidates.filter(
|
|
({ pkg }) => typeof pkg.scripts?.typecheck === "string",
|
|
);
|
|
|
|
console.log(
|
|
`[typecheck:build-gaps] typechecking ${buildGapPackages.length} workspace(s): ${buildGapPackages.map(({ name }) => name).join(", ") || "(none)"}`,
|
|
);
|
|
|
|
if (buildGapPackages.length > 0) {
|
|
run("pnpm", ["--filter", "@paperclipai/plugin-sdk", "ensure-build-deps"]);
|
|
|
|
for (const workspacePkg of buildGapPackages) {
|
|
run("pnpm", ["--filter", workspacePkg.name, "typecheck"]);
|
|
}
|
|
}
|
|
|
|
checkServerRuntimeAssets();
|