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:
@@ -121,14 +121,10 @@ import {
|
|||||||
} from "./agent/import-sessions.js";
|
} from "./agent/import-sessions.js";
|
||||||
import {
|
import {
|
||||||
checkoutLiteFromGitSnapshot,
|
checkoutLiteFromGitSnapshot,
|
||||||
classifyDirectoryForProjectMembership,
|
|
||||||
deriveWorkspaceDisplayName,
|
deriveWorkspaceDisplayName,
|
||||||
generateWorkspaceId,
|
|
||||||
} from "./workspace-registry-model.js";
|
} from "./workspace-registry-model.js";
|
||||||
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js";
|
||||||
import {
|
import {
|
||||||
createPersistedProjectRecord,
|
|
||||||
createPersistedWorkspaceRecord,
|
|
||||||
resolveProjectDisplayName,
|
resolveProjectDisplayName,
|
||||||
resolveWorkspaceDisplayName,
|
resolveWorkspaceDisplayName,
|
||||||
resolveWorkspaceName,
|
resolveWorkspaceName,
|
||||||
@@ -164,6 +160,10 @@ import {
|
|||||||
createGitMutationService,
|
createGitMutationService,
|
||||||
type GitMutationService,
|
type GitMutationService,
|
||||||
} from "./session/git-mutation/git-mutation-service.js";
|
} 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 { expandTilde } from "../utils/path.js";
|
||||||
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
|
import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
|
||||||
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
|
||||||
@@ -564,6 +564,7 @@ export class Session {
|
|||||||
private readonly generateWorkspaceName: typeof generateBranchNameFromFirstAgentContext;
|
private readonly generateWorkspaceName: typeof generateBranchNameFromFirstAgentContext;
|
||||||
private readonly workspaceGitService: WorkspaceGitService;
|
private readonly workspaceGitService: WorkspaceGitService;
|
||||||
private readonly gitMutation: GitMutationService;
|
private readonly gitMutation: GitMutationService;
|
||||||
|
private readonly workspaceProvisioning: WorkspaceProvisioningService;
|
||||||
private readonly daemonConfigStore: DaemonConfigStore;
|
private readonly daemonConfigStore: DaemonConfigStore;
|
||||||
private readonly mcpBaseUrl: string | null;
|
private readonly mcpBaseUrl: string | null;
|
||||||
private readonly pushTokenStore: PushTokenStore;
|
private readonly pushTokenStore: PushTokenStore;
|
||||||
@@ -700,6 +701,11 @@ export class Session {
|
|||||||
github: this.github,
|
github: this.github,
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
});
|
});
|
||||||
|
this.workspaceProvisioning = createWorkspaceProvisioningService({
|
||||||
|
workspaceRegistry: this.workspaceRegistry,
|
||||||
|
projectRegistry: this.projectRegistry,
|
||||||
|
workspaceGitService: this.workspaceGitService,
|
||||||
|
});
|
||||||
this.checkoutSession = new CheckoutSession({
|
this.checkoutSession = new CheckoutSession({
|
||||||
host: {
|
host: {
|
||||||
emit: (msg) => this.emit(msg),
|
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(
|
private async buildProjectPlacementForWorkspace(
|
||||||
workspace: PersistedWorkspaceRecord,
|
workspace: PersistedWorkspaceRecord,
|
||||||
projectRecord?: PersistedProjectRecord | null,
|
projectRecord?: PersistedProjectRecord | null,
|
||||||
@@ -2663,12 +2646,14 @@ export class Session {
|
|||||||
const createAgentConfig: AgentSessionConfig = createdWorktree
|
const createAgentConfig: AgentSessionConfig = createdWorktree
|
||||||
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
||||||
: config;
|
: config;
|
||||||
const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
|
const workspaceId = await this.workspaceProvisioning.resolveOrCreateWorkspaceIdForCreateAgent(
|
||||||
createdWorktree,
|
{
|
||||||
requestedWorkspaceId: msg.workspaceId,
|
createdWorktree,
|
||||||
cwd: createAgentConfig.cwd,
|
requestedWorkspaceId: msg.workspaceId,
|
||||||
initialTitle: workspacePromptTitle,
|
cwd: createAgentConfig.cwd,
|
||||||
});
|
initialTitle: workspacePromptTitle,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { snapshot, liveSnapshot } = await createAgentCommand(
|
const { snapshot, liveSnapshot } = await createAgentCommand(
|
||||||
{
|
{
|
||||||
@@ -2867,7 +2852,9 @@ export class Session {
|
|||||||
}
|
}
|
||||||
// An imported agent mints its own workspace; ownership is its workspaceId,
|
// An imported agent mints its own workspace; ownership is its workspaceId,
|
||||||
// never an existing same-cwd workspace resolved by path.
|
// 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({
|
const { snapshot, timelineSize } = await importProviderSession({
|
||||||
request: normalized,
|
request: normalized,
|
||||||
workspaceId: workspace.workspaceId,
|
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(
|
private buildProjectDescriptor(
|
||||||
project: PersistedProjectRecord,
|
project: PersistedProjectRecord,
|
||||||
): WorkspaceProjectDescriptorPayload {
|
): 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> {
|
private async unarchiveOwningWorkspaceForAgent(agentId: string): Promise<void> {
|
||||||
const record = await this.agentStorage.get(agentId);
|
const record = await this.agentStorage.get(agentId);
|
||||||
if (!record?.workspaceId) {
|
if (!record?.workspaceId) {
|
||||||
@@ -4524,7 +4334,7 @@ export class Session {
|
|||||||
await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
|
await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ensureWorkspaceRecordUnarchived(workspace);
|
await this.workspaceProvisioning.ensureWorkspaceRecordUnarchived(workspace);
|
||||||
await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
|
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(
|
private async createPaseoWorktree(
|
||||||
input: CreatePaseoWorktreeInput,
|
input: CreatePaseoWorktreeInput,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -5356,7 +5142,7 @@ export class Session {
|
|||||||
for (const workspaceRecord of await this.workspaceRegistry.list()) {
|
for (const workspaceRecord of await this.workspaceRegistry.list()) {
|
||||||
workspacesBefore.set(workspaceRecord.workspaceId, workspaceRecord);
|
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);
|
const project = await this.projectRegistry.get(workspace.projectId);
|
||||||
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
||||||
const descriptor = await this.describeWorkspaceRecord(workspace);
|
const descriptor = await this.describeWorkspaceRecord(workspace);
|
||||||
@@ -5441,7 +5227,7 @@ export class Session {
|
|||||||
for (const project of await this.projectRegistry.list()) {
|
for (const project of await this.projectRegistry.list()) {
|
||||||
projectsBefore.set(project.projectId, project);
|
projectsBefore.set(project.projectId, project);
|
||||||
}
|
}
|
||||||
const project = await this.findOrCreateProjectForDirectory(cwd);
|
const project = await this.workspaceProvisioning.findOrCreateProjectForDirectory(cwd);
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{
|
{
|
||||||
requestedCwd,
|
requestedCwd,
|
||||||
|
|||||||
+216
@@ -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);
|
||||||
|
});
|
||||||
+284
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user