refactor(server): extract workspace provisioning into a deep module (#1712)

* refactor(server): extract workspace provisioning into workspace-provisioning module

Move the "find-or-create a workspace & project for a directory" cluster out of
the 6.4k-line Session god class into session/workspace-provisioning/
behind an injected createWorkspaceProvisioningService(deps) port.

Nine methods (~215 LOC) — findOrCreateWorkspaceForDirectory,
resolveOrCreateWorkspaceIdForCreateAgent, createWorkspaceForDirectory,
findOrCreateProjectForDirectory, ensureWorkspaceRecordUnarchived, and the
private helpers resolveWorkspaceDirectory / findExactWorkspaceByDirectory /
reclassifyOrUnarchiveWorkspaceForDirectory / resolveProjectRecordForPlacement —
now live in one deep module with a 5-method interface. The session shed four
pure-function imports it no longer needs and delegates from five call sites.

The module's only dependencies are the workspace/project registries and the
git-service checkout port — no emit, clientActivity, or observer coupling — so
it is now unit-testable with real file-backed registries and a fake git port
(workspace-provisioning-service.test.ts), instead of only through a booted
Session. Behavior is unchanged: the existing open_project / import / create-agent
characterization suites stay green.

* refactor(server): address review — drop redundant spread property, test always-create

- Remove the dead `workspaceId: input.workspace.workspaceId` in
  reclassifyOrUnarchiveWorkspaceForDirectory (already spread via
  ...input.workspace; behavior-identical).
- Add a direct test pinning createWorkspaceForDirectory's always-mint-fresh
  contract (call twice for the same cwd → two distinct workspace ids), the
  property that distinguishes it from findOrCreateWorkspaceForDirectory.
This commit is contained in:
Mohamed Boudra
2026-06-25 03:36:14 +08:00
committed by GitHub
parent 83123987d7
commit 1b9543023e
3 changed files with 524 additions and 238 deletions

View File

@@ -121,14 +121,10 @@ import {
} from "./agent/import-sessions.js";
import {
checkoutLiteFromGitSnapshot,
classifyDirectoryForProjectMembership,
deriveWorkspaceDisplayName,
generateWorkspaceId,
} from "./workspace-registry-model.js";
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
resolveProjectDisplayName,
resolveWorkspaceDisplayName,
resolveWorkspaceName,
@@ -164,6 +160,10 @@ import {
createGitMutationService,
type GitMutationService,
} from "./session/git-mutation/git-mutation-service.js";
import {
createWorkspaceProvisioningService,
type WorkspaceProvisioningService,
} from "./session/workspace-provisioning/workspace-provisioning-service.js";
import { expandTilde } from "../utils/path.js";
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
@@ -564,6 +564,7 @@ export class Session {
private readonly generateWorkspaceName: typeof generateBranchNameFromFirstAgentContext;
private readonly workspaceGitService: WorkspaceGitService;
private readonly gitMutation: GitMutationService;
private readonly workspaceProvisioning: WorkspaceProvisioningService;
private readonly daemonConfigStore: DaemonConfigStore;
private readonly mcpBaseUrl: string | null;
private readonly pushTokenStore: PushTokenStore;
@@ -700,6 +701,11 @@ export class Session {
github: this.github,
logger: this.sessionLogger,
});
this.workspaceProvisioning = createWorkspaceProvisioningService({
workspaceRegistry: this.workspaceRegistry,
projectRegistry: this.projectRegistry,
workspaceGitService: this.workspaceGitService,
});
this.checkoutSession = new CheckoutSession({
host: {
emit: (msg) => this.emit(msg),
@@ -1459,29 +1465,6 @@ export class Session {
}
}
private async findExactWorkspaceByDirectory(
cwd: string,
options?: { refreshGit?: boolean },
): Promise<PersistedWorkspaceRecord | null> {
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
const workspaces = await this.workspaceRegistry.list();
return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
}
private async resolveWorkspaceDirectory(
cwd: string,
options?: { refreshGit?: boolean },
): Promise<string> {
const normalizedCwd = resolve(cwd);
if (options?.refreshGit === false) {
const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
}
const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
return resolve(checkout.worktreeRoot ?? normalizedCwd);
}
private async buildProjectPlacementForWorkspace(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
@@ -2663,12 +2646,14 @@ export class Session {
const createAgentConfig: AgentSessionConfig = createdWorktree
? { ...config, cwd: createdWorktree.worktree.worktreePath }
: config;
const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
createdWorktree,
requestedWorkspaceId: msg.workspaceId,
cwd: createAgentConfig.cwd,
initialTitle: workspacePromptTitle,
});
const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent(
{
createdWorktree,
requestedWorkspaceId: msg.workspaceId,
cwd: createAgentConfig.cwd,
initialTitle: workspacePromptTitle,
},
);
const { snapshot, liveSnapshot } = await createAgentCommand(
{
@@ -2867,7 +2852,9 @@ export class Session {
}
// An imported agent mints its own workspace; ownership is its workspaceId,
// never an existing same-cwd workspace resolved by path.
const workspace = await this.createWorkspaceForDirectory(normalized.cwd);
const workspace = await this.workspaceProvisioning.createWorkspaceForDirectory(
normalized.cwd,
);
const { snapshot, timelineSize } = await importProviderSession({
request: normalized,
workspaceId: workspace.workspaceId,
@@ -4314,108 +4301,6 @@ export class Session {
}
}
private async findOrCreateWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord> {
const inputCwd = resolve(cwd);
const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
const existingWorkspace = await this.findExactWorkspaceByDirectory(normalizedCwd, {
refreshGit: false,
});
if (existingWorkspace) {
if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
const timestamp = new Date().toISOString();
const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
isGit: false,
currentBranch: null,
remoteUrl: null,
repoRoot: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
});
const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
const projectRecord = await this.resolveProjectRecordForPlacement({
membership,
timestamp,
});
await this.projectRegistry.upsert(projectRecord);
const workspaceRecord = createPersistedWorkspaceRecord({
workspaceId: generateWorkspaceId(),
projectId: projectRecord.projectId,
cwd: inputCwd,
kind: membership.workspaceKind,
displayName: membership.workspaceDisplayName,
createdAt: timestamp,
updatedAt: timestamp,
});
await this.workspaceRegistry.upsert(workspaceRecord);
return workspaceRecord;
}
return this.reclassifyOrUnarchiveWorkspaceForDirectory({
workspace: existingWorkspace,
project: await this.projectRegistry.get(existingWorkspace.projectId),
cwd: normalizedCwd,
});
}
return this.createWorkspaceForDirectory(normalizedCwd);
}
private async resolveOrCreateWorkspaceIdForCreateAgent(input: {
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
requestedWorkspaceId?: string;
cwd: string;
initialTitle: string | null;
}): Promise<string> {
if (input.createdWorktree) {
return input.createdWorktree.workspace.workspaceId;
}
if (input.requestedWorkspaceId) {
return input.requestedWorkspaceId;
}
return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
}
private async createWorkspaceForDirectory(
cwd: string,
title?: string | null,
): Promise<PersistedWorkspaceRecord> {
const checkout = await this.workspaceGitService.getCheckout(cwd);
const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
const timestamp = new Date().toISOString();
const projectRecord = await this.resolveProjectRecordForPlacement({
membership,
timestamp,
});
await this.projectRegistry.upsert(projectRecord);
const workspaceRecord = createPersistedWorkspaceRecord({
workspaceId: generateWorkspaceId(),
projectId: projectRecord.projectId,
cwd,
kind: membership.workspaceKind,
displayName: membership.workspaceDisplayName,
title: title ?? null,
createdAt: timestamp,
updatedAt: timestamp,
});
await this.workspaceRegistry.upsert(workspaceRecord);
return workspaceRecord;
}
private async findOrCreateProjectForDirectory(cwd: string): Promise<PersistedProjectRecord> {
const normalizedCwd = resolve(cwd);
const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout });
const projectRecord = await this.resolveProjectRecordForPlacement({
membership,
timestamp: new Date().toISOString(),
});
await this.projectRegistry.upsert(projectRecord);
return projectRecord;
}
private buildProjectDescriptor(
project: PersistedProjectRecord,
): WorkspaceProjectDescriptorPayload {
@@ -4428,81 +4313,6 @@ export class Session {
};
}
private async reclassifyOrUnarchiveWorkspaceForDirectory(input: {
workspace: PersistedWorkspaceRecord;
project: PersistedProjectRecord | null;
cwd: string;
}): Promise<PersistedWorkspaceRecord> {
const checkout = await this.workspaceGitService.getCheckout(input.cwd);
const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
const timestamp = new Date().toISOString();
const projectRecord = await this.resolveProjectRecordForPlacement({
membership,
timestamp,
});
const projectId = projectRecord.projectId;
const kind = membership.workspaceKind;
const displayName = membership.workspaceDisplayName;
if (
input.workspace.projectId === projectId &&
input.workspace.kind === kind &&
input.workspace.displayName === displayName
) {
if (!input.project) {
await this.projectRegistry.upsert(projectRecord);
}
return this.ensureWorkspaceRecordUnarchived(input.workspace);
}
await this.projectRegistry.upsert(projectRecord);
const nextWorkspace = {
...input.workspace,
workspaceId: input.workspace.workspaceId,
projectId,
cwd: input.cwd,
kind,
displayName,
archivedAt: null,
updatedAt: timestamp,
};
await this.workspaceRegistry.upsert(nextWorkspace);
return nextWorkspace;
}
private async resolveProjectRecordForPlacement(input: {
membership: ReturnType<typeof classifyDirectoryForProjectMembership>;
timestamp: string;
}): Promise<PersistedProjectRecord> {
const rootPath = input.membership.projectRootPath;
const kind = input.membership.projectKind;
const projects = await this.projectRegistry.list();
const existingProject =
projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ??
projects.find((project) => project.rootPath === rootPath) ??
null;
if (!existingProject) {
return createPersistedProjectRecord({
projectId: input.membership.projectKey,
rootPath,
kind,
displayName: input.membership.projectName,
createdAt: input.timestamp,
updatedAt: input.timestamp,
});
}
return {
...existingProject,
rootPath,
kind,
archivedAt: null,
updatedAt: input.timestamp,
};
}
private async unarchiveOwningWorkspaceForAgent(agentId: string): Promise<void> {
const record = await this.agentStorage.get(agentId);
if (!record?.workspaceId) {
@@ -4524,7 +4334,7 @@ export class Session {
await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
}
await this.ensureWorkspaceRecordUnarchived(workspace);
await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
}
@@ -4583,30 +4393,6 @@ export class Session {
}
}
private async ensureWorkspaceRecordUnarchived(
workspace: PersistedWorkspaceRecord,
): Promise<PersistedWorkspaceRecord> {
const project = await this.projectRegistry.get(workspace.projectId);
if (!workspace.archivedAt && (!project || !project.archivedAt)) {
return workspace;
}
const timestamp = new Date().toISOString();
let unarchivedWorkspace = workspace;
if (workspace.archivedAt) {
unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp };
await this.workspaceRegistry.upsert(unarchivedWorkspace);
}
if (project?.archivedAt) {
await this.projectRegistry.upsert({
...project,
archivedAt: null,
updatedAt: timestamp,
});
}
return unarchivedWorkspace;
}
private async createPaseoWorktree(
input: CreatePaseoWorktreeInput,
options?: {
@@ -5356,7 +5142,7 @@ export class Session {
for (const workspaceRecord of await this.workspaceRegistry.list()) {
workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
}
const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
const workspace = await this.workspaceProvisioning.findOrCreateWorkspaceForDirectory(cwd);
const project = await this.projectRegistry.get(workspace.projectId);
await this.syncWorkspaceGitObserverForWorkspace(workspace);
const descriptor = await this.describeWorkspaceRecord(workspace);
@@ -5441,7 +5227,7 @@ export class Session {
for (const project of await this.projectRegistry.list()) {
projectsBefore.set(project.projectId, project);
}
const project = await this.findOrCreateProjectForDirectory(cwd);
const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
this.sessionLogger.info(
{
requestedCwd,

View File

@@ -0,0 +1,216 @@
import os from "node:os";
import path from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { afterEach, beforeEach, expect, test } from "vitest";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { createNoopWorkspaceGitService } from "../../test-utils/workspace-git-service-stub.js";
import {
FileBackedProjectRegistry,
FileBackedWorkspaceRegistry,
} from "../../workspace-registry.js";
import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js";
import {
createWorkspaceProvisioningService,
type WorkspaceProvisioningService,
} from "./workspace-provisioning-service.js";
// Real file-backed registries + a fake git-service port (the only dependency that
// shells out to git in production). No module mocks — the service is exercised
// through the same interface its callers in session.ts use.
const logger = createTestLogger();
const ARCHIVED_AT = "2026-01-01T00:00:00.000Z";
let tmpDir: string;
let gitRoots: Set<string>;
let workspaceRegistry: FileBackedWorkspaceRegistry;
let projectRegistry: FileBackedProjectRegistry;
let provisioning: WorkspaceProvisioningService;
function gitService() {
return createNoopWorkspaceGitService({
peekSnapshot: () => null,
getCheckout: async (cwd: string) => {
let worktreeRoot: string | null = null;
for (const root of gitRoots) {
if (
(cwd === root || cwd.startsWith(`${root}${path.sep}`)) &&
root.length > (worktreeRoot?.length ?? -1)
) {
worktreeRoot = root;
}
}
return {
cwd,
isGit: worktreeRoot !== null,
currentBranch: worktreeRoot ? "main" : null,
remoteUrl: null,
worktreeRoot,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
};
},
});
}
beforeEach(async () => {
tmpDir = mkdtempSync(path.join(os.tmpdir(), "workspace-provisioning-"));
gitRoots = new Set();
workspaceRegistry = new FileBackedWorkspaceRegistry(
path.join(tmpDir, "projects", "workspaces.json"),
logger,
);
projectRegistry = new FileBackedProjectRegistry(
path.join(tmpDir, "projects", "projects.json"),
logger,
);
await workspaceRegistry.initialize();
await projectRegistry.initialize();
provisioning = createWorkspaceProvisioningService({
workspaceRegistry,
projectRegistry,
workspaceGitService: gitService(),
});
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
test("fresh git repo creates a workspace at the canonical worktree root", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const workspace = await provisioning.findOrCreateWorkspaceForDirectory(repo);
expect(workspace.cwd).toBe(repo);
expect(await workspaceRegistry.list()).toHaveLength(1);
expect(await projectRegistry.list()).toHaveLength(1);
});
test("fresh non-git directory creates a directory workspace at the exact path", async () => {
const dir = path.join(tmpDir, "plain");
const workspace = await provisioning.findOrCreateWorkspaceForDirectory(dir);
expect(workspace.cwd).toBe(dir);
});
test("re-opening an active workspace by exact path returns the same record without duplicating", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const first = await provisioning.findOrCreateWorkspaceForDirectory(repo);
const second = await provisioning.findOrCreateWorkspaceForDirectory(repo);
expect(second.workspaceId).toBe(first.workspaceId);
expect(await workspaceRegistry.list()).toHaveLength(1);
});
test("re-opening an archived workspace by its exact path unarchives it and keeps the id", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const created = await provisioning.findOrCreateWorkspaceForDirectory(repo);
await workspaceRegistry.archive(created.workspaceId, ARCHIVED_AT);
const reopened = await provisioning.findOrCreateWorkspaceForDirectory(repo);
expect(reopened.workspaceId).toBe(created.workspaceId);
expect(reopened.archivedAt).toBeNull();
});
test("opening a subpath of an archived git workspace mints a fresh workspace at the exact subpath", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const canonical = await provisioning.findOrCreateWorkspaceForDirectory(repo);
await workspaceRegistry.archive(canonical.workspaceId, ARCHIVED_AT);
const sub = path.join(repo, "packages", "app");
const fresh = await provisioning.findOrCreateWorkspaceForDirectory(sub);
expect(fresh.cwd).toBe(sub);
expect(fresh.workspaceId).not.toBe(canonical.workspaceId);
expect((await workspaceRegistry.get(canonical.workspaceId))?.archivedAt).toBe(ARCHIVED_AT);
});
test("ensureWorkspaceRecordUnarchived clears archivedAt on the workspace and its project", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const created = await provisioning.findOrCreateWorkspaceForDirectory(repo);
await projectRegistry.archive(created.projectId, ARCHIVED_AT);
const unarchived = await provisioning.ensureWorkspaceRecordUnarchived({
...created,
archivedAt: ARCHIVED_AT,
});
expect(unarchived.archivedAt).toBeNull();
expect((await workspaceRegistry.get(created.workspaceId))?.archivedAt).toBeNull();
expect((await projectRegistry.get(created.projectId))?.archivedAt).toBeNull();
});
test("resolveOrCreateWorkspaceIdForCreateAgent returns a created worktree's id without touching the registry", async () => {
// The branch only reads workspace.workspaceId off the worktree result.
const createdWorktree = {
workspace: { workspaceId: "ws-from-worktree" },
} as unknown as CreatePaseoWorktreeWorkflowResult;
const id = await provisioning.resolveOrCreateWorkspaceIdForCreateAgent({
createdWorktree,
cwd: path.join(tmpDir, "x"),
initialTitle: null,
});
expect(id).toBe("ws-from-worktree");
expect(await workspaceRegistry.list()).toHaveLength(0);
});
test("resolveOrCreateWorkspaceIdForCreateAgent honors an explicitly requested workspace id", async () => {
const id = await provisioning.resolveOrCreateWorkspaceIdForCreateAgent({
createdWorktree: null,
requestedWorkspaceId: "ws-requested",
cwd: path.join(tmpDir, "x"),
initialTitle: null,
});
expect(id).toBe("ws-requested");
expect(await workspaceRegistry.list()).toHaveLength(0);
});
test("resolveOrCreateWorkspaceIdForCreateAgent creates a titled workspace when nothing is provided", async () => {
const dir = path.join(tmpDir, "plain");
const id = await provisioning.resolveOrCreateWorkspaceIdForCreateAgent({
createdWorktree: null,
cwd: dir,
initialTitle: "My Title",
});
const created = await workspaceRegistry.get(id);
expect(created?.cwd).toBe(dir);
expect(created?.title).toBe("My Title");
});
test("createWorkspaceForDirectory always mints a fresh workspace even when one already occupies the cwd", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const first = await provisioning.createWorkspaceForDirectory(repo);
const second = await provisioning.createWorkspaceForDirectory(repo);
expect(second.workspaceId).not.toBe(first.workspaceId);
expect(await workspaceRegistry.list()).toHaveLength(2);
});
test("findOrCreateProjectForDirectory reuses the active project for the same root", async () => {
const repo = path.join(tmpDir, "repo");
gitRoots.add(repo);
const first = await provisioning.findOrCreateProjectForDirectory(repo);
const second = await provisioning.findOrCreateProjectForDirectory(path.join(repo, "sub"));
expect(second.projectId).toBe(first.projectId);
expect(await projectRegistry.list()).toHaveLength(1);
});

View File

@@ -0,0 +1,284 @@
import { resolve } from "node:path";
import {
checkoutLiteFromGitSnapshot,
classifyDirectoryForProjectMembership,
generateWorkspaceId,
} from "../../workspace-registry-model.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
type PersistedProjectRecord,
type PersistedWorkspaceRecord,
type ProjectRegistry,
type WorkspaceRegistry,
} from "../../workspace-registry.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js";
/**
* Resolves which workspace and project records a directory belongs to, creating,
* reclassifying, or unarchiving them as needed. Every path that needs a workspace
* for a cwd — opening a project, importing an agent, creating an agent, restoring
* an archived worktree — funnels through this one module, so the
* classify → resolve-project → persist → unarchive sequence (and the
* archived-reopen-at-a-different-path and reclassify-vs-unarchive special cases)
* lives in a single place instead of being smeared across the session.
*
* Read-only path resolution (no create/persist) lives in resolve-workspace-id-for-path.ts;
* this module owns the create-and-persist side.
*/
export interface ResolveOrCreateWorkspaceIdInput {
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
requestedWorkspaceId?: string;
cwd: string;
initialTitle: string | null;
}
export interface WorkspaceProvisioningService {
findOrCreateWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord>;
resolveOrCreateWorkspaceIdForCreateAgent(input: ResolveOrCreateWorkspaceIdInput): Promise<string>;
createWorkspaceForDirectory(
cwd: string,
title?: string | null,
): Promise<PersistedWorkspaceRecord>;
findOrCreateProjectForDirectory(cwd: string): Promise<PersistedProjectRecord>;
ensureWorkspaceRecordUnarchived(
workspace: PersistedWorkspaceRecord,
): Promise<PersistedWorkspaceRecord>;
}
export function createWorkspaceProvisioningService(deps: {
workspaceRegistry: WorkspaceRegistry;
projectRegistry: ProjectRegistry;
workspaceGitService: Pick<WorkspaceGitService, "getCheckout" | "peekSnapshot">;
}): WorkspaceProvisioningService {
const { workspaceRegistry, projectRegistry, workspaceGitService } = deps;
async function resolveWorkspaceDirectory(
cwd: string,
options?: { refreshGit?: boolean },
): Promise<string> {
const normalizedCwd = resolve(cwd);
if (options?.refreshGit === false) {
const snapshot = workspaceGitService.peekSnapshot(normalizedCwd);
return resolve(snapshot?.git.repoRoot ?? normalizedCwd);
}
const checkout = await workspaceGitService.getCheckout(normalizedCwd);
return resolve(checkout.worktreeRoot ?? normalizedCwd);
}
async function findExactWorkspaceByDirectory(
cwd: string,
options?: { refreshGit?: boolean },
): Promise<PersistedWorkspaceRecord | null> {
const normalizedCwd = await resolveWorkspaceDirectory(cwd, options);
const workspaces = await workspaceRegistry.list();
return workspaces.find((workspace) => workspace.cwd === normalizedCwd) ?? null;
}
async function resolveProjectRecordForPlacement(input: {
membership: ReturnType<typeof classifyDirectoryForProjectMembership>;
timestamp: string;
}): Promise<PersistedProjectRecord> {
const rootPath = input.membership.projectRootPath;
const kind = input.membership.projectKind;
const projects = await projectRegistry.list();
const existingProject =
projects.find((project) => !project.archivedAt && project.rootPath === rootPath) ??
projects.find((project) => project.rootPath === rootPath) ??
null;
if (!existingProject) {
return createPersistedProjectRecord({
projectId: input.membership.projectKey,
rootPath,
kind,
displayName: input.membership.projectName,
createdAt: input.timestamp,
updatedAt: input.timestamp,
});
}
return {
...existingProject,
rootPath,
kind,
archivedAt: null,
updatedAt: input.timestamp,
};
}
async function reclassifyOrUnarchiveWorkspaceForDirectory(input: {
workspace: PersistedWorkspaceRecord;
project: PersistedProjectRecord | null;
cwd: string;
}): Promise<PersistedWorkspaceRecord> {
const checkout = await workspaceGitService.getCheckout(input.cwd);
const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
const timestamp = new Date().toISOString();
const projectRecord = await resolveProjectRecordForPlacement({
membership,
timestamp,
});
const projectId = projectRecord.projectId;
const kind = membership.workspaceKind;
const displayName = membership.workspaceDisplayName;
if (
input.workspace.projectId === projectId &&
input.workspace.kind === kind &&
input.workspace.displayName === displayName
) {
if (!input.project) {
await projectRegistry.upsert(projectRecord);
}
return ensureWorkspaceRecordUnarchived(input.workspace);
}
await projectRegistry.upsert(projectRecord);
const nextWorkspace = {
...input.workspace,
projectId,
cwd: input.cwd,
kind,
displayName,
archivedAt: null,
updatedAt: timestamp,
};
await workspaceRegistry.upsert(nextWorkspace);
return nextWorkspace;
}
async function findOrCreateWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord> {
const inputCwd = resolve(cwd);
const normalizedCwd = await resolveWorkspaceDirectory(cwd);
const existingWorkspace = await findExactWorkspaceByDirectory(normalizedCwd, {
refreshGit: false,
});
if (existingWorkspace) {
if (existingWorkspace.archivedAt && inputCwd !== normalizedCwd) {
const timestamp = new Date().toISOString();
const checkout = checkoutLiteFromGitSnapshot(inputCwd, {
isGit: false,
currentBranch: null,
remoteUrl: null,
repoRoot: null,
isPaseoOwnedWorktree: false,
mainRepoRoot: null,
});
const membership = classifyDirectoryForProjectMembership({ cwd: inputCwd, checkout });
const projectRecord = await resolveProjectRecordForPlacement({
membership,
timestamp,
});
await projectRegistry.upsert(projectRecord);
const workspaceRecord = createPersistedWorkspaceRecord({
workspaceId: generateWorkspaceId(),
projectId: projectRecord.projectId,
cwd: inputCwd,
kind: membership.workspaceKind,
displayName: membership.workspaceDisplayName,
createdAt: timestamp,
updatedAt: timestamp,
});
await workspaceRegistry.upsert(workspaceRecord);
return workspaceRecord;
}
return reclassifyOrUnarchiveWorkspaceForDirectory({
workspace: existingWorkspace,
project: await projectRegistry.get(existingWorkspace.projectId),
cwd: normalizedCwd,
});
}
return createWorkspaceForDirectory(normalizedCwd);
}
async function resolveOrCreateWorkspaceIdForCreateAgent(
input: ResolveOrCreateWorkspaceIdInput,
): Promise<string> {
if (input.createdWorktree) {
return input.createdWorktree.workspace.workspaceId;
}
if (input.requestedWorkspaceId) {
return input.requestedWorkspaceId;
}
return (await createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
}
async function createWorkspaceForDirectory(
cwd: string,
title?: string | null,
): Promise<PersistedWorkspaceRecord> {
const checkout = await workspaceGitService.getCheckout(cwd);
const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
const timestamp = new Date().toISOString();
const projectRecord = await resolveProjectRecordForPlacement({
membership,
timestamp,
});
await projectRegistry.upsert(projectRecord);
const workspaceRecord = createPersistedWorkspaceRecord({
workspaceId: generateWorkspaceId(),
projectId: projectRecord.projectId,
cwd,
kind: membership.workspaceKind,
displayName: membership.workspaceDisplayName,
title: title ?? null,
createdAt: timestamp,
updatedAt: timestamp,
});
await workspaceRegistry.upsert(workspaceRecord);
return workspaceRecord;
}
async function findOrCreateProjectForDirectory(cwd: string): Promise<PersistedProjectRecord> {
const normalizedCwd = resolve(cwd);
const checkout = await workspaceGitService.getCheckout(normalizedCwd);
const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout });
const projectRecord = await resolveProjectRecordForPlacement({
membership,
timestamp: new Date().toISOString(),
});
await projectRegistry.upsert(projectRecord);
return projectRecord;
}
async function ensureWorkspaceRecordUnarchived(
workspace: PersistedWorkspaceRecord,
): Promise<PersistedWorkspaceRecord> {
const project = await projectRegistry.get(workspace.projectId);
if (!workspace.archivedAt && (!project || !project.archivedAt)) {
return workspace;
}
const timestamp = new Date().toISOString();
let unarchivedWorkspace = workspace;
if (workspace.archivedAt) {
unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp };
await workspaceRegistry.upsert(unarchivedWorkspace);
}
if (project?.archivedAt) {
await projectRegistry.upsert({
...project,
archivedAt: null,
updatedAt: timestamp,
});
}
return unarchivedWorkspace;
}
return {
findOrCreateWorkspaceForDirectory,
resolveOrCreateWorkspaceIdForCreateAgent,
createWorkspaceForDirectory,
findOrCreateProjectForDirectory,
ensureWorkspaceRecordUnarchived,
};
}