@solaqua/gji 0.12.2 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap-output.d.ts +3 -2
- package/dist/bootstrap-output.js +1 -16
- package/dist/bootstrap-preview.d.ts +2 -3
- package/dist/bootstrap-preview.js +1 -11
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +3 -13
- package/dist/config.js +12 -59
- package/dist/dependency-bootstrap.d.ts +11 -45
- package/dist/dependency-bootstrap.js +142 -608
- package/dist/gji-bundle.mjs +567 -2367
- package/dist/new.d.ts +4 -5
- package/dist/new.js +10 -45
- package/dist/pr.d.ts +4 -5
- package/dist/pr.js +9 -69
- package/dist/shell-completion.js +7 -5
- package/dist/uv-validation.d.ts +4 -0
- package/dist/uv-validation.js +210 -0
- package/dist/worktree-bootstrap.d.ts +5 -16
- package/dist/worktree-bootstrap.js +1 -24
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +8 -5
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +6 -3
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +2 -2
- package/package.json +1 -1
- package/dist/dependency-bootstrap-prompt.d.ts +0 -24
- package/dist/dependency-bootstrap-prompt.js +0 -103
- package/dist/dir-clone.d.ts +0 -32
- package/dist/dir-clone.js +0 -641
- package/dist/install-prompt.d.ts +0 -12
- package/dist/install-prompt.js +0 -127
- package/dist/package-manager.d.ts +0 -5
- package/dist/package-manager.js +0 -159
- package/dist/sync-directories.d.ts +0 -29
- package/dist/sync-directories.js +0 -77
- package/dist/sync-plan.d.ts +0 -16
- package/dist/sync-plan.js +0 -128
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { lstat, open, readdir, readFile, realpath, rm } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
6
3
|
import { runCommand } from "./command-runner.js";
|
|
7
|
-
import {
|
|
8
|
-
import { isNotFoundError, pathExists } from "./fs-utils.js";
|
|
4
|
+
import { pathExists } from "./fs-utils.js";
|
|
9
5
|
import { inspectDestination } from "./safe-destination.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { validateUvInstallation, validateUvStructure, } from "./uv-validation.js";
|
|
7
|
+
export function resolveDependencyBootstrapMode(dependencyBootstrap, noInstall = false) {
|
|
8
|
+
if (noInstall || dependencyBootstrap === "off")
|
|
9
|
+
return "off";
|
|
10
|
+
return "install";
|
|
11
|
+
}
|
|
14
12
|
export async function prepareDependencyBootstrap(mode, context) {
|
|
15
13
|
if (mode === "off")
|
|
16
14
|
return { mode, targets: [] };
|
|
17
|
-
const adapters = createBootstrapAdapters(context.
|
|
15
|
+
const adapters = createBootstrapAdapters(context.cargoBuildCommand);
|
|
18
16
|
const sourceRoots = [];
|
|
19
17
|
for (const sourceRoot of uniquePaths([
|
|
20
18
|
context.repoRoot,
|
|
@@ -29,8 +27,6 @@ export async function prepareDependencyBootstrap(mode, context) {
|
|
|
29
27
|
for (const adapter of adapters) {
|
|
30
28
|
if (plannedRelativePaths.has(adapter.relativePath))
|
|
31
29
|
continue;
|
|
32
|
-
let firstDetected;
|
|
33
|
-
let selected;
|
|
34
30
|
for (const sourceRoot of sourceRoots) {
|
|
35
31
|
const target = await adapter.detect({
|
|
36
32
|
detectionRoot: context.detectionRoot,
|
|
@@ -39,46 +35,21 @@ export async function prepareDependencyBootstrap(mode, context) {
|
|
|
39
35
|
});
|
|
40
36
|
if (!target)
|
|
41
37
|
continue;
|
|
42
|
-
|
|
43
|
-
const candidate = { target, seedable };
|
|
44
|
-
firstDetected ??= candidate;
|
|
45
|
-
if (mode === "install-only" || seedable) {
|
|
46
|
-
selected = candidate;
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const plannedTarget = selected ?? firstDetected;
|
|
51
|
-
if (plannedTarget) {
|
|
52
|
-
targets.push(plannedTarget);
|
|
38
|
+
targets.push({ adapter, target });
|
|
53
39
|
plannedRelativePaths.add(adapter.relativePath);
|
|
40
|
+
break;
|
|
54
41
|
}
|
|
55
42
|
}
|
|
56
43
|
return { mode, targets };
|
|
57
44
|
}
|
|
58
|
-
export async function detectDependencyBootstrapCandidate(context) {
|
|
59
|
-
const plan = await prepareDependencyBootstrap("cow-then-repair", context);
|
|
60
|
-
const planned = plan.targets[0];
|
|
61
|
-
if (!planned)
|
|
62
|
-
return null;
|
|
63
|
-
return {
|
|
64
|
-
adapter: planned.target.adapter,
|
|
65
|
-
kind: planned.target.kind,
|
|
66
|
-
lockfile: adapterForTarget(planned.target).lockfile,
|
|
67
|
-
target: planned.target.relativePath,
|
|
68
|
-
repairCommand: planned.target.repairCommand,
|
|
69
|
-
seedable: planned.seedable,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
45
|
export function previewDependencyBootstrap(plan) {
|
|
73
46
|
return {
|
|
74
47
|
mode: plan.mode,
|
|
75
|
-
targets: plan.targets.map(({
|
|
76
|
-
adapter:
|
|
77
|
-
kind:
|
|
48
|
+
targets: plan.targets.map(({ adapter, target }) => ({
|
|
49
|
+
adapter: adapter.name,
|
|
50
|
+
kind: adapter.kind,
|
|
78
51
|
target: target.relativePath,
|
|
79
|
-
|
|
80
|
-
seedable,
|
|
81
|
-
strategy: bootstrapStrategy(plan.mode, target, seedable),
|
|
52
|
+
command: target.installCommand,
|
|
82
53
|
})),
|
|
83
54
|
};
|
|
84
55
|
}
|
|
@@ -86,27 +57,16 @@ export async function executeDependencyBootstrap(plan, options) {
|
|
|
86
57
|
if (plan.mode === "off")
|
|
87
58
|
return { mode: plan.mode, ready: true, events: [] };
|
|
88
59
|
const events = [];
|
|
89
|
-
const cloneDirectory = options.cloneDirectory ?? cloneDir;
|
|
90
60
|
const execution = {
|
|
91
61
|
runCommand: options.runCommand ?? runCommand,
|
|
92
62
|
stderr: options.stderr ?? (() => undefined),
|
|
93
63
|
stdout: options.stdout ?? ((chunk) => process.stdout.write(chunk)),
|
|
94
64
|
};
|
|
95
|
-
const seededDirectories = new Set(options.seededDirectories ?? []);
|
|
96
65
|
if (plan.targets.length === 0) {
|
|
97
|
-
recordBootstrapEvent(events, options.reporter, {
|
|
98
|
-
adapter: "none",
|
|
99
|
-
kind: "dependency",
|
|
100
|
-
reason: "no-lockfile",
|
|
101
|
-
state: "skipped",
|
|
102
|
-
target: "",
|
|
103
|
-
message: "no supported dependency or build-state lockfile was detected",
|
|
104
|
-
});
|
|
105
66
|
return { mode: plan.mode, ready: true, events };
|
|
106
67
|
}
|
|
107
|
-
for (const {
|
|
108
|
-
|
|
109
|
-
await executeBootstrapTarget(adapter, target, seedable, plan.mode, cloneDirectory, execution, seededDirectories.has(target.relativePath), events, options.reporter);
|
|
68
|
+
for (const { adapter, target } of plan.targets) {
|
|
69
|
+
await installTarget(adapter, target, execution, events, options.reporter);
|
|
110
70
|
}
|
|
111
71
|
return {
|
|
112
72
|
mode: plan.mode,
|
|
@@ -114,207 +74,33 @@ export async function executeDependencyBootstrap(plan, options) {
|
|
|
114
74
|
events,
|
|
115
75
|
};
|
|
116
76
|
}
|
|
117
|
-
async function
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (mode === "install-only") {
|
|
124
|
-
await repairTarget(adapter, target, execution, destinationInspection.kind === "exists" ? "preserve" : "clean", events, reporter);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
let input = destinationInspection.kind === "exists" ? "preserve" : "clean";
|
|
128
|
-
const destinationExistsNow = destinationInspection.kind === "exists";
|
|
129
|
-
if (target.initialInput === "preserve" && destinationExistsNow) {
|
|
130
|
-
recordBootstrapEvent(events, reporter, {
|
|
131
|
-
adapter: adapter.name,
|
|
132
|
-
kind: adapter.kind,
|
|
133
|
-
reason: "target-exists",
|
|
134
|
-
state: "skipped",
|
|
135
|
-
target: target.relativePath,
|
|
136
|
-
message: "target already existed; using it as the repair input",
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
else if (seededBySyncDirs && destinationExistsNow) {
|
|
140
|
-
if (seedable) {
|
|
141
|
-
input = "seed";
|
|
142
|
-
recordBootstrapEvent(events, reporter, {
|
|
143
|
-
adapter: adapter.name,
|
|
144
|
-
kind: adapter.kind,
|
|
145
|
-
reason: "generic-seed",
|
|
146
|
-
state: "seeded",
|
|
147
|
-
target: target.relativePath,
|
|
148
|
-
message: "reusing a seed created by syncDirs",
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
input = "preserve";
|
|
153
|
-
recordBootstrapEvent(events, reporter, {
|
|
154
|
-
adapter: adapter.name,
|
|
155
|
-
kind: adapter.kind,
|
|
156
|
-
reason: "generic-seed",
|
|
157
|
-
state: "repair-only",
|
|
158
|
-
target: target.relativePath,
|
|
159
|
-
message: "syncDirs created a generic target; this adapter uses repair without CoW",
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
else if (destinationExistsNow) {
|
|
164
|
-
input = "preserve";
|
|
165
|
-
recordBootstrapEvent(events, reporter, {
|
|
166
|
-
adapter: adapter.name,
|
|
167
|
-
kind: adapter.kind,
|
|
168
|
-
reason: "destination-race",
|
|
169
|
-
state: "skipped",
|
|
170
|
-
target: target.relativePath,
|
|
171
|
-
message: "target appeared during bootstrap; preserving it as the repair input",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
else if (seedable) {
|
|
175
|
-
try {
|
|
176
|
-
await cloneDirectory(adapter.seedPath(target), target.targetPath, {
|
|
177
|
-
destinationRoot: target.worktreePath,
|
|
178
|
-
measureBytes: reporter.measureCloneSize,
|
|
179
|
-
});
|
|
180
|
-
input = "seed";
|
|
181
|
-
recordBootstrapEvent(events, reporter, {
|
|
182
|
-
adapter: adapter.name,
|
|
183
|
-
kind: adapter.kind,
|
|
184
|
-
state: "seeded",
|
|
185
|
-
target: target.relativePath,
|
|
186
|
-
message: "seeded with copy-on-write",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
if (isCloneDestinationExistsError(error)) {
|
|
191
|
-
input = "preserve";
|
|
192
|
-
recordBootstrapEvent(events, reporter, {
|
|
193
|
-
adapter: adapter.name,
|
|
194
|
-
kind: adapter.kind,
|
|
195
|
-
state: "skipped",
|
|
196
|
-
target: target.relativePath,
|
|
197
|
-
message: "target appeared during CoW seeding; preserving it as the repair input",
|
|
198
|
-
});
|
|
199
|
-
await repairTarget(adapter, target, execution, input, events, reporter);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (isCloneInProgressError(error)) {
|
|
203
|
-
recordBootstrapFailure(events, reporter, adapter, target, `CoW seed is already in progress: ${toErrorMessage(error)}`, "clone-in-progress");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
const failedCloneInspection = await inspectDestination(target.worktreePath, target.targetPath);
|
|
207
|
-
if (failedCloneInspection.kind === "unsafe") {
|
|
208
|
-
recordBootstrapFailure(events, reporter, adapter, target, failedCloneInspection.reason, "destination-unsafe");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
input = failedCloneInspection.kind === "exists" ? "preserve" : "clean";
|
|
212
|
-
const repairInput = input === "preserve" ? "the preserved target" : "an empty target";
|
|
213
|
-
recordBootstrapEvent(events, reporter, {
|
|
214
|
-
adapter: adapter.name,
|
|
215
|
-
kind: adapter.kind,
|
|
216
|
-
reason: "cow-seed-failed",
|
|
217
|
-
state: "repair-only",
|
|
218
|
-
target: target.relativePath,
|
|
219
|
-
message: `CoW seed failed; repairing from ${repairInput} (${toErrorMessage(error)})`,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
recordBootstrapEvent(events, reporter, {
|
|
225
|
-
adapter: adapter.name,
|
|
226
|
-
kind: adapter.kind,
|
|
227
|
-
reason: "seed-unavailable",
|
|
228
|
-
state: "repair-only",
|
|
229
|
-
target: target.relativePath,
|
|
230
|
-
message: "CoW seed is unavailable; repairing from an empty target",
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
await repairTarget(adapter, target, execution, input, events, reporter);
|
|
234
|
-
}
|
|
235
|
-
async function repairTarget(adapter, target, execution, input, events, reporter) {
|
|
236
|
-
const beforeRepairInspection = await inspectDestination(target.worktreePath, target.targetPath);
|
|
237
|
-
if (beforeRepairInspection.kind === "unsafe") {
|
|
238
|
-
recordBootstrapFailure(events, reporter, adapter, target, beforeRepairInspection.reason, "destination-unsafe");
|
|
77
|
+
async function installTarget(adapter, target, execution, events, reporter) {
|
|
78
|
+
const beforeInstallInspection = target.targetPath
|
|
79
|
+
? await inspectDestination(target.worktreePath, target.targetPath)
|
|
80
|
+
: undefined;
|
|
81
|
+
if (beforeInstallInspection?.kind === "unsafe") {
|
|
82
|
+
recordBootstrapFailure(events, reporter, adapter, target, beforeInstallInspection.reason, "destination-unsafe");
|
|
239
83
|
return;
|
|
240
84
|
}
|
|
241
|
-
const effectiveInput =
|
|
242
|
-
? input === "seed"
|
|
243
|
-
? "seed"
|
|
244
|
-
: "preserve"
|
|
245
|
-
: "clean";
|
|
85
|
+
const effectiveInput = beforeInstallInspection?.kind === "exists" ? "preserve" : "clean";
|
|
246
86
|
try {
|
|
247
|
-
await adapter.
|
|
87
|
+
await adapter.install(target, { ...execution, input: effectiveInput });
|
|
248
88
|
recordBootstrapEvent(events, reporter, {
|
|
249
89
|
adapter: adapter.name,
|
|
250
90
|
kind: adapter.kind,
|
|
251
|
-
state:
|
|
91
|
+
state: "installed",
|
|
252
92
|
target: target.relativePath,
|
|
253
|
-
message:
|
|
93
|
+
message: installSuccessMessage(target, effectiveInput),
|
|
254
94
|
});
|
|
255
95
|
}
|
|
256
|
-
catch (
|
|
257
|
-
|
|
258
|
-
input: effectiveInput,
|
|
259
|
-
target,
|
|
260
|
-
};
|
|
261
|
-
const cleanupError = await adapter
|
|
262
|
-
.cleanupAfterRepairFailure(firstFailureContext)
|
|
263
|
-
.then(() => undefined)
|
|
264
|
-
.catch((error) => toErrorMessage(error));
|
|
265
|
-
if (!adapter.shouldRetryAfterRepairFailure(firstFailureContext)) {
|
|
266
|
-
recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(firstError, cleanupError === undefined ? undefined : cleanupError), "repair-failed");
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (cleanupError) {
|
|
270
|
-
recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(firstError, cleanupError), "repair-cleanup-failed");
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
recordBootstrapEvent(events, reporter, {
|
|
274
|
-
adapter: adapter.name,
|
|
275
|
-
kind: adapter.kind,
|
|
276
|
-
reason: "seed-repair-failed",
|
|
277
|
-
state: "repair-only",
|
|
278
|
-
target: target.relativePath,
|
|
279
|
-
message: `seed repair failed; removed the seed and retrying clean (${toErrorMessage(firstError)})`,
|
|
280
|
-
});
|
|
281
|
-
const beforeRetryInspection = await inspectDestination(target.worktreePath, target.targetPath);
|
|
282
|
-
if (beforeRetryInspection.kind === "unsafe") {
|
|
283
|
-
recordBootstrapFailure(events, reporter, adapter, target, beforeRetryInspection.reason, "destination-unsafe");
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const retryInput = beforeRetryInspection.kind === "exists" ? "preserve" : "clean";
|
|
287
|
-
try {
|
|
288
|
-
await adapter.repair(target, { ...execution, input: retryInput });
|
|
289
|
-
recordBootstrapEvent(events, reporter, {
|
|
290
|
-
adapter: adapter.name,
|
|
291
|
-
kind: adapter.kind,
|
|
292
|
-
reason: "repair-retry",
|
|
293
|
-
state: target.repairState,
|
|
294
|
-
target: target.relativePath,
|
|
295
|
-
message: repairSuccessMessage(target, retryInput),
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
catch (secondError) {
|
|
299
|
-
const cleanupError = await adapter
|
|
300
|
-
.cleanupAfterRepairFailure({
|
|
301
|
-
input: retryInput,
|
|
302
|
-
target,
|
|
303
|
-
})
|
|
304
|
-
.then(() => undefined)
|
|
305
|
-
.catch((error) => toErrorMessage(error));
|
|
306
|
-
recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(secondError, cleanupError), "repair-failed");
|
|
307
|
-
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
recordBootstrapFailure(events, reporter, adapter, target, formatInstallFailure(error), "install-failed");
|
|
308
98
|
}
|
|
309
99
|
}
|
|
310
|
-
function
|
|
311
|
-
if (input === "seed")
|
|
312
|
-
return "reused and repaired";
|
|
100
|
+
function installSuccessMessage(_target, input) {
|
|
313
101
|
if (input === "preserve")
|
|
314
|
-
return "
|
|
315
|
-
return
|
|
316
|
-
? "installed from a clean target"
|
|
317
|
-
: "repaired from a clean target";
|
|
102
|
+
return "installed into the existing target";
|
|
103
|
+
return "installed into a clean target";
|
|
318
104
|
}
|
|
319
105
|
function recordBootstrapFailure(events, reporter, adapter, target, message, reason) {
|
|
320
106
|
recordBootstrapEvent(events, reporter, {
|
|
@@ -330,447 +116,195 @@ function recordBootstrapEvent(events, reporter, event) {
|
|
|
330
116
|
events.push(event);
|
|
331
117
|
reporter.dependency(event);
|
|
332
118
|
}
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
return cleanupError ? `${message}; cleanup failed: ${cleanupError}` : message;
|
|
336
|
-
}
|
|
337
|
-
function bootstrapStrategy(mode, target, seedable) {
|
|
338
|
-
if (mode === "install-only" || target.repairState === "installed") {
|
|
339
|
-
return "install-only";
|
|
340
|
-
}
|
|
341
|
-
return seedable ? "cow-then-repair" : "repair-only";
|
|
119
|
+
function formatInstallFailure(error) {
|
|
120
|
+
return `install failed: ${toErrorMessage(error)}`;
|
|
342
121
|
}
|
|
343
|
-
function
|
|
344
|
-
const adapter = createBootstrapAdapters(defaultCheckUvRuntime).find((candidate) => candidate.name === target.adapter);
|
|
345
|
-
if (!adapter)
|
|
346
|
-
throw new Error(`unsupported bootstrap adapter: ${target.adapter}`);
|
|
347
|
-
return adapter;
|
|
348
|
-
}
|
|
349
|
-
function createBootstrapAdapters(checkUvRuntime, cargoBuildCommand) {
|
|
122
|
+
function createBootstrapAdapters(cargoBuildCommand) {
|
|
350
123
|
return [
|
|
351
124
|
new LockfileBootstrapAdapter({
|
|
352
125
|
name: "pnpm",
|
|
353
126
|
kind: "dependency",
|
|
354
|
-
|
|
127
|
+
lockfiles: ["pnpm-lock.yaml"],
|
|
355
128
|
relativePath: "node_modules",
|
|
356
|
-
|
|
357
|
-
shell: false,
|
|
358
|
-
beforeRepair: async (target, context) => {
|
|
359
|
-
if (context.input === "seed") {
|
|
360
|
-
await rm(join(target.targetPath, ".modules.yaml"), {
|
|
361
|
-
force: true,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
},
|
|
129
|
+
installCommand: "pnpm install --frozen-lockfile",
|
|
365
130
|
}),
|
|
366
131
|
new LockfileBootstrapAdapter({
|
|
367
132
|
name: "yarn",
|
|
368
133
|
kind: "dependency",
|
|
369
|
-
|
|
134
|
+
lockfiles: ["yarn.lock"],
|
|
370
135
|
relativePath: "node_modules",
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
136
|
+
installCommand: "yarn install --frozen-lockfile",
|
|
137
|
+
selectInstallCommand: async (_target, _input) => (await isYarnBerry(_target.detectionRoot ?? _target.sourceRoot))
|
|
138
|
+
? "yarn install --immutable"
|
|
139
|
+
: "yarn install --frozen-lockfile",
|
|
140
|
+
}),
|
|
141
|
+
new LockfileBootstrapAdapter({
|
|
142
|
+
name: "bun",
|
|
143
|
+
kind: "dependency",
|
|
144
|
+
lockfiles: ["bun.lock", "bun.lockb"],
|
|
145
|
+
relativePath: "node_modules",
|
|
146
|
+
installCommand: "bun install --frozen-lockfile",
|
|
380
147
|
}),
|
|
381
148
|
new LockfileBootstrapAdapter({
|
|
382
149
|
name: "npm",
|
|
383
150
|
kind: "dependency",
|
|
384
|
-
|
|
151
|
+
lockfiles: ["package-lock.json"],
|
|
385
152
|
relativePath: "node_modules",
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
seedPolicy: "never",
|
|
389
|
-
selectRepairCommand: (_target, input) => input === "preserve" ? "npm install" : "npm ci",
|
|
390
|
-
repairState: "installed",
|
|
153
|
+
installCommand: "npm ci",
|
|
154
|
+
selectInstallCommand: (_target, input) => input === "preserve" ? "npm install" : "npm ci",
|
|
391
155
|
}),
|
|
392
156
|
new LockfileBootstrapAdapter({
|
|
393
157
|
name: "bundler",
|
|
394
158
|
kind: "dependency",
|
|
395
|
-
|
|
159
|
+
lockfiles: ["Gemfile.lock"],
|
|
396
160
|
relativePath: "vendor/bundle",
|
|
397
|
-
|
|
398
|
-
shell: false,
|
|
161
|
+
installCommand: "bundle install",
|
|
399
162
|
commandOptions: () => ({
|
|
400
163
|
env: { BUNDLE_PATH: "vendor/bundle" },
|
|
401
164
|
}),
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
165
|
+
}),
|
|
166
|
+
new LockfileBootstrapAdapter({
|
|
167
|
+
name: "poetry",
|
|
168
|
+
kind: "dependency",
|
|
169
|
+
lockfiles: ["poetry.lock"],
|
|
170
|
+
relativePath: ".venv",
|
|
171
|
+
installCommand: "poetry install --no-interaction",
|
|
172
|
+
commandOptions: () => ({
|
|
173
|
+
env: { POETRY_VIRTUALENVS_IN_PROJECT: "true" },
|
|
174
|
+
}),
|
|
407
175
|
}),
|
|
408
176
|
new LockfileBootstrapAdapter({
|
|
409
177
|
name: "uv",
|
|
410
178
|
kind: "dependency",
|
|
411
|
-
|
|
179
|
+
lockfiles: ["uv.lock"],
|
|
180
|
+
relativePath: ".venv",
|
|
181
|
+
installCommand: "uv sync --locked",
|
|
182
|
+
beforeInstall: validateUvStructure,
|
|
183
|
+
afterInstall: validateUvInstallation,
|
|
184
|
+
}),
|
|
185
|
+
new LockfileBootstrapAdapter({
|
|
186
|
+
name: "pipenv",
|
|
187
|
+
kind: "dependency",
|
|
188
|
+
lockfiles: ["Pipfile.lock"],
|
|
412
189
|
relativePath: ".venv",
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
190
|
+
installCommand: "pipenv sync",
|
|
191
|
+
commandOptions: () => ({
|
|
192
|
+
env: { PIPENV_VENV_IN_PROJECT: "1" },
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
new LockfileBootstrapAdapter({
|
|
196
|
+
name: "go",
|
|
197
|
+
kind: "dependency",
|
|
198
|
+
lockfiles: ["go.mod"],
|
|
199
|
+
relativePath: "",
|
|
200
|
+
installCommand: "go mod download",
|
|
201
|
+
}),
|
|
202
|
+
new LockfileBootstrapAdapter({
|
|
203
|
+
name: "composer",
|
|
204
|
+
kind: "dependency",
|
|
205
|
+
lockfiles: ["composer.lock"],
|
|
206
|
+
relativePath: "vendor",
|
|
207
|
+
installCommand: "composer install --no-interaction --prefer-dist",
|
|
418
208
|
}),
|
|
419
209
|
new LockfileBootstrapAdapter({
|
|
420
210
|
name: "cargo",
|
|
421
211
|
kind: "build-cache",
|
|
422
|
-
|
|
212
|
+
lockfiles: ["Cargo.lock"],
|
|
423
213
|
relativePath: "target",
|
|
424
|
-
|
|
214
|
+
installCommand: cargoBuildCommand?.trim() || "cargo check",
|
|
425
215
|
shell: Boolean(cargoBuildCommand),
|
|
426
216
|
}),
|
|
427
217
|
];
|
|
428
218
|
}
|
|
429
219
|
class LockfileBootstrapAdapter {
|
|
430
220
|
kind;
|
|
431
|
-
lockfile;
|
|
432
221
|
name;
|
|
433
222
|
relativePath;
|
|
434
|
-
|
|
223
|
+
defaultInstallCommand;
|
|
224
|
+
lockfiles;
|
|
435
225
|
shell;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
beforeRepair;
|
|
439
|
-
afterRepair;
|
|
226
|
+
beforeInstall;
|
|
227
|
+
afterInstall;
|
|
440
228
|
commandOptions;
|
|
441
|
-
|
|
442
|
-
repairState;
|
|
229
|
+
selectInstallCommand;
|
|
443
230
|
constructor(spec) {
|
|
444
231
|
this.name = spec.name;
|
|
445
232
|
this.kind = spec.kind;
|
|
446
|
-
this.
|
|
233
|
+
this.lockfiles = spec.lockfiles;
|
|
447
234
|
this.relativePath = spec.relativePath;
|
|
448
|
-
this.
|
|
449
|
-
this.shell = spec.shell;
|
|
450
|
-
this.
|
|
451
|
-
this.
|
|
452
|
-
this.beforeRepair = spec.beforeRepair;
|
|
453
|
-
this.afterRepair = spec.afterRepair;
|
|
235
|
+
this.defaultInstallCommand = spec.installCommand;
|
|
236
|
+
this.shell = spec.shell ?? false;
|
|
237
|
+
this.beforeInstall = spec.beforeInstall;
|
|
238
|
+
this.afterInstall = spec.afterInstall;
|
|
454
239
|
this.commandOptions = spec.commandOptions;
|
|
455
|
-
this.
|
|
456
|
-
this.repairState = spec.repairState ?? "repaired";
|
|
240
|
+
this.selectInstallCommand = spec.selectInstallCommand;
|
|
457
241
|
}
|
|
458
242
|
async detect(context) {
|
|
459
243
|
const detectionRoot = context.detectionRoot ?? context.sourceRoot;
|
|
460
|
-
|
|
244
|
+
const hasLockfile = await hasExistingPath(detectionRoot, this.lockfiles);
|
|
245
|
+
if (!hasLockfile)
|
|
461
246
|
return null;
|
|
462
|
-
const sourcePath =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
247
|
+
const sourcePath = this.relativePath
|
|
248
|
+
? join(context.sourceRoot, this.relativePath)
|
|
249
|
+
: undefined;
|
|
250
|
+
const targetPath = this.relativePath
|
|
251
|
+
? join(context.worktreePath, this.relativePath)
|
|
252
|
+
: undefined;
|
|
253
|
+
const destinationInspection = targetPath
|
|
254
|
+
? await inspectDestination(context.worktreePath, targetPath)
|
|
255
|
+
: undefined;
|
|
256
|
+
const initialInput = destinationInspection?.kind === "exists" ? "preserve" : "clean";
|
|
466
257
|
const target = {
|
|
467
|
-
adapter: this.name,
|
|
468
258
|
kind: this.kind,
|
|
469
259
|
relativePath: this.relativePath,
|
|
470
260
|
sourceRoot: context.sourceRoot,
|
|
261
|
+
detectionRoot,
|
|
471
262
|
worktreePath: context.worktreePath,
|
|
472
263
|
sourcePath,
|
|
473
264
|
targetPath,
|
|
474
|
-
|
|
475
|
-
repairState: this.repairState,
|
|
265
|
+
installCommand: this.defaultInstallCommand,
|
|
476
266
|
shell: this.shell,
|
|
477
|
-
initialInput,
|
|
478
267
|
};
|
|
479
268
|
return {
|
|
480
269
|
...target,
|
|
481
|
-
|
|
482
|
-
target.repairCommand,
|
|
270
|
+
installCommand: await this.selectCommand(target, initialInput),
|
|
483
271
|
};
|
|
484
272
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
await this.beforeRepair?.(target, context);
|
|
490
|
-
const repairCommand = this.selectRepairCommand?.(target, context.input) ?? target.repairCommand;
|
|
491
|
-
await context.runCommand(repairCommand, target.worktreePath, context.stderr, context.stdout, {
|
|
273
|
+
async install(target, context) {
|
|
274
|
+
await this.beforeInstall?.(target, context);
|
|
275
|
+
const installCommand = await this.selectCommand(target, context.input);
|
|
276
|
+
await context.runCommand(installCommand, target.worktreePath, context.stderr, context.stdout, {
|
|
492
277
|
...this.commandOptions?.(target),
|
|
493
278
|
shell: target.shell,
|
|
494
279
|
});
|
|
495
|
-
await this.
|
|
280
|
+
await this.afterInstall?.(target, context);
|
|
496
281
|
}
|
|
497
|
-
async
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (!(await safeSourceDirectory(target)))
|
|
501
|
-
return false;
|
|
502
|
-
return (await this.canSeedOverride?.(target)) ?? true;
|
|
503
|
-
}
|
|
504
|
-
shouldRetryAfterRepairFailure(context) {
|
|
505
|
-
return context.input === "seed";
|
|
506
|
-
}
|
|
507
|
-
async cleanupAfterRepairFailure(context) {
|
|
508
|
-
if (context.input !== "seed")
|
|
509
|
-
return;
|
|
510
|
-
await rm(context.target.targetPath, { force: true, recursive: true });
|
|
282
|
+
async selectCommand(target, input) {
|
|
283
|
+
return ((await this.selectInstallCommand?.(target, input)) ??
|
|
284
|
+
target.installCommand);
|
|
511
285
|
}
|
|
512
286
|
}
|
|
513
|
-
async function
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
realpath(target.sourcePath),
|
|
518
|
-
]);
|
|
519
|
-
const sourceStats = await lstat(source);
|
|
520
|
-
return sourceStats.isDirectory() && isWithin(root, source);
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
return false;
|
|
287
|
+
async function hasExistingPath(root, paths) {
|
|
288
|
+
for (const path of paths) {
|
|
289
|
+
if (await pathExists(join(root, path)))
|
|
290
|
+
return true;
|
|
524
291
|
}
|
|
292
|
+
return false;
|
|
525
293
|
}
|
|
526
|
-
async function
|
|
294
|
+
async function isYarnBerry(root) {
|
|
295
|
+
if (await pathExists(join(root, ".yarnrc.yml")))
|
|
296
|
+
return true;
|
|
527
297
|
try {
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.match(/^implementation\s*=\s*([^\s#]+)/imu)?.[1]
|
|
534
|
-
?.toLowerCase();
|
|
535
|
-
const sourceInterpreter = join(target.sourcePath, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
536
|
-
const fingerprintScript = "import platform, sys; print(f'{sys.version_info.major}.{sys.version_info.minor}|{platform.machine()}|{sys.implementation.name}')";
|
|
537
|
-
const source = await execFileAsync(sourceInterpreter, [
|
|
538
|
-
"-c",
|
|
539
|
-
fingerprintScript,
|
|
540
|
-
]);
|
|
541
|
-
const sourceFingerprint = source.stdout.trim();
|
|
542
|
-
const [sourceVersion, sourceMachine, sourceImplementation, extra] = sourceFingerprint.split("|");
|
|
543
|
-
return (extra === undefined &&
|
|
544
|
-
sourceVersion === expected &&
|
|
545
|
-
Boolean(sourceMachine) &&
|
|
546
|
-
Boolean(sourceImplementation) &&
|
|
547
|
-
(expectedImplementation === undefined ||
|
|
548
|
-
sourceImplementation.toLowerCase() === expectedImplementation));
|
|
298
|
+
const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
299
|
+
const packageManager = packageJson.packageManager;
|
|
300
|
+
const match = typeof packageManager === "string" &&
|
|
301
|
+
packageManager.match(/^yarn@(\d+)/u);
|
|
302
|
+
return match ? Number(match[1]) >= 2 : false;
|
|
549
303
|
}
|
|
550
304
|
catch {
|
|
551
305
|
return false;
|
|
552
306
|
}
|
|
553
307
|
}
|
|
554
|
-
function virtualEnvironmentPrefixes(text) {
|
|
555
|
-
const prefixes = [];
|
|
556
|
-
for (const line of text.split(/\r?\n/u)) {
|
|
557
|
-
if (!/\bVIRTUAL_ENV\b/u.test(line))
|
|
558
|
-
continue;
|
|
559
|
-
const quotedAssignment = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*(?:"([^"]+)"|'([^']+)')/u);
|
|
560
|
-
const quotedPrefix = quotedAssignment?.[1] ?? quotedAssignment?.[2];
|
|
561
|
-
if (quotedPrefix && isAbsolute(quotedPrefix))
|
|
562
|
-
prefixes.push(quotedPrefix);
|
|
563
|
-
const unquoted = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*([^\s"']+)/u)?.[1];
|
|
564
|
-
if (unquoted && isAbsolute(unquoted))
|
|
565
|
-
prefixes.push(unquoted);
|
|
566
|
-
}
|
|
567
|
-
return uniquePaths(prefixes);
|
|
568
|
-
}
|
|
569
|
-
function pythonPrefixFromShebang(text) {
|
|
570
|
-
const command = text
|
|
571
|
-
.match(/^#!\s*([^\r\n]+)/u)?.[1]
|
|
572
|
-
?.trim()
|
|
573
|
-
.split(/\s+/u)[0];
|
|
574
|
-
if (!command || !isAbsolute(command))
|
|
575
|
-
return undefined;
|
|
576
|
-
if (!/^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(command.split(sep).at(-1) ?? "")) {
|
|
577
|
-
return undefined;
|
|
578
|
-
}
|
|
579
|
-
return dirname(dirname(command));
|
|
580
|
-
}
|
|
581
|
-
function pythonPrefixesFromText(text) {
|
|
582
|
-
const interpreters = [];
|
|
583
|
-
for (const match of text.matchAll(/["'](\/[^"']+)["']/gu)) {
|
|
584
|
-
if (match[1])
|
|
585
|
-
interpreters.push(match[1]);
|
|
586
|
-
}
|
|
587
|
-
for (const match of text.matchAll(/(?:^|\s)(\/[^\s"']+)/gmu)) {
|
|
588
|
-
if (match[1])
|
|
589
|
-
interpreters.push(match[1]);
|
|
590
|
-
}
|
|
591
|
-
return uniquePaths(interpreters.flatMap((interpreter) => {
|
|
592
|
-
const name = interpreter.split(sep).at(-1) ?? "";
|
|
593
|
-
return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name)
|
|
594
|
-
? [dirname(dirname(interpreter))]
|
|
595
|
-
: [];
|
|
596
|
-
}));
|
|
597
|
-
}
|
|
598
|
-
async function validateUvRelocation(target, context) {
|
|
599
|
-
if (context.input === "preserve")
|
|
600
|
-
return;
|
|
601
|
-
const scriptsDirectory = join(target.targetPath, process.platform === "win32" ? "Scripts" : "bin");
|
|
602
|
-
const interpreter = join(scriptsDirectory, process.platform === "win32" ? "python.exe" : "python");
|
|
603
|
-
const { stdout } = await execFileAsync(interpreter, [
|
|
604
|
-
"-c",
|
|
605
|
-
"import os, sys; print(os.path.realpath(sys.prefix))",
|
|
606
|
-
]);
|
|
607
|
-
const [actualPrefix, expectedPrefix] = await Promise.all([
|
|
608
|
-
realpath(stdout.trim()),
|
|
609
|
-
realpath(target.targetPath),
|
|
610
|
-
]);
|
|
611
|
-
if (actualPrefix !== expectedPrefix) {
|
|
612
|
-
throw new Error(`uv environment still points to its source prefix: ${actualPrefix}`);
|
|
613
|
-
}
|
|
614
|
-
const sourcePaths = uniquePaths([
|
|
615
|
-
target.sourcePath,
|
|
616
|
-
await realpath(target.sourcePath).catch(() => undefined),
|
|
617
|
-
]);
|
|
618
|
-
const entries = await readdir(scriptsDirectory, { withFileTypes: true });
|
|
619
|
-
assertSafeUvScriptEntries(entries);
|
|
620
|
-
const environmentName = basename(target.targetPath);
|
|
621
|
-
const acceptedPrefixes = new Set([
|
|
622
|
-
resolve(target.targetPath),
|
|
623
|
-
await realpath(target.targetPath),
|
|
624
|
-
]);
|
|
625
|
-
let validatedBytes = 0;
|
|
626
|
-
for (const path of [
|
|
627
|
-
join(target.targetPath, "pyvenv.cfg"),
|
|
628
|
-
...entries
|
|
629
|
-
.filter((entry) => entry.isFile())
|
|
630
|
-
.map((entry) => join(scriptsDirectory, entry.name)),
|
|
631
|
-
]) {
|
|
632
|
-
const validated = await readBoundedUvTextFile(path, UV_VALIDATION_MAX_TOTAL_BYTES - validatedBytes);
|
|
633
|
-
if (!validated)
|
|
634
|
-
continue;
|
|
635
|
-
validatedBytes += validated.bytes;
|
|
636
|
-
if (validated.text === undefined)
|
|
637
|
-
continue;
|
|
638
|
-
const { text } = validated;
|
|
639
|
-
if (sourcePaths.some((sourcePath) => text.includes(sourcePath))) {
|
|
640
|
-
throw new Error(`uv environment contains a stale source path: ${path}`);
|
|
641
|
-
}
|
|
642
|
-
const shebangPrefix = pythonPrefixFromShebang(text);
|
|
643
|
-
if (shebangPrefix &&
|
|
644
|
-
basename(shebangPrefix) === environmentName &&
|
|
645
|
-
!acceptedPrefixes.has(resolve(shebangPrefix))) {
|
|
646
|
-
throw new Error(`uv launcher points outside its environment: ${path}`);
|
|
647
|
-
}
|
|
648
|
-
if (pythonPrefixesFromText(text)
|
|
649
|
-
.filter((prefix) => basename(prefix) === environmentName)
|
|
650
|
-
.some((prefix) => !acceptedPrefixes.has(resolve(prefix)))) {
|
|
651
|
-
throw new Error(`uv launcher points outside its environment: ${path}`);
|
|
652
|
-
}
|
|
653
|
-
if (virtualEnvironmentPrefixes(text).some((prefix) => !acceptedPrefixes.has(resolve(prefix)))) {
|
|
654
|
-
throw new Error(`uv activation script points outside its environment: ${path}`);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
async function validateUvStructure(target, context) {
|
|
659
|
-
if (context.input === "preserve")
|
|
660
|
-
return;
|
|
661
|
-
const targetStats = await lstat(target.targetPath).catch(() => undefined);
|
|
662
|
-
if (!targetStats)
|
|
663
|
-
return;
|
|
664
|
-
if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
|
|
665
|
-
throw new Error("uv environment must be a real directory");
|
|
666
|
-
}
|
|
667
|
-
const configStats = await lstat(join(target.targetPath, "pyvenv.cfg")).catch(() => undefined);
|
|
668
|
-
if (configStats && (!configStats.isFile() || configStats.isSymbolicLink())) {
|
|
669
|
-
throw new Error("uv pyvenv.cfg must be a regular file");
|
|
670
|
-
}
|
|
671
|
-
const scriptsDirectory = join(target.targetPath, process.platform === "win32" ? "Scripts" : "bin");
|
|
672
|
-
const scriptsStats = await lstat(scriptsDirectory).catch(() => undefined);
|
|
673
|
-
if (!scriptsStats)
|
|
674
|
-
return;
|
|
675
|
-
if (!scriptsStats.isDirectory() || scriptsStats.isSymbolicLink()) {
|
|
676
|
-
throw new Error("uv scripts path must be a real directory");
|
|
677
|
-
}
|
|
678
|
-
assertSafeUvScriptEntries(await readdir(scriptsDirectory, { withFileTypes: true }));
|
|
679
|
-
}
|
|
680
|
-
function assertSafeUvScriptEntries(entries) {
|
|
681
|
-
if (entries.length > UV_VALIDATION_MAX_ENTRIES) {
|
|
682
|
-
throw new Error("uv scripts directory exceeds the validation entry limit");
|
|
683
|
-
}
|
|
684
|
-
for (const entry of entries) {
|
|
685
|
-
if (entry.isSymbolicLink() && !isUvInterpreterName(entry.name)) {
|
|
686
|
-
throw new Error(`uv script must not be a symbolic link: ${entry.name}`);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
async function readBoundedUvTextFile(path, remainingBytes) {
|
|
691
|
-
let handle;
|
|
692
|
-
try {
|
|
693
|
-
handle = await open(path, constants.O_RDONLY |
|
|
694
|
-
(process.platform === "win32" ? 0 : constants.O_NOFOLLOW));
|
|
695
|
-
}
|
|
696
|
-
catch (error) {
|
|
697
|
-
if (isNotFoundError(error))
|
|
698
|
-
return undefined;
|
|
699
|
-
throw error;
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
const stats = await handle.stat();
|
|
703
|
-
if (!stats.isFile())
|
|
704
|
-
return undefined;
|
|
705
|
-
if (stats.size > UV_VALIDATION_MAX_FILE_BYTES) {
|
|
706
|
-
throw new Error(`uv launcher exceeds the validation size limit: ${path}`);
|
|
707
|
-
}
|
|
708
|
-
if (stats.size > remainingBytes) {
|
|
709
|
-
throw new Error("uv launchers exceed the total validation size limit");
|
|
710
|
-
}
|
|
711
|
-
const readLimit = Math.min(UV_VALIDATION_MAX_FILE_BYTES, remainingBytes);
|
|
712
|
-
const contents = await readFilePrefix(handle, readLimit + 1);
|
|
713
|
-
if (contents.byteLength > UV_VALIDATION_MAX_FILE_BYTES) {
|
|
714
|
-
throw new Error(`uv launcher exceeds the validation size limit: ${path}`);
|
|
715
|
-
}
|
|
716
|
-
if (contents.byteLength > remainingBytes) {
|
|
717
|
-
throw new Error("uv launchers exceed the total validation size limit");
|
|
718
|
-
}
|
|
719
|
-
const text = contents.toString("utf8");
|
|
720
|
-
return Buffer.from(text, "utf8").equals(contents)
|
|
721
|
-
? { bytes: contents.byteLength, text }
|
|
722
|
-
: { bytes: contents.byteLength };
|
|
723
|
-
}
|
|
724
|
-
finally {
|
|
725
|
-
await handle.close();
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
async function readFilePrefix(handle, maxBytes) {
|
|
729
|
-
const contents = Buffer.allocUnsafe(maxBytes);
|
|
730
|
-
let offset = 0;
|
|
731
|
-
while (offset < contents.byteLength) {
|
|
732
|
-
const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
|
|
733
|
-
if (bytesRead === 0)
|
|
734
|
-
break;
|
|
735
|
-
offset += bytesRead;
|
|
736
|
-
}
|
|
737
|
-
return contents.subarray(0, offset);
|
|
738
|
-
}
|
|
739
|
-
function isUvInterpreterName(name) {
|
|
740
|
-
return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name);
|
|
741
|
-
}
|
|
742
|
-
async function removeBundlerExtensionMarkers(root) {
|
|
743
|
-
const pending = [root];
|
|
744
|
-
while (pending.length > 0) {
|
|
745
|
-
const current = pending.pop();
|
|
746
|
-
if (!current)
|
|
747
|
-
continue;
|
|
748
|
-
let entries;
|
|
749
|
-
try {
|
|
750
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
751
|
-
}
|
|
752
|
-
catch (error) {
|
|
753
|
-
if (isNotFoundError(error))
|
|
754
|
-
continue;
|
|
755
|
-
throw error;
|
|
756
|
-
}
|
|
757
|
-
for (const entry of entries) {
|
|
758
|
-
const path = join(current, entry.name);
|
|
759
|
-
if (entry.isDirectory())
|
|
760
|
-
pending.push(path);
|
|
761
|
-
else if (entry.isFile() && isBundlerExtensionMarker(root, path)) {
|
|
762
|
-
await rm(path, { force: true });
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
function isBundlerExtensionMarker(root, path) {
|
|
768
|
-
const segments = relative(root, path).split(sep);
|
|
769
|
-
return (segments.length >= 7 &&
|
|
770
|
-
segments[0] === "ruby" &&
|
|
771
|
-
segments[2] === "extensions" &&
|
|
772
|
-
segments.at(-1) === "gem.build_complete");
|
|
773
|
-
}
|
|
774
308
|
function uniquePaths(paths) {
|
|
775
309
|
return [...new Set(paths.filter((path) => Boolean(path)))];
|
|
776
310
|
}
|