|
1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | +import type { Stats } from "node:fs"; |
2 | 3 | import type { WorkspaceMode, WorkspaceStore } from "./workspace-store.js"; |
3 | 4 | import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises"; |
4 | 5 | import { dirname, join, relative, resolve, sep } from "node:path"; |
@@ -66,6 +67,12 @@ export interface OpenWorkspaceInput { |
66 | 67 | baseRef?: string; |
67 | 68 | } |
68 | 69 |
|
| 70 | +type PathStats = Stats; |
| 71 | +type DirectoryOps = { |
| 72 | + stat: (path: string) => Promise<PathStats>; |
| 73 | + mkdir: (path: string, options: { recursive: true }) => Promise<unknown>; |
| 74 | +}; |
| 75 | + |
69 | 76 | export class WorkspaceRegistry { |
70 | 77 | private readonly workspaces = new Map<string, Workspace>(); |
71 | 78 |
|
@@ -168,9 +175,7 @@ export class WorkspaceRegistry { |
168 | 175 |
|
169 | 176 | private async openCheckoutWorkspace(path: string): Promise<WorkspaceContext> { |
170 | 177 | const root = assertAllowedPath(path, this.config.allowedRoots); |
171 | | - await mkdir(root, { recursive: true }); |
172 | | - |
173 | | - const rootStats = await stat(root); |
| 178 | + const rootStats = await ensureCheckoutWorkspaceRoot(root); |
174 | 179 | if (!rootStats.isDirectory()) { |
175 | 180 | throw new Error(`Workspace root must be a directory: ${path}`); |
176 | 181 | } |
@@ -298,6 +303,22 @@ export class WorkspaceRegistry { |
298 | 303 | } |
299 | 304 | } |
300 | 305 |
|
| 306 | +export async function ensureCheckoutWorkspaceRoot( |
| 307 | + path: string, |
| 308 | + ops: DirectoryOps = { stat, mkdir }, |
| 309 | +): Promise<PathStats> { |
| 310 | + try { |
| 311 | + return await ops.stat(path); |
| 312 | + } catch (error) { |
| 313 | + if (!isErrnoException(error) || error.code !== "ENOENT") { |
| 314 | + throw error; |
| 315 | + } |
| 316 | + } |
| 317 | + |
| 318 | + await ops.mkdir(path, { recursive: true }); |
| 319 | + return await ops.stat(path); |
| 320 | +} |
| 321 | + |
301 | 322 | const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); |
302 | 323 | const SKIPPED_CONTEXT_DIRS = new Set([ |
303 | 324 | ".git", |
@@ -379,3 +400,7 @@ async function walkWorkspace( |
379 | 400 | await visit(path, entry); |
380 | 401 | } |
381 | 402 | } |
| 403 | + |
| 404 | +function isErrnoException(error: unknown): error is NodeJS.ErrnoException { |
| 405 | + return error instanceof Error && "code" in error; |
| 406 | +} |
0 commit comments