import { access, stat } from "node:fs/promises"; import path from "node:path"; import { readGitCheckout } from "@rakazo/core/git-checkout"; import type { PrismaClient } from "./client.js"; export class InvalidProjectWorkspacePathError extends Error { constructor(message: string) { super(message); this.name = "InvalidProjectWorkspacePathError"; } } export function normalizeProjectWorkspacePath(input: string): string { const trimmed = input.trim(); if (!trimmed) throw new InvalidProjectWorkspacePathError("A folder path is required."); if (trimmed.length > 500) { throw new InvalidProjectWorkspacePathError("That folder path is too long."); } if (trimmed.includes("\0")) { throw new InvalidProjectWorkspacePathError("That folder path is not valid."); } return path.resolve(trimmed); } export async function assertProjectWorkspaceDirectory(workspacePath: string): Promise { const resolved = normalizeProjectWorkspacePath(workspacePath); let info: Awaited>; try { await access(resolved); info = await stat(resolved); } catch { throw new InvalidProjectWorkspacePathError("That folder does not exist or is not readable."); } if (!info.isDirectory()) { throw new InvalidProjectWorkspacePathError("That path is not a folder."); } } export interface WiredProjectWorkspace { path: string; git: { remoteUrl: string; defaultBranch: string; headCommit: string | null; dirty: boolean; dirtyPaths: string[]; } | null; } /** Persist a host folder and optional git metadata on a newly created space. */ export async function wireProjectWorkspace( prisma: Pick, input: { spaceId: string; workspacePath: string }, ): Promise { const resolved = normalizeProjectWorkspacePath(input.workspacePath); await assertProjectWorkspaceDirectory(resolved); const git = await readGitCheckout(resolved); await prisma.space.update({ where: { id: input.spaceId }, data: { workspacePath: resolved, gitRemoteUrl: git.present && git.remoteUrl ? git.remoteUrl : null, gitDefaultBranch: git.present && git.defaultBranch ? git.defaultBranch : null, gitHeadCommit: git.present ? git.headCommit : null, }, }); return { path: resolved, git: git.present && git.remoteUrl && git.defaultBranch ? { remoteUrl: git.remoteUrl, defaultBranch: git.defaultBranch, headCommit: git.headCommit, dirty: git.dirty, dirtyPaths: git.dirtyPaths, } : null, }; } export function projectWorkspaceDto(space: { workspacePath: string | null; gitRemoteUrl: string | null; gitDefaultBranch: string | null; gitHeadCommit: string | null; }): WiredProjectWorkspace | null { if (!space.workspacePath) return null; return { path: space.workspacePath, git: space.gitRemoteUrl && space.gitDefaultBranch ? { remoteUrl: space.gitRemoteUrl, defaultBranch: space.gitDefaultBranch, headCommit: space.gitHeadCommit, dirty: false, dirtyPaths: [], } : null, }; }