From d607ca0089fe349b81f2913c564cbf98683066da Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 9 Apr 2026 08:11:23 -0500 Subject: [PATCH] Scope workspace link preflight to linked worktrees Co-Authored-By: Paperclip --- scripts/ensure-workspace-package-links.ts | 16 +++++++- .../src/__tests__/workspace-runtime.test.ts | 41 +++++++++++++++++++ server/src/services/workspace-runtime.ts | 13 +++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/scripts/ensure-workspace-package-links.ts b/scripts/ensure-workspace-package-links.ts index d8be419c..17a99909 100644 --- a/scripts/ensure-workspace-package-links.ts +++ b/scripts/ensure-workspace-package-links.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node --import tsx import fs from "node:fs/promises"; -import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; +import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import path from "node:path"; import { repoRoot } from "./dev-service-profile.ts"; @@ -43,6 +43,20 @@ function discoverWorkspacePackagePaths(rootDir: string): Map { return packagePaths; } +function isLinkedGitWorktreeCheckout(rootDir: string) { + const gitMetadataPath = path.join(rootDir, ".git"); + if (!existsSync(gitMetadataPath)) return false; + + const stat = lstatSync(gitMetadataPath); + if (!stat.isFile()) return false; + + return readFileSync(gitMetadataPath, "utf8").trimStart().startsWith("gitdir:"); +} + +if (!isLinkedGitWorktreeCheckout(repoRoot)) { + process.exit(0); +} + const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot); const workspaceDirs = Array.from( new Set( diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 5f4fc645..ff492ad2 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -200,6 +200,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { await fs.mkdir(expectedPackageDir, { recursive: true }); await fs.mkdir(stalePackageDir, { recursive: true }); await fs.mkdir(serverNodeModulesScopeDir, { recursive: true }); + await fs.writeFile(path.join(repoRoot, ".git"), "gitdir: /tmp/paperclip-main/.git/worktrees/runtime-links\n", "utf8"); await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8"); await fs.writeFile( path.join(repoRoot, "server", "package.json"), @@ -235,6 +236,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); await fs.mkdir(expectedPackageDir, { recursive: true }); await fs.mkdir(serverNodeModulesScopeDir, { recursive: true }); + await fs.writeFile(path.join(repoRoot, ".git"), "gitdir: /tmp/paperclip-main/.git/worktrees/runtime-links-current\n", "utf8"); await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8"); await fs.writeFile( path.join(repoRoot, "server", "package.json"), @@ -255,6 +257,45 @@ describe("ensureServerWorkspaceLinksCurrent", () => { await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); }); + + it("skips relinking outside linked git worktrees", async () => { + const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-non-worktree-")); + const staleRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-links-non-worktree-stale-")); + const serverNodeModulesScopeDir = path.join(repoRoot, "server", "node_modules", "@paperclipai"); + const expectedPackageDir = path.join(repoRoot, "packages", "db"); + const stalePackageDir = path.join(staleRoot, "db"); + + await fs.mkdir(path.join(repoRoot, ".git"), { recursive: true }); + await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); + await fs.mkdir(expectedPackageDir, { recursive: true }); + await fs.mkdir(stalePackageDir, { recursive: true }); + await fs.mkdir(serverNodeModulesScopeDir, { recursive: true }); + await fs.writeFile(path.join(repoRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n - server\n", "utf8"); + await fs.writeFile( + path.join(repoRoot, "server", "package.json"), + JSON.stringify({ + name: "@paperclipai/server", + dependencies: { + "@paperclipai/db": "workspace:*", + }, + }), + "utf8", + ); + await fs.writeFile( + path.join(expectedPackageDir, "package.json"), + JSON.stringify({ name: "@paperclipai/db" }), + "utf8", + ); + await fs.writeFile( + path.join(stalePackageDir, "package.json"), + JSON.stringify({ name: "@paperclipai/db" }), + "utf8", + ); + await fs.symlink(stalePackageDir, path.join(serverNodeModulesScopeDir, "db")); + + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); + expect(await fs.realpath(path.join(serverNodeModulesScopeDir, "db"))).toBe(await fs.realpath(stalePackageDir)); + }); }); describe("realizeExecutionWorkspace", () => { diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index fc75d0d5..c0447bcc 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -1,5 +1,5 @@ import { spawn, type ChildProcess } from "node:child_process"; -import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; +import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import fs from "node:fs/promises"; import net from "node:net"; import { createHash, randomUUID } from "node:crypto"; @@ -157,6 +157,16 @@ function findWorkspaceRoot(startCwd: string) { } } +function isLinkedGitWorktreeCheckout(rootDir: string) { + const gitMetadataPath = path.join(rootDir, ".git"); + if (!existsSync(gitMetadataPath)) return false; + + const stat = lstatSync(gitMetadataPath); + if (!stat.isFile()) return false; + + return readFileSync(gitMetadataPath, "utf8").trimStart().startsWith("gitdir:"); +} + function discoverWorkspacePackagePaths(rootDir: string): Map { const packagePaths = new Map(); const ignoredDirNames = new Set([".git", ".paperclip", "dist", "node_modules"]); @@ -228,6 +238,7 @@ export async function ensureServerWorkspaceLinksCurrent( ) { const workspaceRoot = findWorkspaceRoot(startCwd); if (!workspaceRoot) return; + if (!isLinkedGitWorktreeCheckout(workspaceRoot)) return; const mismatches = findServerWorkspaceLinkMismatches(workspaceRoot); if (mismatches.length === 0) return;