-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathutils.ts
More file actions
121 lines (106 loc) · 2.92 KB
/
utils.ts
File metadata and controls
121 lines (106 loc) · 2.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { execFile } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
export interface GitHubRepo {
organization: string;
repository: string;
}
export async function safeSymlink(
source: string,
target: string,
type: "file" | "dir",
): Promise<boolean> {
if (path.resolve(source) === path.resolve(target)) {
return false;
}
const sourceDir = path.dirname(path.resolve(source));
const targetDir = path.dirname(path.resolve(target));
if (
sourceDir === targetDir &&
path.basename(source) === path.basename(target)
) {
return false;
}
try {
await fs.access(source);
} catch {
return false;
}
try {
if (os.platform() === "win32") {
// On Windows, skip symlinks entirely — they need admin/Developer Mode.
// Use junctions for directories and hard links for files instead,
// matching the approach used by pnpm, Deno, and npm.
if (type === "dir") {
await fs.symlink(source, target, "junction");
} else {
try {
await fs.link(source, target);
} catch {
// Hard link can fail across drives — copy as last resort
await fs.copyFile(source, target);
}
}
} else {
await fs.symlink(source, target, type);
}
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
return false;
}
throw error;
}
}
/**
* copy file or directory, use copy-on-write, fall back to cp
*/
export async function clonePath(
source: string,
destination: string,
): Promise<boolean> {
try {
await fs.access(source);
} catch {
return false;
}
const parentDir = path.dirname(destination);
await fs.mkdir(parentDir, { recursive: true });
const platform = os.platform();
try {
if (platform === "darwin") {
await execFileAsync("cp", ["-c", "-a", source, destination]);
} else {
await execFileAsync("cp", ["--reflink=auto", "-a", source, destination]);
}
return true;
} catch {
// CoW not supported, fall back to regular copy
}
await fs.cp(source, destination, { recursive: true });
return true;
}
function execFileAsync(
command: string,
args: string[],
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
}
export function parseGitHubUrl(url: string): GitHubRepo | null {
// Trim whitespace/newlines that git commands may include
const trimmedUrl = url.trim();
const match =
trimmedUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/) ||
trimmedUrl.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/);
if (!match) return null;
return { organization: match[1], repository: match[2].replace(/\.git$/, "") };
}