Repair server workspace package links in worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
ec1210caaa
commit
2b18fc4007
3 changed files with 185 additions and 28 deletions
113
scripts/ensure-workspace-package-links.ts
Normal file
113
scripts/ensure-workspace-package-links.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env -S node --import tsx
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { repoRoot } from "./dev-service-profile.ts";
|
||||
|
||||
type WorkspaceLinkMismatch = {
|
||||
packageName: string;
|
||||
expectedPath: string;
|
||||
actualPath: string | null;
|
||||
};
|
||||
|
||||
function readJsonFile(filePath: string): Record<string, unknown> {
|
||||
return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function resolveWorkspacePackagePath(packageName: string): string | null {
|
||||
if (packageName === "@paperclipai/adapter-utils") {
|
||||
return path.join(repoRoot, "packages", "adapter-utils");
|
||||
}
|
||||
if (packageName === "@paperclipai/db") {
|
||||
return path.join(repoRoot, "packages", "db");
|
||||
}
|
||||
if (packageName === "@paperclipai/shared") {
|
||||
return path.join(repoRoot, "packages", "shared");
|
||||
}
|
||||
if (packageName === "@paperclipai/plugin-sdk") {
|
||||
return path.join(repoRoot, "packages", "plugins", "sdk");
|
||||
}
|
||||
if (packageName.startsWith("@paperclipai/adapter-")) {
|
||||
return path.join(repoRoot, "packages", "adapters", packageName.slice("@paperclipai/adapter-".length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] {
|
||||
const serverPackageJson = readJsonFile(path.join(repoRoot, "server", "package.json"));
|
||||
const dependencies = {
|
||||
...(serverPackageJson.dependencies as Record<string, unknown> | undefined),
|
||||
...(serverPackageJson.devDependencies as Record<string, unknown> | undefined),
|
||||
};
|
||||
const mismatches: WorkspaceLinkMismatch[] = [];
|
||||
|
||||
for (const [packageName, version] of Object.entries(dependencies)) {
|
||||
if (typeof version !== "string" || !version.startsWith("workspace:")) continue;
|
||||
|
||||
const expectedPath = resolveWorkspacePackagePath(packageName);
|
||||
if (!expectedPath) continue;
|
||||
|
||||
const linkPath = path.join(repoRoot, "server", "node_modules", ...packageName.split("/"));
|
||||
const actualPath = existsSync(linkPath) ? path.resolve(realpathSync(linkPath)) : null;
|
||||
if (actualPath === path.resolve(expectedPath)) continue;
|
||||
|
||||
mismatches.push({
|
||||
packageName,
|
||||
expectedPath: path.resolve(expectedPath),
|
||||
actualPath,
|
||||
});
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], cwd: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(
|
||||
new Error(
|
||||
`${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureServerWorkspaceLinksCurrent() {
|
||||
const mismatches = findServerWorkspaceLinkMismatches();
|
||||
if (mismatches.length === 0) return;
|
||||
|
||||
console.log("[paperclip] detected stale workspace package links for server; relinking dependencies...");
|
||||
for (const mismatch of mismatches) {
|
||||
console.log(
|
||||
`[paperclip] ${mismatch.packageName}: ${mismatch.actualPath ?? "missing"} -> ${mismatch.expectedPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
await runCommand(
|
||||
pnpmBin,
|
||||
["install", "--force", "--config.confirmModulesPurge=false"],
|
||||
repoRoot,
|
||||
);
|
||||
|
||||
const remainingMismatches = findServerWorkspaceLinkMismatches();
|
||||
if (remainingMismatches.length === 0) return;
|
||||
|
||||
throw new Error(
|
||||
`Workspace relink did not repair all server package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
await ensureServerWorkspaceLinksCurrent();
|
||||
|
|
@ -32,15 +32,16 @@
|
|||
"skills"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"dev:watch": "cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx ./scripts/dev-watch.ts",
|
||||
"preflight:workspace-links": "tsx ../scripts/ensure-workspace-package-links.ts",
|
||||
"dev": "pnpm run preflight:workspace-links && tsx src/index.ts",
|
||||
"dev:watch": "pnpm run preflight:workspace-links && cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx ./scripts/dev-watch.ts",
|
||||
"prepare:ui-dist": "bash ../scripts/prepare-server-ui-dist.sh",
|
||||
"build": "tsc && mkdir -p dist/onboarding-assets && cp -R src/onboarding-assets/. dist/onboarding-assets/",
|
||||
"build": "pnpm run preflight:workspace-links && tsc && mkdir -p dist/onboarding-assets && cp -R src/onboarding-assets/. dist/onboarding-assets/",
|
||||
"prepack": "pnpm run prepare:ui-dist",
|
||||
"postpack": "rm -rf ui-dist",
|
||||
"clean": "rm -rf dist",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
|
||||
"typecheck": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.888.0",
|
||||
|
|
|
|||
|
|
@ -1,34 +1,77 @@
|
|||
// Re-export everything from the shared adapter-utils/server-utils package.
|
||||
// This file is kept as a convenience shim so existing in-tree
|
||||
// imports (process/, http/, heartbeat.ts) don't need rewriting.
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
export {
|
||||
type RunProcessResult,
|
||||
runningProcesses,
|
||||
MAX_CAPTURE_BYTES,
|
||||
MAX_EXCERPT_BYTES,
|
||||
parseObject,
|
||||
asString,
|
||||
asNumber,
|
||||
asBoolean,
|
||||
asStringArray,
|
||||
parseJson,
|
||||
appendWithCap,
|
||||
resolvePathValue,
|
||||
renderTemplate,
|
||||
redactEnvForLogs,
|
||||
buildInvocationEnvForLogs,
|
||||
buildPaperclipEnv,
|
||||
defaultPathForPlatform,
|
||||
ensurePathInEnv,
|
||||
ensureAbsoluteDirectory,
|
||||
ensureCommandResolvable,
|
||||
resolveCommandForLogs,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import * as serverUtils from "@paperclipai/adapter-utils/server-utils";
|
||||
export type { RunProcessResult } from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
type BuildInvocationEnvForLogsOptions = {
|
||||
runtimeEnv?: NodeJS.ProcessEnv | Record<string, string>;
|
||||
includeRuntimeKeys?: string[];
|
||||
resolvedCommand?: string | null;
|
||||
resolvedCommandEnvKey?: string;
|
||||
};
|
||||
|
||||
export const runningProcesses: Map<string, { child: ChildProcess; graceSec: number }> =
|
||||
serverUtils.runningProcesses;
|
||||
export const MAX_CAPTURE_BYTES = serverUtils.MAX_CAPTURE_BYTES;
|
||||
export const MAX_EXCERPT_BYTES = serverUtils.MAX_EXCERPT_BYTES;
|
||||
export const parseObject = serverUtils.parseObject;
|
||||
export const asString = serverUtils.asString;
|
||||
export const asNumber = serverUtils.asNumber;
|
||||
export const asBoolean = serverUtils.asBoolean;
|
||||
export const asStringArray = serverUtils.asStringArray;
|
||||
export const parseJson = serverUtils.parseJson;
|
||||
export const appendWithCap = serverUtils.appendWithCap;
|
||||
export const resolvePathValue = serverUtils.resolvePathValue;
|
||||
export const renderTemplate = serverUtils.renderTemplate;
|
||||
export const redactEnvForLogs = serverUtils.redactEnvForLogs;
|
||||
export const buildPaperclipEnv = serverUtils.buildPaperclipEnv;
|
||||
export const defaultPathForPlatform = serverUtils.defaultPathForPlatform;
|
||||
export const ensurePathInEnv = serverUtils.ensurePathInEnv;
|
||||
export const ensureAbsoluteDirectory = serverUtils.ensureAbsoluteDirectory;
|
||||
export const ensureCommandResolvable = serverUtils.ensureCommandResolvable;
|
||||
export const resolveCommandForLogs = serverUtils.resolveCommandForLogs;
|
||||
|
||||
export function buildInvocationEnvForLogs(
|
||||
env: Record<string, string>,
|
||||
options: BuildInvocationEnvForLogsOptions = {},
|
||||
): Record<string, string> {
|
||||
const maybeBuildInvocationEnvForLogs = (
|
||||
serverUtils as typeof serverUtils & {
|
||||
buildInvocationEnvForLogs?: (
|
||||
env: Record<string, string>,
|
||||
options?: BuildInvocationEnvForLogsOptions,
|
||||
) => Record<string, string>;
|
||||
}
|
||||
).buildInvocationEnvForLogs;
|
||||
|
||||
if (typeof maybeBuildInvocationEnvForLogs === "function") {
|
||||
return maybeBuildInvocationEnvForLogs(env, options);
|
||||
}
|
||||
|
||||
const merged: Record<string, string> = { ...env };
|
||||
const runtimeEnv = options.runtimeEnv ?? {};
|
||||
|
||||
for (const key of options.includeRuntimeKeys ?? []) {
|
||||
if (key in merged) continue;
|
||||
const value = runtimeEnv[key];
|
||||
if (typeof value !== "string" || value.length === 0) continue;
|
||||
merged[key] = value;
|
||||
}
|
||||
|
||||
const resolvedCommand = options.resolvedCommand?.trim();
|
||||
if (resolvedCommand) {
|
||||
merged[options.resolvedCommandEnvKey ?? "PAPERCLIP_RESOLVED_COMMAND"] = resolvedCommand;
|
||||
}
|
||||
|
||||
return redactEnvForLogs(merged);
|
||||
}
|
||||
|
||||
// Re-export runChildProcess with the server's pino logger wired in.
|
||||
import { runChildProcess as _runChildProcess } from "@paperclipai/adapter-utils/server-utils";
|
||||
import type { RunProcessResult } from "@paperclipai/adapter-utils/server-utils";
|
||||
const _runChildProcess = serverUtils.runChildProcess;
|
||||
|
||||
export async function runChildProcess(
|
||||
runId: string,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue