diff --git a/src/helpers.ts b/src/helpers.ts index fc2070e..e9a6b13 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -8,7 +8,7 @@ import { z } from "zod"; import { existsSync } from "node:fs"; -import { resolve } from "node:path"; +import { isAbsolute, resolve } from "node:path"; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -81,24 +81,34 @@ export function applyModelDefaults( /** * Normalize and validate a directory path: - * - Resolves to absolute (handles "..", trailing slashes, etc.) - * - Rejects relative paths (must start with /) + * - Resolves to absolute (handles "..", ".", trailing slashes, and + * converts relative inputs against `process.cwd()`) + * - Confirms the resolved path is absolute for the current platform * - Validates that the path exists on disk * + * Accepts both POSIX ("/home/user/my-project") and Windows + * ("C:\\Users\\me\\my-project", "\\\\server\\share") absolute paths via + * the platform-aware `resolve` + `isAbsolute` from `node:path`. + * * Returns the normalized path, or undefined if input was undefined. * Throws a descriptive Error on validation failure. */ export function normalizeDirectory(directory?: string): string | undefined { if (!directory) return undefined; - // Resolve to absolute (handles "..", ".", trailing slashes) + // Resolve to an absolute, platform-appropriate form. `resolve` handles + // "..", ".", trailing slashes, and will convert a relative input against + // `process.cwd()`. const normalized = resolve(directory); - // Must be an absolute path - if (!normalized.startsWith("/")) { + // Defensive check: `resolve` guarantees an absolute path on every + // supported platform, but we verify via the platform-aware `isAbsolute` + // so callers get a clear error if that assumption is ever violated. + if (!isAbsolute(normalized)) { throw new Error( `Invalid directory: "${directory}" is not an absolute path. ` + - `Provide a full path like "/home/user/my-project".`, + `Provide a full path like "/home/user/my-project" (POSIX) or ` + + `"C:\\\\Users\\\\me\\\\my-project" (Windows).`, ); } diff --git a/tests/helpers.test.ts b/tests/helpers.test.ts index bb718c4..b942a84 100644 --- a/tests/helpers.test.ts +++ b/tests/helpers.test.ts @@ -927,6 +927,29 @@ describe("normalizeDirectory", () => { const result = normalizeDirectory("/tmp"); expect(result).toBe("/tmp"); }); + + it("accepts the process working directory on any platform", () => { + // Platform-agnostic regression: process.cwd() is always absolute for the + // current platform, so normalizeDirectory must accept it regardless of OS. + const result = normalizeDirectory(process.cwd()); + expect(result).toBeDefined(); + }); + + // Regression for Windows drive-letter paths. + // Before the fix, normalizeDirectory required the resolved path to start + // with "/", which meant Windows absolute paths like "C:\\Users\\..." were + // rejected. The fix switches to the platform-aware `path.isAbsolute`, so + // this test guards the Windows path on a Windows CI runner. + it.runIf(process.platform === "win32")( + "accepts Windows drive-letter absolute paths", + () => { + const cwd = process.cwd(); // e.g. "C:\\Users\\runner\\work\\..." + expect(cwd).toMatch(/^[A-Za-z]:\\/); + const result = normalizeDirectory(cwd); + expect(result).toBeDefined(); + expect(result).toMatch(/^[A-Za-z]:\\/); + }, + ); }); // ─── diagnoseError (via toolError) ───────────────────────────────────────