Phase 3a: project/workspace DB cutover + legacy JSON import

Bootstrap now opens PGlite and uses DB-backed project/workspace
registries as the sole runtime authority. Legacy JSON files are
imported transactionally on first startup; restart is idempotent.

Key changes:
- bootstrap.ts: DB registries replace file-backed, DB lifecycle
  managed on startup/shutdown/error
- workspace-registry-bootstrap.ts: fixed FK write order (project
  before workspace), uses list() instead of existsOnDisk()
- New transactional import module with rollback safety
- Bootstrap smoke tests for import, idempotence, and reconciliation

Audit-verified: authority cutover (13/13 checks), boundary (10/10).
This commit is contained in:
Mohamed Boudra
2026-03-25 20:39:07 +07:00
parent 7f11c0c64c
commit 307a6e85f9
7 changed files with 627 additions and 31 deletions
@@ -1,5 +1,6 @@
import os from "node:os";
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { Writable } from "node:stream";
import pino from "pino";
@@ -8,6 +9,8 @@ import { afterEach, describe, expect, test, vi } from "vitest";
import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { createTestAgentClients } from "./test-utils/fake-agent-client.js";
import { openPaseoDatabase } from "./db/pglite-database.js";
import { projects, workspaces } from "./db/schema.js";
describe("paseo daemon bootstrap", () => {
afterEach(() => {
@@ -182,4 +185,216 @@ describe("paseo daemon bootstrap", () => {
await rm(staticDir, { recursive: true, force: true });
}
});
test("imports legacy project and workspace JSON into the DB on first bootstrap", async () => {
const { config, cleanup } = await createBootstrapConfig();
writeLegacyProjectWorkspaceJson(config.paseoHome, {
projects: [
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
workspaces: [
{
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
});
const daemon = await createPaseoDaemon(config, pino({ level: "silent" }));
try {
await daemon.start();
await daemon.stop();
const database = await openPaseoDatabase(path.join(config.paseoHome, "db"));
try {
expect(await database.db.select().from(projects)).toEqual([
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
expect(await database.db.select().from(workspaces)).toEqual([
{
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
} finally {
await database.close();
}
} finally {
await cleanup();
}
});
test("does not duplicate imported legacy JSON across daemon restarts", async () => {
const { config, cleanup } = await createBootstrapConfig();
writeLegacyProjectWorkspaceJson(config.paseoHome, {
projects: [
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
workspaces: [
{
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
});
try {
const firstDaemon = await createPaseoDaemon(config, pino({ level: "silent" }));
await firstDaemon.start();
await firstDaemon.stop();
const secondDaemon = await createPaseoDaemon(config, pino({ level: "silent" }));
await secondDaemon.start();
await secondDaemon.stop();
const database = await openPaseoDatabase(path.join(config.paseoHome, "db"));
try {
expect(await database.db.select().from(projects)).toHaveLength(1);
expect(await database.db.select().from(workspaces)).toHaveLength(1);
} finally {
await database.close();
}
} finally {
await cleanup();
}
});
test("reconciles workspace records into the DB without recreating legacy JSON registry files", async () => {
const { config, cleanup } = await createBootstrapConfig();
const agentStorageDir = path.join(config.paseoHome, "agents");
mkdirSync(agentStorageDir, { recursive: true });
const storageBucket = path.join(agentStorageDir, "tmp-db-only-project");
mkdirSync(storageBucket, { recursive: true });
writeFileSync(
path.join(storageBucket, "agent-1.json"),
JSON.stringify(
{
id: "agent-1",
provider: "codex",
cwd: "/tmp/db-only-project",
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,
},
null,
2,
),
"utf8",
);
try {
const daemon = await createPaseoDaemon(config, pino({ level: "silent" }));
await daemon.start();
await daemon.stop();
const database = await openPaseoDatabase(path.join(config.paseoHome, "db"));
try {
expect(await database.db.select().from(projects)).toHaveLength(1);
expect(await database.db.select().from(workspaces)).toHaveLength(1);
} finally {
await database.close();
}
expect(existsSync(path.join(config.paseoHome, "projects", "projects.json"))).toBe(false);
expect(existsSync(path.join(config.paseoHome, "projects", "workspaces.json"))).toBe(false);
} finally {
await cleanup();
}
});
});
async function createBootstrapConfig(): Promise<{
config: PaseoDaemonConfig;
cleanup: () => Promise<void>;
}> {
const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-bootstrap-db-"));
const paseoHome = path.join(paseoHomeRoot, ".paseo");
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
await mkdir(paseoHome, { recursive: true });
return {
config: {
listen: "127.0.0.1:0",
paseoHome,
corsAllowedOrigins: [],
allowedHosts: true,
mcpEnabled: false,
staticDir,
mcpDebug: false,
agentClients: createTestAgentClients(),
agentStoragePath: path.join(paseoHome, "agents"),
relayEnabled: false,
appBaseUrl: "https://app.paseo.sh",
openai: undefined,
speech: undefined,
},
cleanup: async () => {
await rm(paseoHomeRoot, { recursive: true, force: true });
await rm(staticDir, { recursive: true, force: true });
},
};
}
function writeLegacyProjectWorkspaceJson(
paseoHome: string,
input: {
projects: unknown[];
workspaces: unknown[];
},
): void {
const projectsDir = path.join(paseoHome, "projects");
mkdirSync(projectsDir, { recursive: true });
writeFileSync(path.join(projectsDir, "projects.json"), JSON.stringify(input.projects, null, 2), "utf8");
writeFileSync(path.join(projectsDir, "workspaces.json"), JSON.stringify(input.workspaces, null, 2), "utf8");
}
+17 -9
View File
@@ -88,7 +88,10 @@ import { AgentStorage } from "./agent/agent-storage.js";
import { createAgentMcpServer } from "./agent/mcp-server.js";
import { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
import { DbProjectRegistry } from "./db/db-project-registry.js";
import { DbWorkspaceRegistry } from "./db/db-workspace-registry.js";
import { importLegacyProjectWorkspaceJson } from "./db/legacy-project-workspace-import.js";
import { openPaseoDatabase, type PaseoDatabaseHandle } from "./db/pglite-database.js";
import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js";
import { createConnectionOfferV2, encodeOfferToFragmentUrl } from "./connection-offer.js";
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
@@ -196,6 +199,7 @@ export async function createPaseoDaemon(
const pidLockMode = config.pidLock?.mode ?? "self";
const pidLockOwnerPid = config.pidLock?.ownerPid;
const ownsPidLock = pidLockMode === "self";
let database: PaseoDatabaseHandle | null = null;
// Acquire PID lock before expensive bootstrap work so duplicate starts fail immediately.
if (ownsPidLock) {
@@ -356,14 +360,6 @@ export async function createPaseoDaemon(
const httpServer = createHTTPServer(app);
const agentStorage = new AgentStorage(config.agentStoragePath, logger);
const projectRegistry = new FileBackedProjectRegistry(
path.join(config.paseoHome, "projects", "projects.json"),
logger,
);
const workspaceRegistry = new FileBackedWorkspaceRegistry(
path.join(config.paseoHome, "projects", "workspaces.json"),
logger,
);
const agentManager = new AgentManager({
clients: {
...createAllClients(logger, {
@@ -378,6 +374,16 @@ export async function createPaseoDaemon(
const terminalManager = createTerminalManager();
await agentStorage.initialize();
logger.info({ elapsed: elapsed() }, "Agent storage initialized");
database = await openPaseoDatabase(path.join(config.paseoHome, "db"));
logger.info({ elapsed: elapsed() }, "Paseo database opened");
const projectRegistry = new DbProjectRegistry(database.db);
const workspaceRegistry = new DbWorkspaceRegistry(database.db);
await importLegacyProjectWorkspaceJson({
db: database.db,
paseoHome: config.paseoHome,
logger,
});
logger.info({ elapsed: elapsed() }, "Legacy project/workspace import checked");
await bootstrapWorkspaceRegistries({
paseoHome: config.paseoHome,
agentStorage,
@@ -717,6 +723,7 @@ export async function createPaseoDaemon(
if (voiceMcpBridgeManager) {
await voiceMcpBridgeManager.stop().catch(() => undefined);
}
await database?.close().catch(() => undefined);
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
@@ -742,6 +749,7 @@ export async function createPaseoDaemon(
getListenTarget: () => boundListenTarget,
};
} catch (err) {
await database?.close().catch(() => undefined);
if (ownsPidLock) {
await releasePidLock(config.paseoHome, {
ownerPid: pidLockOwnerPid,
@@ -0,0 +1,191 @@
import os from "node:os";
import path from "node:path";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { eq } from "drizzle-orm";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { openPaseoDatabase, type PaseoDatabaseHandle } from "./pglite-database.js";
import { importLegacyProjectWorkspaceJson } from "./legacy-project-workspace-import.js";
import { projects, workspaces } from "./schema.js";
describe("importLegacyProjectWorkspaceJson", () => {
let tmpDir: string;
let paseoHome: string;
let dbDir: string;
let database: PaseoDatabaseHandle;
beforeEach(async () => {
tmpDir = mkdtempSync(path.join(os.tmpdir(), "paseo-legacy-import-"));
paseoHome = path.join(tmpDir, ".paseo");
dbDir = path.join(paseoHome, "db");
mkdirSync(paseoHome, { recursive: true });
database = await openPaseoDatabase(dbDir);
});
afterEach(async () => {
await database?.close();
rmSync(tmpDir, { recursive: true, force: true });
});
test("imports legacy projects and workspaces once when the DB is empty", async () => {
writeLegacyJson({
paseoHome,
projectsJson: [
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
workspacesJson: [
{
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
});
const result = await importLegacyProjectWorkspaceJson({
db: database.db,
paseoHome,
logger: createTestLogger(),
});
expect(result).toEqual({
status: "imported",
importedProjects: 1,
importedWorkspaces: 1,
});
expect(await database.db.select().from(projects)).toEqual([
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
expect(await database.db.select().from(workspaces)).toEqual([
{
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
});
test("skips import when the DB already has project or workspace data", async () => {
await database.db.insert(projects).values({
projectId: "existing-project",
rootPath: "/tmp/existing-project",
kind: "git",
displayName: "Existing Project",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-01T00:00:00.000Z",
archivedAt: null,
});
writeLegacyJson({
paseoHome,
projectsJson: [
{
projectId: "legacy-project",
rootPath: "/tmp/legacy-project",
kind: "git",
displayName: "Legacy Project",
createdAt: "2026-03-02T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
workspacesJson: [],
});
const result = await importLegacyProjectWorkspaceJson({
db: database.db,
paseoHome,
logger: createTestLogger(),
});
expect(result).toEqual({
status: "skipped",
reason: "database-not-empty",
});
expect(
await database.db.select().from(projects).where(eq(projects.projectId, "legacy-project")),
).toEqual([]);
});
test("rolls back the whole import when workspace insertion fails", async () => {
writeLegacyJson({
paseoHome,
projectsJson: [
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
workspacesJson: [
{
workspaceId: "workspace-1",
projectId: "missing-project",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
],
});
await expect(
importLegacyProjectWorkspaceJson({
db: database.db,
paseoHome,
logger: createTestLogger(),
}),
).rejects.toThrow();
expect(await database.db.select().from(projects)).toEqual([]);
expect(await database.db.select().from(workspaces)).toEqual([]);
});
});
function writeLegacyJson(input: {
paseoHome: string;
projectsJson: unknown[];
workspacesJson: unknown[];
}): void {
const projectsPath = path.join(input.paseoHome, "projects", "projects.json");
const workspacesPath = path.join(input.paseoHome, "projects", "workspaces.json");
mkdirSync(path.dirname(projectsPath), { recursive: true });
writeFileSync(projectsPath, JSON.stringify(input.projectsJson, null, 2), { encoding: "utf8", flag: "w" });
writeFileSync(workspacesPath, JSON.stringify(input.workspacesJson, null, 2), {
encoding: "utf8",
flag: "w",
});
}
@@ -0,0 +1,109 @@
import path from "node:path";
import { promises as fs } from "node:fs";
import { count } from "drizzle-orm";
import type { Logger } from "pino";
import {
parsePersistedProjectRecords,
parsePersistedWorkspaceRecords,
} from "../workspace-registry.js";
import type { PaseoDatabaseHandle } from "./pglite-database.js";
import { projects, workspaces } from "./schema.js";
export type LegacyProjectWorkspaceImportResult =
| {
status: "imported";
importedProjects: number;
importedWorkspaces: number;
}
| {
status: "skipped";
reason: "database-not-empty" | "no-legacy-files";
};
export async function importLegacyProjectWorkspaceJson(options: {
db: PaseoDatabaseHandle["db"];
paseoHome: string;
logger: Logger;
}): Promise<LegacyProjectWorkspaceImportResult> {
const projectsPath = path.join(options.paseoHome, "projects", "projects.json");
const workspacesPath = path.join(options.paseoHome, "projects", "workspaces.json");
const [projectRows, workspaceRows, databaseHasRows] = await Promise.all([
readLegacyProjects(projectsPath),
readLegacyWorkspaces(workspacesPath),
hasAnyProjectWorkspaceRows(options.db),
]);
if (databaseHasRows) {
options.logger.info("Skipping legacy project/workspace JSON import because the DB is not empty");
return {
status: "skipped",
reason: "database-not-empty",
};
}
if (projectRows.length === 0 && workspaceRows.length === 0) {
options.logger.info("Skipping legacy project/workspace JSON import because no legacy files exist");
return {
status: "skipped",
reason: "no-legacy-files",
};
}
await options.db.transaction(async (tx) => {
if (projectRows.length > 0) {
await tx.insert(projects).values(projectRows);
}
if (workspaceRows.length > 0) {
await tx.insert(workspaces).values(workspaceRows);
}
});
options.logger.info(
{
importedProjects: projectRows.length,
importedWorkspaces: workspaceRows.length,
},
"Imported legacy project/workspace JSON into the database",
);
return {
status: "imported",
importedProjects: projectRows.length,
importedWorkspaces: workspaceRows.length,
};
}
async function readLegacyProjects(filePath: string) {
const raw = await readOptionalJsonFile(filePath);
return raw ? parsePersistedProjectRecords(raw) : [];
}
async function readLegacyWorkspaces(filePath: string) {
const raw = await readOptionalJsonFile(filePath);
return raw ? parsePersistedWorkspaceRecords(raw) : [];
}
async function readOptionalJsonFile(filePath: string): Promise<unknown | null> {
try {
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
return null;
}
throw error;
}
}
async function hasAnyProjectWorkspaceRows(db: PaseoDatabaseHandle["db"]): Promise<boolean> {
const [projectCountRows, workspaceCountRows] = await Promise.all([
db.select({ count: count() }).from(projects),
db.select({ count: count() }).from(workspaces),
]);
const projectCount = projectCountRows[0]?.count ?? 0;
const workspaceCount = workspaceCountRows[0]?.count ?? 0;
return projectCount > 0 || workspaceCount > 0;
}
@@ -1,6 +1,6 @@
import os from "node:os";
import path from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
@@ -8,6 +8,9 @@ import { createTestLogger } from "../test-utils/test-logger.js";
import { AgentStorage } from "./agent/agent-storage.js";
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { openPaseoDatabase } from "./db/pglite-database.js";
import { DbProjectRegistry } from "./db/db-project-registry.js";
import { DbWorkspaceRegistry } from "./db/db-workspace-registry.js";
describe("bootstrapWorkspaceRegistries", () => {
let tmpDir: string;
@@ -164,4 +167,66 @@ describe("bootstrapWorkspaceRegistries", () => {
expect(await workspaceRegistry.list()).toHaveLength(1);
expect((await workspaceRegistry.list())[0]?.workspaceId).toBe("/tmp/existing");
});
test("materializes into DB-backed registries when the database is empty", async () => {
mkdirSync(paseoHome, { recursive: true });
const database = await openPaseoDatabase(path.join(paseoHome, "db"));
const dbProjectRegistry = new DbProjectRegistry(database.db);
const dbWorkspaceRegistry = new DbWorkspaceRegistry(database.db);
try {
await agentStorage.initialize();
await agentStorage.upsert({
id: "agent-1",
provider: "codex",
cwd: "/tmp/non-git-project",
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: dbProjectRegistry,
workspaceRegistry: dbWorkspaceRegistry,
logger,
});
expect(await dbProjectRegistry.list()).toEqual([
{
projectId: "/tmp/non-git-project",
rootPath: "/tmp/non-git-project",
kind: "non_git",
displayName: "non-git-project",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
expect(await dbWorkspaceRegistry.list()).toEqual([
{
workspaceId: "/tmp/non-git-project",
projectId: "/tmp/non-git-project",
cwd: "/tmp/non-git-project",
kind: "directory",
displayName: "non-git-project",
createdAt: "2026-03-01T00:00:00.000Z",
updatedAt: "2026-03-02T00:00:00.000Z",
archivedAt: null,
},
]);
} finally {
await database.close();
}
});
});
@@ -54,14 +54,14 @@ export async function bootstrapWorkspaceRegistries(options: {
workspaceRegistry: WorkspaceRegistry;
logger: Logger;
}): Promise<void> {
const [projectsExists, workspacesExists] = await Promise.all([
options.projectRegistry.existsOnDisk(),
options.workspaceRegistry.existsOnDisk(),
]);
await Promise.all([options.projectRegistry.initialize(), options.workspaceRegistry.initialize()]);
if (projectsExists && workspacesExists) {
const [existingProjects, existingWorkspaces] = await Promise.all([
options.projectRegistry.list(),
options.workspaceRegistry.list(),
]);
if (existingProjects.length > 0 || existingWorkspaces.length > 0) {
return;
}
@@ -92,21 +92,6 @@ export async function bootstrapWorkspaceRegistries(options: {
const createdAt = workspaceCreatedAt ?? new Date().toISOString();
const updatedAt = workspaceUpdatedAt ?? createdAt;
await options.workspaceRegistry.upsert(
createPersistedWorkspaceRecord({
workspaceId,
projectId: placement.projectKey,
cwd: workspaceId,
kind: deriveWorkspaceKind(placement.checkout),
displayName: deriveWorkspaceDisplayName({
cwd: workspaceId,
checkout: placement.checkout,
}),
createdAt,
updatedAt,
}),
);
const existingProjectRange = projectRanges.get(placement.projectKey) ?? {
createdAt: null,
updatedAt: null,
@@ -128,6 +113,21 @@ export async function bootstrapWorkspaceRegistries(options: {
updatedAt: existingProjectRange.updatedAt ?? updatedAt,
}),
);
await options.workspaceRegistry.upsert(
createPersistedWorkspaceRecord({
workspaceId,
projectId: placement.projectKey,
cwd: workspaceId,
kind: deriveWorkspaceKind(placement.checkout),
displayName: deriveWorkspaceDisplayName({
cwd: workspaceId,
checkout: placement.checkout,
}),
createdAt,
updatedAt,
}),
);
}
options.logger.info(
@@ -31,6 +31,14 @@ const PersistedWorkspaceRecordSchema = z.object({
export type PersistedProjectRecord = z.infer<typeof PersistedProjectRecordSchema>;
export type PersistedWorkspaceRecord = z.infer<typeof PersistedWorkspaceRecordSchema>;
export function parsePersistedProjectRecords(input: unknown): PersistedProjectRecord[] {
return z.array(PersistedProjectRecordSchema).parse(input);
}
export function parsePersistedWorkspaceRecords(input: unknown): PersistedWorkspaceRecord[] {
return z.array(PersistedWorkspaceRecordSchema).parse(input);
}
export interface ProjectRegistry {
initialize(): Promise<void>;
existsOnDisk(): Promise<boolean>;