Skip to content

Commit 6ccefbf

Browse files
authored
Merge pull request #43 from dodjdnh/fix/windows-drive-root-checkout
Fix checkout workspace opening for Windows drive roots
2 parents 1f294d4 + 629318c commit 6ccefbf

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

src/workspaces.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import assert from "node:assert/strict";
77
import { loadConfig } from "./config.js";
88
import { GitWorktreeError } from "./git-worktrees.js";
99
import { SqliteWorkspaceStore } from "./workspace-store.js";
10-
import { WorkspaceRegistry } from "./workspaces.js";
10+
import { ensureCheckoutWorkspaceRoot, WorkspaceRegistry } from "./workspaces.js";
1111

1212
const execFileAsync = promisify(execFile);
1313
const root = await mkdtemp(join(tmpdir(), "devspace-workspace-test-"));
@@ -106,6 +106,21 @@ try {
106106
assert.equal(missingWorkspace.workspace.mode, "checkout");
107107
assert.equal((await stat(missingWorkspaceRoot)).isDirectory(), true);
108108

109+
{
110+
let mkdirCalls = 0;
111+
const existingStats = await ensureCheckoutWorkspaceRoot(root, {
112+
stat: async (path) => {
113+
assert.equal(path, root);
114+
return await stat(path);
115+
},
116+
mkdir: async () => {
117+
mkdirCalls += 1;
118+
},
119+
});
120+
assert.equal(existingStats.isDirectory(), true);
121+
assert.equal(mkdirCalls, 0);
122+
}
123+
109124
await assert.rejects(
110125
() => registry.openWorkspace({ path: root, mode: "worktree" }),
111126
(error: unknown) =>

src/workspaces.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { randomUUID } from "node:crypto";
2+
import type { Stats } from "node:fs";
23
import type { WorkspaceMode, WorkspaceStore } from "./workspace-store.js";
34
import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
45
import { dirname, join, relative, resolve, sep } from "node:path";
@@ -66,6 +67,12 @@ export interface OpenWorkspaceInput {
6667
baseRef?: string;
6768
}
6869

70+
type PathStats = Stats;
71+
type DirectoryOps = {
72+
stat: (path: string) => Promise<PathStats>;
73+
mkdir: (path: string, options: { recursive: true }) => Promise<unknown>;
74+
};
75+
6976
export class WorkspaceRegistry {
7077
private readonly workspaces = new Map<string, Workspace>();
7178

@@ -168,9 +175,7 @@ export class WorkspaceRegistry {
168175

169176
private async openCheckoutWorkspace(path: string): Promise<WorkspaceContext> {
170177
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);
174179
if (!rootStats.isDirectory()) {
175180
throw new Error(`Workspace root must be a directory: ${path}`);
176181
}
@@ -298,6 +303,22 @@ export class WorkspaceRegistry {
298303
}
299304
}
300305

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+
301322
const CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
302323
const SKIPPED_CONTEXT_DIRS = new Set([
303324
".git",
@@ -379,3 +400,7 @@ async function walkWorkspace(
379400
await visit(path, entry);
380401
}
381402
}
403+
404+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
405+
return error instanceof Error && "code" in error;
406+
}

0 commit comments

Comments
 (0)