diff --git a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts index e1cea95ad..252a0b313 100644 --- a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts +++ b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts @@ -1,8 +1,9 @@ -import { basename, isAbsolute, relative, resolve, sep } from "node:path"; +import { basename, resolve } from "node:path"; import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages"; import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js"; +import { getRealpathAwareRelativePath } from "../utils/path.js"; import { deriveProjectGroupKey } from "./project-group-key.js"; import { deriveProjectKind, @@ -80,15 +81,8 @@ function deriveProjectRootPath(input: { ? parseGitRevParsePath(input.checkout.worktreeRoot) : null; if (!worktreeRoot) return input.checkout.mainRepoRoot; - const selectedPath = relative(resolve(worktreeRoot), resolve(input.cwd)); - if ( - !selectedPath || - selectedPath === "." || - selectedPath === ".." || - selectedPath.startsWith(`..${sep}`) || - isAbsolute(selectedPath) - ) { - return input.checkout.mainRepoRoot; - } - return resolve(input.checkout.mainRepoRoot, selectedPath); + const selectedPath = getRealpathAwareRelativePath(worktreeRoot, input.cwd); + return selectedPath + ? resolve(input.checkout.mainRepoRoot, selectedPath) + : input.checkout.mainRepoRoot; } diff --git a/packages/server/src/server/workspace-registry-bootstrap.test.ts b/packages/server/src/server/workspace-registry-bootstrap.test.ts index f4c3a1ca1..f0d37249f 100644 --- a/packages/server/src/server/workspace-registry-bootstrap.test.ts +++ b/packages/server/src/server/workspace-registry-bootstrap.test.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; @@ -456,6 +456,61 @@ describe("bootstrapWorkspaceRegistries", () => { ); }); + test.skipIf(process.platform === "win32")( + "maps a symlinked legacy subproject onto the main checkout", + async () => { + const physicalProject = path.join(GIT_WORKTREE, "packages", "app"); + const linkedProject = path.join(tmpDir, "app-link"); + mkdirSync(physicalProject, { recursive: true }); + symlinkSync(physicalProject, linkedProject, "dir"); + workspaceGitService = createNoopWorkspaceGitService({ + getCheckout: async (cwd) => ({ + cwd, + isGit: true, + currentBranch: "main", + remoteUrl: "git@github.com:acme/legacy-project.git", + worktreeRoot: GIT_WORKTREE, + isPaseoOwnedWorktree: false, + mainRepoRoot: GIT_PROJECT, + }), + }); + await agentStorage.initialize(); + await agentStorage.upsert({ + id: "linked-app-agent", + provider: "codex", + cwd: linkedProject, + createdAt: "2026-03-01T00:00:00.000Z", + updatedAt: "2026-03-02T00:00:00.000Z", + lastActivityAt: "2026-03-02T00:00:00.000Z", + lastUserMessageAt: null, + title: null, + labels: {}, + lastStatus: "idle", + lastModeId: null, + config: null, + runtimeInfo: { provider: "codex", sessionId: null }, + persistence: null, + archivedAt: null, + }); + + await bootstrapWorkspaceRegistries({ + paseoHome, + agentStorage, + projectRegistry, + workspaceRegistry, + workspaceGitService, + logger, + }); + + expect(await projectRegistry.list()).toEqual([ + expect.objectContaining({ + projectId: "remote:github.com/acme/legacy-project#subdir:packages/app", + rootPath: path.join(GIT_PROJECT, "packages", "app"), + }), + ]); + }, + ); + test("migrates cwd-only agents to the oldest existing same-cwd workspace", async () => { await projectRegistry.initialize(); await workspaceRegistry.initialize();