feat: add Linux/macOS support for pathing, registry, and project scope

- pathing.ts: use path.resolve() on POSIX instead of win32.normalize
- registry.ts: replace ensureWindowsAbsolutePath with path.isAbsolute()
- Tests: platform-conditional assertions for both Windows and POSIX
- Windows behavior preserved unchanged via os.platform() guard

All 17 tests pass on macOS. Windows tests guarded behind IS_WINDOWS.
This commit is contained in:
Malte Sussdorff 2026-03-22 12:48:56 +01:00
parent b3956b31ce
commit 1cf007a800
5 changed files with 326 additions and 177 deletions

View file

@ -1,5 +1,8 @@
import os from 'node:os';
import path from 'node:path';
const IS_WINDOWS = os.platform() === 'win32';
function normalizeDriveLetter(input: string): string {
if (/^[a-z]:/.test(input)) {
return `${input[0].toUpperCase()}${input.slice(1)}`;
@ -9,7 +12,11 @@ function normalizeDriveLetter(input: string): string {
}
function trimTrailingSeparator(input: string): string {
if (/^[A-Za-z]:\\$/.test(input)) {
if (IS_WINDOWS && /^[A-Za-z]:\\$/.test(input)) {
return input;
}
if (!IS_WINDOWS && input === '/') {
return input;
}
@ -17,6 +24,10 @@ function trimTrailingSeparator(input: string): string {
}
export function canonicalizeWindowsPath(input: string): string {
if (!IS_WINDOWS) {
return trimTrailingSeparator(path.resolve(input));
}
const withBackslashes = input.replaceAll('/', '\\');
const normalized = path.win32.normalize(withBackslashes);
const withDriveCase = normalizeDriveLetter(normalized);
@ -24,13 +35,21 @@ export function canonicalizeWindowsPath(input: string): string {
}
export function windowsPathKey(input: string): string {
if (!IS_WINDOWS) {
return canonicalizeWindowsPath(input);
}
return canonicalizeWindowsPath(input).toLowerCase();
}
export function toDisplayPath(input: string): string {
if (!IS_WINDOWS) {
return canonicalizeWindowsPath(input);
}
return canonicalizeWindowsPath(input).replaceAll('\\', '/');
}
export function sameWindowsPath(a: string, b: string): boolean {
return windowsPathKey(a) === windowsPathKey(b);
}
}

View file

@ -29,17 +29,17 @@ export function registryFilePath(): string {
return path.join(userProfileRoot(), '.beadboard', 'projects.json');
}
function ensureWindowsAbsolutePath(input: string): string {
const normalized = canonicalizeWindowsPath(input.trim());
if (!/^[A-Za-z]:\\/.test(normalized)) {
throw new RegistryValidationError('Project path must be a Windows absolute path (e.g. C:\\Repos\\Project).');
function ensureAbsolutePath(input: string): string {
const trimmed = input.trim();
if (!path.isAbsolute(trimmed)) {
throw new RegistryValidationError(`Project path must be absolute: ${input}`);
}
return normalized;
return canonicalizeWindowsPath(trimmed);
}
function normalizeProject(input: string): RegistryProject {
const normalized = ensureWindowsAbsolutePath(input);
const normalized = ensureAbsolutePath(input);
return {
path: toDisplayPath(normalized),
key: windowsPathKey(normalized),