@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.
Files changed (55) hide show
  1. package/dist/bootstrap-output.d.ts +3 -2
  2. package/dist/bootstrap-output.js +1 -16
  3. package/dist/bootstrap-preview.d.ts +2 -3
  4. package/dist/bootstrap-preview.js +1 -11
  5. package/dist/cli.js +5 -1
  6. package/dist/config.d.ts +3 -13
  7. package/dist/config.js +12 -59
  8. package/dist/dependency-bootstrap.d.ts +11 -45
  9. package/dist/dependency-bootstrap.js +142 -608
  10. package/dist/gji-bundle.mjs +567 -2367
  11. package/dist/new.d.ts +4 -5
  12. package/dist/new.js +10 -45
  13. package/dist/pr.d.ts +4 -5
  14. package/dist/pr.js +9 -69
  15. package/dist/shell-completion.js +7 -5
  16. package/dist/uv-validation.d.ts +4 -0
  17. package/dist/uv-validation.js +210 -0
  18. package/dist/worktree-bootstrap.d.ts +5 -16
  19. package/dist/worktree-bootstrap.js +1 -24
  20. package/man/man1/gji-back.1 +1 -1
  21. package/man/man1/gji-clean.1 +1 -1
  22. package/man/man1/gji-completion.1 +1 -1
  23. package/man/man1/gji-config.1 +1 -1
  24. package/man/man1/gji-doctor.1 +1 -1
  25. package/man/man1/gji-done.1 +1 -1
  26. package/man/man1/gji-go.1 +1 -1
  27. package/man/man1/gji-history.1 +1 -1
  28. package/man/man1/gji-init.1 +1 -1
  29. package/man/man1/gji-ls.1 +1 -1
  30. package/man/man1/gji-new.1 +8 -5
  31. package/man/man1/gji-open.1 +1 -1
  32. package/man/man1/gji-pr.1 +6 -3
  33. package/man/man1/gji-remove.1 +1 -1
  34. package/man/man1/gji-root.1 +1 -1
  35. package/man/man1/gji-run-hook.1 +1 -1
  36. package/man/man1/gji-status.1 +1 -1
  37. package/man/man1/gji-sync-files.1 +1 -1
  38. package/man/man1/gji-sync.1 +1 -1
  39. package/man/man1/gji-task.1 +1 -1
  40. package/man/man1/gji-undo.1 +1 -1
  41. package/man/man1/gji-warp.1 +1 -1
  42. package/man/man1/gji.1 +2 -2
  43. package/package.json +1 -1
  44. package/dist/dependency-bootstrap-prompt.d.ts +0 -24
  45. package/dist/dependency-bootstrap-prompt.js +0 -103
  46. package/dist/dir-clone.d.ts +0 -32
  47. package/dist/dir-clone.js +0 -641
  48. package/dist/install-prompt.d.ts +0 -12
  49. package/dist/install-prompt.js +0 -127
  50. package/dist/package-manager.d.ts +0 -5
  51. package/dist/package-manager.js +0 -159
  52. package/dist/sync-directories.d.ts +0 -29
  53. package/dist/sync-directories.js +0 -77
  54. package/dist/sync-plan.d.ts +0 -16
  55. package/dist/sync-plan.js +0 -128
@@ -1,20 +1,18 @@
1
- import { execFile } from "node:child_process";
2
- import { constants } from "node:fs";
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 { cloneDir, isCloneDestinationExistsError, isCloneInProgressError, } from "./dir-clone.js";
8
- import { isNotFoundError, pathExists } from "./fs-utils.js";
4
+ import { pathExists } from "./fs-utils.js";
9
5
  import { inspectDestination } from "./safe-destination.js";
