1import { accessSync, constants } from "node:fs";
2import * as os from "node:os";
3import { isAbsolute, resolve as resolvePath } from "node:path";
4
5const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
6
7function normalizeAtPrefix(filePath: string): string {
8 return filePath.startsWith("@") ? filePath.slice(1) : filePath;
9}
10
11function fileExists(path: string): boolean {
12 try {
13 accessSync(path, constants.F_OK);
14 return true;
15 } catch {
16 return false;
17 }
18}
19
20function tryMacOSScreenshotPath(filePath: string): string {
21 return filePath.replace(/ (AM|PM)\./g, "\u202F$1.");
22}
23
24function tryNFDVariant(filePath: string): string {
25 return filePath.normalize("NFD");
26}
27
28function tryCurlyQuoteVariant(filePath: string): string {
29 return filePath.replace(/'/g, "\u2019");
30}
31
32export function expandPath(filePath: string): string {
33 let result = filePath.replace(UNICODE_SPACES, " ");
34 result = normalizeAtPrefix(result);
35
36 if (result === "~") {
37 return os.homedir();
38 }
39 if (result.startsWith("~/")) {
40 return resolvePath(os.homedir(), result.slice(2));
41 }
42
43 return result;
44}
45
46export function resolveToCwd(filePath: string, cwd: string): string {
47 const expanded = expandPath(filePath);
48 if (isAbsolute(expanded)) {
49 return expanded;
50 }
51 return resolvePath(cwd, expanded);
52}
53
54export function resolveReadPath(filePath: string, cwd: string): string {
55 const resolved = resolveToCwd(filePath, cwd);
56
57 if (fileExists(resolved)) {
58 return resolved;
59 }
60
61 const variants = [tryNFDVariant, tryMacOSScreenshotPath, tryCurlyQuoteVariant];
62 for (const variant of variants) {
63 const candidate = variant(resolved);
64 if (candidate !== resolved && fileExists(candidate)) {
65 return candidate;
66 }
67 }
68
69 return resolved;
70}