agent-ecosystem/src/renderer/store/utils/pathResolution.ts
infiniti 4adc233fa4 fix: harden Windows frontend path handling
Harden Windows path handling and packaged app smoke checks.
2026-05-16 17:40:15 +03:00

112 lines
3.4 KiB
TypeScript

/**
* Path resolution utilities for the store.
*/
import { stripTrailingSeparators } from '@shared/utils/platformPath';
/**
* Resolves a relative path against a base path, handling various path formats.
* Handles:
* - Absolute paths: /full/path/file.tsx (returned as-is)
* - Relative paths with ./: ./apps/foo/bar.tsx (strips ./)
* - Parent paths with ../: ../other/file.tsx (walks up directories)
* - Plain paths: apps/foo/bar.tsx (joins with base)
* - Paths with @ prefix: @apps/foo/bar.tsx (strips @ then joins)
*/
export function resolveFilePath(base: string, relativePath: string): string {
// If already absolute, return as-is
if (isAbsolutePath(relativePath)) {
return relativePath;
}
const cleanBase = stripTrailingSeparators(base);
// Handle @ prefix (file mention marker) - strip it if present
let cleanRelative = relativePath;
if (cleanRelative.startsWith('@')) {
cleanRelative = cleanRelative.slice(1);
}
// Tilde paths (~/) are home-relative absolute paths - pass through as-is
// The main process will expand ~ to the actual home directory
if (cleanRelative.startsWith('~/') || cleanRelative.startsWith('~\\') || cleanRelative === '~') {
return cleanRelative;
}
// Handle ./ prefix (current directory)
if (cleanRelative.startsWith('./') || cleanRelative.startsWith('.\\')) {
cleanRelative = cleanRelative.slice(2);
}
// Handle ../ prefixes (parent directory)
const separator = cleanBase.includes('\\') ? '\\' : '/';
const hasUncRoot = cleanBase.startsWith('\\\\') || cleanBase.startsWith('//');
const hasUnixRoot = !hasUncRoot && cleanBase.startsWith('/');
const minRootParts = hasUncRoot ? 2 : 1;
const normalizedRelative = normalizeSeparators(cleanRelative, separator);
const baseParts = splitPath(cleanBase);
let remainingRelative = normalizedRelative;
while (remainingRelative.startsWith(`..${separator}`)) {
remainingRelative = remainingRelative.slice(3);
if (baseParts.length > minRootParts) {
baseParts.pop();
}
}
// Join the normalized paths
let normalizedBase = baseParts.join(separator);
if (hasUnixRoot && !normalizedBase.startsWith('/')) {
normalizedBase = `/${normalizedBase}`;
}
if (hasUncRoot && !normalizedBase.startsWith(`${separator}${separator}`)) {
normalizedBase = `${separator}${separator}${normalizedBase}`;
}
return remainingRelative ? `${normalizedBase}${separator}${remainingRelative}` : normalizedBase;
}
function isAbsolutePath(input: string): boolean {
return input.startsWith('/') || input.startsWith('\\\\') || /^[a-zA-Z]:[\\/]/.test(input);
}
function normalizeSeparators(input: string, separator: '/' | '\\'): string {
let output = '';
let prevWasSeparator = false;
for (const char of input) {
const isSeparator = char === '/' || char === '\\';
if (isSeparator) {
if (!prevWasSeparator) {
output += separator;
}
prevWasSeparator = true;
} else {
output += char;
prevWasSeparator = false;
}
}
return output;
}
function splitPath(input: string): string[] {
const parts: string[] = [];
let current = '';
for (const char of input) {
if (char === '/' || char === '\\') {
if (current.length > 0) {
parts.push(current);
current = '';
}
} else {
current += char;
}
}
if (current.length > 0) {
parts.push(current);
}
return parts;
}