10
- const execFileAsync = promisify(execFile);
11
- const UV_VALIDATION_MAX_ENTRIES = 10_000;
12
- const UV_VALIDATION_MAX_FILE_BYTES = 1024 * 1024;
13
- const UV_VALIDATION_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
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.checkUvRuntime ?? defaultCheckUvRuntime, context.cargoBuildCommand);
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
- const seedable = mode !== "install-only" && (await adapter.canSeed(target));
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(({ target, seedable }) => ({
76
- adapter: target.adapter,
77
- kind: target.kind,
48
+ targets: plan.targets.map(({ adapter, target }) => ({
49
+ adapter: adapter.name,
50
+ kind: adapter.kind,
78
51
  target: target.relativePath,
79
- repairCommand: target.repairCommand,
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 { target, seedable } of plan.targets) {
108
- const adapter = adapterForTarget(target);
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 executeBootstrapTarget(adapter, target, seedable, mode, cloneDirectory, execution, seededBySyncDirs, events, reporter) {
118
- const destinationInspection = await inspectDestination(target.worktreePath, target.targetPath);
119
- if (destinationInspection.kind === "unsafe") {
120
- recordBootstrapFailure(events, reporter, adapter, target, destinationInspection.reason, "destination-unsafe");
121
- return;
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 = beforeRepairInspection.kind === "exists"
242
- ? input === "seed"
243
- ? "seed"
244
- : "preserve"
245
- : "clean";
85
+ const effectiveInput = beforeInstallInspection?.kind === "exists" ? "preserve" : "clean";
246
86
  try {
247
- await adapter.repair(target, { ...execution, input: effectiveInput });
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: target.repairState,
91
+ state: "installed",
252
92
  target: target.relativePath,
253
- message: repairSuccessMessage(target, effectiveInput),
93
+ message: installSuccessMessage(target, effectiveInput),
254
94
  });
255
95
  }
256
- catch (firstError) {
257
- const firstFailureContext = {
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 repairSuccessMessage(target, input) {
311
- if (input === "seed")
312
- return "reused and repaired";
100
+ function installSuccessMessage(_target, input) {
313
101
  if (input === "preserve")
314
- return "repaired the existing target";
315
- return target.repairState === "installed"
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 formatRepairFailure(error, cleanupError) {
334
- const message = `repair failed: ${toErrorMessage(error)}`;
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 adapterForTarget(target) {
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
- lockfile: "pnpm-lock.yaml",
127
+ lockfiles: ["pnpm-lock.yaml"],
355
128
  relativePath: "node_modules",
356
- repairCommand: "pnpm install --frozen-lockfile",
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
- lockfile: "yarn.lock",
134
+ lockfiles: ["yarn.lock"],
370
135
  relativePath: "node_modules",
371
- repairCommand: "yarn install --immutable",
372
- shell: false,
373
- beforeRepair: async (target, context) => {
374
- if (context.input === "seed") {
375
- await rm(join(target.targetPath, ".yarn-state.yml"), {
376
- force: true,
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
- lockfile: "package-lock.json",
151
+ lockfiles: ["package-lock.json"],
385
152
  relativePath: "node_modules",
386
- repairCommand: "npm ci",
387
- shell: false,
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
- lockfile: "Gemfile.lock",
159
+ lockfiles: ["Gemfile.lock"],
396
160
  relativePath: "vendor/bundle",
397
- repairCommand: "bundle install",
398
- shell: false,
161
+ installCommand: "bundle install",
399
162
  commandOptions: () => ({
400
163
  env: { BUNDLE_PATH: "vendor/bundle" },
401
164
  }),
402
- beforeRepair: async (target, context) => {
403
- if (context.input === "seed") {
404
- await removeBundlerExtensionMarkers(target.targetPath);
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
- lockfile: "uv.lock",
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
- repairCommand: "uv sync --locked",
414
- shell: false,
415
- canSeedOverride: checkUvRuntime,
416
- beforeRepair: validateUvStructure,
417
- afterRepair: validateUvRelocation,
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
- lockfile: "Cargo.lock",
212
+ lockfiles: ["Cargo.lock"],
423
213
  relativePath: "target",
424
- repairCommand: cargoBuildCommand?.trim() || "cargo check",
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
- defaultRepairCommand;
223
+ defaultInstallCommand;
224
+ lockfiles;
435
225
  shell;
436
- seedPolicy;
437
- canSeedOverride;
438
- beforeRepair;
439
- afterRepair;
226
+ beforeInstall;
227
+ afterInstall;
440
228
  commandOptions;
441
- selectRepairCommand;
442
- repairState;
229
+ selectInstallCommand;
443
230
  constructor(spec) {
444
231
  this.name = spec.name;
445
232
  this.kind = spec.kind;
446
- this.lockfile = spec.lockfile;
233
+ this.lockfiles = spec.lockfiles;
447
234
  this.relativePath = spec.relativePath;
448
- this.defaultRepairCommand = spec.repairCommand;
449
- this.shell = spec.shell;
450
- this.seedPolicy = spec.seedPolicy ?? "always";
451
- this.canSeedOverride = spec.canSeedOverride;
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.selectRepairCommand = spec.selectRepairCommand;
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
- if (!(await pathExists(join(detectionRoot, this.lockfile))))
244
+ const hasLockfile = await hasExistingPath(detectionRoot, this.lockfiles);
245
+ if (!hasLockfile)
461
246
  return null;
462
- const sourcePath = join(context.sourceRoot, this.relativePath);
463
- const targetPath = join(context.worktreePath, this.relativePath);
464
- const destinationInspection = await inspectDestination(context.worktreePath, targetPath);
465
- const initialInput = destinationInspection.kind === "exists" ? "preserve" : "clean";
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
- repairCommand: this.defaultRepairCommand,
475
- repairState: this.repairState,
265
+ installCommand: this.defaultInstallCommand,
476
266
  shell: this.shell,
477
- initialInput,
478
267
  };
479
268
  return {
480
269
  ...target,
481
- repairCommand: this.selectRepairCommand?.(target, initialInput) ??
482
- target.repairCommand,
270
+ installCommand: await this.selectCommand(target, initialInput),
483
271
  };
484
272
  }
485
- seedPath(target) {
486
- return target.sourcePath;
487
- }
488
- async repair(target, context) {
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.afterRepair?.(target, context);
280
+ await this.afterInstall?.(target, context);
496
281
  }
497
- async canSeed(target) {
498
- if (this.seedPolicy === "never")
499
- return false;
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 safeSourceDirectory(target) {
514
- try {
515
- const [root, source] = await Promise.all([
516
- realpath(target.sourceRoot),
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 defaultCheckUvRuntime(target) {
294
+ async function isYarnBerry(root) {
295
+ if (await pathExists(join(root, ".yarnrc.yml")))
296
+ return true;
527
297
  try {
528
- const config = await readFile(join(target.sourcePath, "pyvenv.cfg"), "utf8");
529
- const expected = config.match(/^version(?:_info)?\s*=\s*(\d+\.\d+)/mu)?.[1];
530
- if (!expected)
531
- return false;
532
- const expectedImplementation = config
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
  }