@solaqua/gji 0.9.0 → 0.11.0

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 (100) hide show
  1. package/README.md +93 -20
  2. package/dist/back.d.ts +3 -0
  3. package/dist/back.js +19 -7
  4. package/dist/bootstrap-output.d.ts +3 -0
  5. package/dist/bootstrap-output.js +27 -0
  6. package/dist/bootstrap-preview.d.ts +10 -0
  7. package/dist/bootstrap-preview.js +21 -0
  8. package/dist/clean.js +25 -3
  9. package/dist/cli.js +105 -19
  10. package/dist/clone-failure-store.d.ts +18 -0
  11. package/dist/clone-failure-store.js +230 -0
  12. package/dist/command-runner.d.ts +6 -0
  13. package/dist/command-runner.js +38 -0
  14. package/dist/completion.js +7 -2
  15. package/dist/config-command.d.ts +1 -0
  16. package/dist/config-command.js +7 -1
  17. package/dist/config.d.ts +26 -1
  18. package/dist/config.js +110 -6
  19. package/dist/dependency-bootstrap-prompt.d.ts +24 -0
  20. package/dist/dependency-bootstrap-prompt.js +103 -0
  21. package/dist/dependency-bootstrap.d.ts +129 -0
  22. package/dist/dependency-bootstrap.js +566 -0
  23. package/dist/dir-clone.d.ts +33 -0
  24. package/dist/dir-clone.js +505 -0
  25. package/dist/done.d.ts +10 -0
  26. package/dist/done.js +208 -0
  27. package/dist/file-sync.js +71 -22
  28. package/dist/format-bytes.d.ts +1 -0
  29. package/dist/format-bytes.js +14 -0
  30. package/dist/fs-utils.d.ts +4 -0
  31. package/dist/fs-utils.js +26 -0
  32. package/dist/git.d.ts +5 -0
  33. package/dist/git.js +50 -0
  34. package/dist/gji-bundle.mjs +5489 -1206
  35. package/dist/go-resolver.d.ts +35 -0
  36. package/dist/go-resolver.js +185 -0
  37. package/dist/go.d.ts +5 -2
  38. package/dist/go.js +262 -62
  39. package/dist/hooks.d.ts +1 -1
  40. package/dist/hooks.js +13 -7
  41. package/dist/init.js +30 -22
  42. package/dist/install-prompt.d.ts +1 -0
  43. package/dist/install-prompt.js +3 -25
  44. package/dist/navigation-output.d.ts +11 -0
  45. package/dist/navigation-output.js +6 -0
  46. package/dist/new.d.ts +14 -2
  47. package/dist/new.js +434 -36
  48. package/dist/open.d.ts +1 -0
  49. package/dist/open.js +14 -2
  50. package/dist/pr.d.ts +6 -2
  51. package/dist/pr.js +135 -30
  52. package/dist/remove.js +20 -0
  53. package/dist/repo.d.ts +3 -0
  54. package/dist/repo.js +57 -6
  55. package/dist/safe-destination.d.ts +15 -0
  56. package/dist/safe-destination.js +166 -0
  57. package/dist/shell-completion.js +47 -14
  58. package/dist/sync-directories.d.ts +33 -0
  59. package/dist/sync-directories.js +97 -0
  60. package/dist/sync-plan.d.ts +16 -0
  61. package/dist/sync-plan.js +128 -0
  62. package/dist/sync.js +7 -13
  63. package/dist/undo.d.ts +36 -0
  64. package/dist/undo.js +392 -0
  65. package/dist/warp.d.ts +3 -1
  66. package/dist/warp.js +32 -125
  67. package/dist/worktree-bootstrap.d.ts +35 -0
  68. package/dist/worktree-bootstrap.js +93 -0
  69. package/dist/worktree-management.d.ts +1 -0
  70. package/dist/worktree-management.js +23 -4
  71. package/dist/worktree-picker.d.ts +21 -8
  72. package/dist/worktree-picker.js +107 -15
  73. package/dist/worktree-prompts.js +1 -1
  74. package/dist/worktree-source.d.ts +6 -0
  75. package/dist/worktree-source.js +1 -0
  76. package/dist/worktree-sources.d.ts +3 -0
  77. package/dist/worktree-sources.js +31 -0
  78. package/man/man1/gji-back.1 +1 -1
  79. package/man/man1/gji-clean.1 +1 -1
  80. package/man/man1/gji-completion.1 +1 -1
  81. package/man/man1/gji-config.1 +1 -1
  82. package/man/man1/gji-doctor.1 +1 -1
  83. package/man/man1/gji-done.1 +19 -0
  84. package/man/man1/gji-go.1 +9 -3
  85. package/man/man1/gji-history.1 +1 -1
  86. package/man/man1/gji-init.1 +1 -1
  87. package/man/man1/gji-ls.1 +1 -1
  88. package/man/man1/gji-new.1 +21 -3
  89. package/man/man1/gji-open.1 +4 -1
  90. package/man/man1/gji-pr.1 +7 -1
  91. package/man/man1/gji-remove.1 +3 -3
  92. package/man/man1/gji-root.1 +1 -1
  93. package/man/man1/gji-run-hook.1 +1 -1
  94. package/man/man1/gji-status.1 +1 -1
  95. package/man/man1/gji-sync-files.1 +1 -1
  96. package/man/man1/gji-sync.1 +1 -1
  97. package/man/man1/gji-undo.1 +16 -0
  98. package/man/man1/gji-warp.1 +3 -6
  99. package/man/man1/gji.1 +13 -5
  100. package/package.json +3 -1
@@ -0,0 +1,566 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readFile, realpath, rm } from "node:fs/promises";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { cloneFailureScope, defaultCloneFailureStore, } from "./clone-failure-store.js";
6
+ import { runCommand } from "./command-runner.js";
7
+ import { cloneDir, isCloneDestinationExistsError, isCloneInProgressError, isCloneUnsupportedError, } from "./dir-clone.js";
8
+ import { pathExists } from "./fs-utils.js";
9
+ import { inspectDestination } from "./safe-destination.js";
10
+ const execFileAsync = promisify(execFile);
11
+ export async function prepareDependencyBootstrap(mode, context) {
12
+ if (mode === "off")
13
+ return { mode, targets: [] };
14
+ const adapters = createBootstrapAdapters(context.checkUvRuntime ?? defaultCheckUvRuntime, context.cargoBuildCommand);
15
+ const sourceRoots = [];
16
+ for (const sourceRoot of uniquePaths([
17
+ context.repoRoot,
18
+ context.currentRoot,
19
+ ])) {
20
+ if (await isAllowedSourceRoot(context.repoRoot, sourceRoot)) {
21
+ sourceRoots.push(sourceRoot);
22
+ }
23
+ }
24
+ const targets = [];
25
+ const plannedRelativePaths = new Set();
26
+ for (const adapter of adapters) {
27
+ if (plannedRelativePaths.has(adapter.relativePath))
28
+ continue;
29
+ let fallback;
30
+ let selected;
31
+ for (const sourceRoot of sourceRoots) {
32
+ const target = await adapter.detect({
33
+ detectionRoot: context.detectionRoot,
34
+ sourceRoot,
35
+ worktreePath: context.worktreePath,
36
+ });
37
+ if (!target)
38
+ continue;
39
+ const seedable = mode !== "install-only" && (await adapter.canSeed(target));
40
+ const candidate = { adapter, target, seedable };
41
+ fallback ??= candidate;
42
+ if (mode === "install-only" || seedable) {
43
+ selected = candidate;
44
+ break;
45
+ }
46
+ }
47
+ const plannedTarget = selected ?? fallback;
48
+ if (plannedTarget) {
49
+ targets.push(plannedTarget);
50
+ plannedRelativePaths.add(adapter.relativePath);
51
+ }
52
+ }
53
+ return { mode, targets };
54
+ }
55
+ export async function detectDependencyBootstrapCandidate(context) {
56
+ const plan = await prepareDependencyBootstrap("cow-then-repair", context);
57
+ const planned = plan.targets[0];
58
+ if (!planned)
59
+ return null;
60
+ return {
61
+ adapter: planned.adapter.name,
62
+ kind: planned.adapter.kind,
63
+ lockfile: planned.adapter.lockfile,
64
+ target: planned.target.relativePath,
65
+ repairCommand: planned.target.repairCommand,
66
+ seedable: planned.seedable,
67
+ };
68
+ }
69
+ export function previewDependencyBootstrap(plan) {
70
+ return {
71
+ mode: plan.mode,
72
+ targets: plan.targets.map(({ adapter, target, seedable }) => ({
73
+ adapter: adapter.output(target).adapter,
74
+ kind: adapter.kind,
75
+ target: adapter.output(target).target,
76
+ repairCommand: target.repairCommand,
77
+ seedable,
78
+ strategy: bootstrapStrategy(plan.mode, target, seedable),
79
+ })),
80
+ };
81
+ }
82
+ export async function executeDependencyBootstrap(plan, options) {
83
+ if (plan.mode === "off")
84
+ return { mode: plan.mode, ready: true, events: [] };
85
+ const events = [];
86
+ const failureStore = options.failureStore ?? defaultCloneFailureStore;
87
+ const cloneDirectory = options.cloneDirectory ?? cloneDir;
88
+ const execution = {
89
+ runCommand: options.runCommand ?? runCommand,
90
+ stderr: options.stderr ?? (() => undefined),
91
+ stdout: options.stdout ?? ((chunk) => process.stdout.write(chunk)),
92
+ };
93
+ const seededDirectories = new Set(options.seededDirectories ?? []);
94
+ if (plan.targets.length === 0) {
95
+ recordBootstrapEvent(events, options.reporter, {
96
+ adapter: "none",
97
+ kind: "dependency",
98
+ reason: "no-lockfile",
99
+ state: "skipped",
100
+ target: "",
101
+ message: "no supported dependency or build-state lockfile was detected",
102
+ });
103
+ return { mode: plan.mode, ready: true, events };
104
+ }
105
+ for (const { adapter, target, seedable } of plan.targets) {
106
+ await executeBootstrapTarget(adapter, target, seedable, plan.mode, cloneDirectory, failureStore, execution, seededDirectories.has(target.relativePath), events, options.reporter, options.repoRoot);
107
+ }
108
+ return {
109
+ mode: plan.mode,
110
+ ready: !events.some(({ state }) => state === "failed"),
111
+ events,
112
+ };
113
+ }
114
+ async function executeBootstrapTarget(adapter, target, seedable, mode, cloneDirectory, failureStore, execution, seededBySyncDirs, events, reporter, repoRoot) {
115
+ const destinationInspection = await inspectDestination(target.worktreePath, target.targetPath);
116
+ if (destinationInspection.kind === "unsafe") {
117
+ recordBootstrapFailure(events, reporter, adapter, target, destinationInspection.reason, "destination-unsafe");
118
+ return;
119
+ }
120
+ if (mode === "install-only") {
121
+ await repairTarget(adapter, target, execution, false, target.existingBeforeBootstrap ? "preserve" : "empty", events, reporter);
122
+ return;
123
+ }
124
+ let seeded = false;
125
+ const failureScope = seedable
126
+ ? await cloneFailureScope(adapter.seedPath(target), target.targetPath)
127
+ : undefined;
128
+ let ownership = target.existingBeforeBootstrap
129
+ ? "preserve"
130
+ : "empty";
131
+ const destinationExistsNow = destinationInspection.kind === "exists";
132
+ if (target.existingBeforeBootstrap) {
133
+ recordBootstrapEvent(events, reporter, {
134
+ adapter: adapter.name,
135
+ kind: adapter.kind,
136
+ reason: "target-exists",
137
+ state: "skipped",
138
+ target: target.relativePath,
139
+ message: "target already existed; using it as the repair input",
140
+ });
141
+ }
142
+ else if (seededBySyncDirs && destinationExistsNow) {
143
+ if (seedable) {
144
+ seeded = true;
145
+ ownership = "syncDirs";
146
+ recordBootstrapEvent(events, reporter, {
147
+ adapter: adapter.name,
148
+ kind: adapter.kind,
149
+ reason: "generic-seed",
150
+ state: "seeded",
151
+ target: target.relativePath,
152
+ message: "reusing a seed created by syncDirs",
153
+ });
154
+ }
155
+ else {
156
+ ownership = "preserve";
157
+ recordBootstrapEvent(events, reporter, {
158
+ adapter: adapter.name,
159
+ kind: adapter.kind,
160
+ reason: "generic-seed",
161
+ state: "fallback",
162
+ target: target.relativePath,
163
+ message: "syncDirs created a generic target; this adapter uses repair without CoW",
164
+ });
165
+ }
166
+ }
167
+ else if (destinationExistsNow) {
168
+ ownership = "preserve";
169
+ recordBootstrapEvent(events, reporter, {
170
+ adapter: adapter.name,
171
+ kind: adapter.kind,
172
+ reason: "destination-race",
173
+ state: "skipped",
174
+ target: target.relativePath,
175
+ message: "target appeared during bootstrap; preserving it as the repair input",
176
+ });
177
+ }
178
+ else if (seedable &&
179
+ (await failureStore.isCached(repoRoot, target.relativePath, failureScope))) {
180
+ recordBootstrapEvent(events, reporter, {
181
+ adapter: adapter.name,
182
+ kind: adapter.kind,
183
+ reason: "cow-failure-cached",
184
+ state: "fallback",
185
+ target: target.relativePath,
186
+ message: "previous CoW failure is cached; repairing from an empty target",
187
+ });
188
+ }
189
+ else if (seedable) {
190
+ try {
191
+ await cloneDirectory(adapter.seedPath(target), target.targetPath, {
192
+ destinationRoot: target.worktreePath,
193
+ measureBytes: reporter.measureCloneSize,
194
+ });
195
+ await failureStore.clear(repoRoot, target.relativePath, failureScope);
196
+ seeded = true;
197
+ ownership = "adapter";
198
+ recordBootstrapEvent(events, reporter, {
199
+ adapter: adapter.name,
200
+ kind: adapter.kind,
201
+ state: "seeded",
202
+ target: target.relativePath,
203
+ message: "seeded with copy-on-write",
204
+ });
205
+ }
206
+ catch (error) {
207
+ if (isCloneDestinationExistsError(error)) {
208
+ ownership = "preserve";
209
+ recordBootstrapEvent(events, reporter, {
210
+ adapter: adapter.name,
211
+ kind: adapter.kind,
212
+ state: "skipped",
213
+ target: target.relativePath,
214
+ message: "target appeared during CoW seeding; preserving it as the repair input",
215
+ });
216
+ await repairTarget(adapter, target, execution, false, ownership, events, reporter);
217
+ return;
218
+ }
219
+ if (isCloneInProgressError(error)) {
220
+ recordBootstrapFailure(events, reporter, adapter, target, `CoW seed is already in progress: ${toErrorMessage(error)}`, "clone-in-progress");
221
+ return;
222
+ }
223
+ if (isCloneUnsupportedError(error)) {
224
+ await failureStore.cache(repoRoot, target.relativePath, toErrorMessage(error), failureScope);
225
+ }
226
+ recordBootstrapEvent(events, reporter, {
227
+ adapter: adapter.name,
228
+ kind: adapter.kind,
229
+ reason: "cow-unsupported",
230
+ state: "fallback",
231
+ target: target.relativePath,
232
+ message: `CoW seed failed; repairing from an empty target (${toErrorMessage(error)})`,
233
+ });
234
+ }
235
+ }
236
+ else {
237
+ recordBootstrapEvent(events, reporter, {
238
+ adapter: adapter.name,
239
+ kind: adapter.kind,
240
+ reason: "seed-unavailable",
241
+ state: "fallback",
242
+ target: target.relativePath,
243
+ message: "CoW seed is unavailable; repairing from an empty target",
244
+ });
245
+ }
246
+ await repairTarget(adapter, target, execution, seeded, ownership, events, reporter);
247
+ }
248
+ async function repairTarget(adapter, target, execution, seeded, ownership, events, reporter) {
249
+ const beforeRepairInspection = await inspectDestination(target.worktreePath, target.targetPath);
250
+ if (beforeRepairInspection.kind === "unsafe") {
251
+ recordBootstrapFailure(events, reporter, adapter, target, beforeRepairInspection.reason, "destination-unsafe");
252
+ return;
253
+ }
254
+ const presentBeforeRepair = beforeRepairInspection.kind === "exists";
255
+ try {
256
+ await adapter.repair(target, execution);
257
+ recordBootstrapEvent(events, reporter, {
258
+ adapter: adapter.name,
259
+ kind: adapter.kind,
260
+ state: target.repairState,
261
+ target: target.relativePath,
262
+ message: seeded
263
+ ? "reused and repaired"
264
+ : "installed or repaired from a clean target",
265
+ });
266
+ }
267
+ catch (firstError) {
268
+ const firstFailureContext = {
269
+ target,
270
+ ownership,
271
+ targetExistedBeforeRepair: presentBeforeRepair,
272
+ };
273
+ const cleanupError = await adapter
274
+ .cleanupAfterRepairFailure(firstFailureContext)
275
+ .then(() => undefined)
276
+ .catch((error) => toErrorMessage(error));
277
+ if (!adapter.shouldRetryAfterRepairFailure(firstFailureContext)) {
278
+ recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(firstError, cleanupError === undefined ? undefined : cleanupError), "repair-failed");
279
+ return;
280
+ }
281
+ if (cleanupError) {
282
+ recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(firstError, cleanupError), "repair-cleanup-failed");
283
+ return;
284
+ }
285
+ recordBootstrapEvent(events, reporter, {
286
+ adapter: adapter.name,
287
+ kind: adapter.kind,
288
+ reason: "seed-repair-failed",
289
+ state: "fallback",
290
+ target: target.relativePath,
291
+ message: `seed repair failed; removed the seed and retrying clean (${toErrorMessage(firstError)})`,
292
+ });
293
+ const beforeRetryInspection = await inspectDestination(target.worktreePath, target.targetPath);
294
+ if (beforeRetryInspection.kind === "unsafe") {
295
+ recordBootstrapFailure(events, reporter, adapter, target, beforeRetryInspection.reason, "destination-unsafe");
296
+ return;
297
+ }
298
+ const presentBeforeRetry = beforeRetryInspection.kind === "exists";
299
+ try {
300
+ await adapter.repair(target, execution);
301
+ recordBootstrapEvent(events, reporter, {
302
+ adapter: adapter.name,
303
+ kind: adapter.kind,
304
+ reason: "repair-retry",
305
+ state: target.repairState,
306
+ target: target.relativePath,
307
+ message: "installed or repaired from a clean target",
308
+ });
309
+ }
310
+ catch (secondError) {
311
+ const cleanupError = await adapter
312
+ .cleanupAfterRepairFailure({
313
+ target,
314
+ ownership,
315
+ targetExistedBeforeRepair: presentBeforeRetry,
316
+ })
317
+ .then(() => undefined)
318
+ .catch((error) => toErrorMessage(error));
319
+ recordBootstrapFailure(events, reporter, adapter, target, formatRepairFailure(secondError, cleanupError), "repair-failed");
320
+ }
321
+ }
322
+ }
323
+ function recordBootstrapFailure(events, reporter, adapter, target, message, reason) {
324
+ recordBootstrapEvent(events, reporter, {
325
+ adapter: adapter.name,
326
+ kind: adapter.kind,
327
+ reason,
328
+ state: "failed",
329
+ target: target.relativePath,
330
+ message,
331
+ });
332
+ }
333
+ function recordBootstrapEvent(events, reporter, event) {
334
+ events.push(event);
335
+ reporter.dependency(event);
336
+ }
337
+ function formatRepairFailure(error, cleanupError) {
338
+ const message = `repair failed: ${toErrorMessage(error)}`;
339
+ return cleanupError ? `${message}; cleanup failed: ${cleanupError}` : message;
340
+ }
341
+ function bootstrapStrategy(mode, target, seedable) {
342
+ if (mode === "install-only" || target.repairState === "installed") {
343
+ return "install-only";
344
+ }
345
+ return seedable ? "cow-then-repair" : "repair-only";
346
+ }
347
+ function createBootstrapAdapters(checkUvRuntime, cargoBuildCommand) {
348
+ return [
349
+ new LockfileBootstrapAdapter({
350
+ name: "pnpm",
351
+ kind: "dependency",
352
+ lockfile: "pnpm-lock.yaml",
353
+ relativePath: "node_modules",
354
+ repairCommand: "pnpm install --frozen-lockfile",
355
+ shell: false,
356
+ beforeRepair: async (target) => {
357
+ if (!target.existingBeforeBootstrap) {
358
+ await rm(join(target.targetPath, ".modules.yaml"), {
359
+ force: true,
360
+ });
361
+ }
362
+ },
363
+ }),
364
+ new LockfileBootstrapAdapter({
365
+ name: "yarn",
366
+ kind: "dependency",
367
+ lockfile: "yarn.lock",
368
+ relativePath: "node_modules",
369
+ repairCommand: "yarn install --immutable",
370
+ shell: false,
371
+ }),
372
+ new LockfileBootstrapAdapter({
373
+ name: "npm",
374
+ kind: "dependency",
375
+ lockfile: "package-lock.json",
376
+ relativePath: "node_modules",
377
+ repairCommand: "npm ci",
378
+ shell: false,
379
+ seedPolicy: "never",
380
+ repairCommandOverride: (target) => target.existingBeforeBootstrap ? "npm install" : "npm ci",
381
+ repairState: "installed",
382
+ }),
383
+ new LockfileBootstrapAdapter({
384
+ name: "bundler",
385
+ kind: "dependency",
386
+ lockfile: "Gemfile.lock",
387
+ relativePath: "vendor/bundle",
388
+ repairCommand: "bundle install",
389
+ shell: false,
390
+ commandOptions: () => ({
391
+ env: { BUNDLE_PATH: "vendor/bundle" },
392
+ }),
393
+ }),
394
+ new LockfileBootstrapAdapter({
395
+ name: "uv",
396
+ kind: "dependency",
397
+ lockfile: "uv.lock",
398
+ relativePath: ".venv",
399
+ repairCommand: "uv sync --locked",
400
+ shell: false,
401
+ canSeedOverride: checkUvRuntime,
402
+ }),
403
+ new LockfileBootstrapAdapter({
404
+ name: "cargo",
405
+ kind: "build-cache",
406
+ lockfile: "Cargo.lock",
407
+ relativePath: "target",
408
+ repairCommand: cargoBuildCommand?.trim() || "cargo check",
409
+ shell: cargoBuildCommand ? undefined : false,
410
+ }),
411
+ ];
412
+ }
413
+ class LockfileBootstrapAdapter {
414
+ kind;
415
+ lockfile;
416
+ name;
417
+ relativePath;
418
+ defaultRepairCommand;
419
+ shell;
420
+ seedPolicy;
421
+ canSeedOverride;
422
+ beforeRepair;
423
+ commandOptions;
424
+ repairCommandOverride;
425
+ repairState;
426
+ constructor(spec) {
427
+ this.name = spec.name;
428
+ this.kind = spec.kind;
429
+ this.lockfile = spec.lockfile;
430
+ this.relativePath = spec.relativePath;
431
+ this.defaultRepairCommand = spec.repairCommand;
432
+ this.shell = spec.shell;
433
+ this.seedPolicy = spec.seedPolicy ?? "always";
434
+ this.canSeedOverride = spec.canSeedOverride;
435
+ this.beforeRepair = spec.beforeRepair;
436
+ this.commandOptions = spec.commandOptions;
437
+ this.repairCommandOverride = spec.repairCommandOverride;
438
+ this.repairState = spec.repairState ?? "repaired";
439
+ }
440
+ async detect(context) {
441
+ const detectionRoot = context.detectionRoot ?? context.sourceRoot;
442
+ if (!(await pathExists(join(detectionRoot, this.lockfile))))
443
+ return null;
444
+ const sourcePath = join(context.sourceRoot, this.relativePath);
445
+ const targetPath = join(context.worktreePath, this.relativePath);
446
+ const destinationInspection = await inspectDestination(context.worktreePath, targetPath);
447
+ const target = {
448
+ adapter: this.name,
449
+ kind: this.kind,
450
+ relativePath: this.relativePath,
451
+ sourceRoot: context.sourceRoot,
452
+ worktreePath: context.worktreePath,
453
+ sourcePath,
454
+ targetPath,
455
+ repairCommand: this.defaultRepairCommand,
456
+ repairState: this.repairState,
457
+ existingBeforeBootstrap: destinationInspection.kind === "exists",
458
+ };
459
+ return {
460
+ ...target,
461
+ repairCommand: this.repairCommandOverride?.(target) ?? target.repairCommand,
462
+ };
463
+ }
464
+ seedPath(target) {
465
+ return target.sourcePath;
466
+ }
467
+ async repair(target, context) {
468
+ await this.beforeRepair?.(target);
469
+ await context.runCommand(target.repairCommand, target.worktreePath, context.stderr, context.stdout, {
470
+ ...this.commandOptions?.(target),
471
+ shell: this.shell,
472
+ });
473
+ }
474
+ async canSeed(target) {
475
+ if (this.seedPolicy === "never")
476
+ return false;
477
+ if (!(await safeSourceDirectory(target)))
478
+ return false;
479
+ return (await this.canSeedOverride?.(target)) ?? true;
480
+ }
481
+ output(target) {
482
+ return {
483
+ adapter: this.name,
484
+ kind: this.kind,
485
+ target: target.relativePath,
486
+ repairCommand: target.repairCommand,
487
+ };
488
+ }
489
+ shouldRetryAfterRepairFailure(context) {
490
+ return context.ownership === "adapter" || context.ownership === "syncDirs";
491
+ }
492
+ async cleanupAfterRepairFailure(context) {
493
+ if (context.target.existingBeforeBootstrap) {
494
+ return;
495
+ }
496
+ if (context.ownership === "adapter" || context.ownership === "syncDirs") {
497
+ await rm(context.target.targetPath, { force: true, recursive: true });
498
+ return;
499
+ }
500
+ if (context.ownership === "empty" && !context.targetExistedBeforeRepair) {
501
+ await rm(context.target.targetPath, { force: true, recursive: true });
502
+ }
503
+ }
504
+ }
505
+ async function safeSourceDirectory(target) {
506
+ try {
507
+ const [root, source] = await Promise.all([
508
+ realpath(target.sourceRoot),
509
+ realpath(target.sourcePath),
510
+ ]);
511
+ const sourceStats = await lstat(source);
512
+ return sourceStats.isDirectory() && isWithin(root, source);
513
+ }
514
+ catch {
515
+ return false;
516
+ }
517
+ }
518
+ async function defaultCheckUvRuntime(target) {
519
+ try {
520
+ const config = await readFile(join(target.sourcePath, "pyvenv.cfg"), "utf8");
521
+ const expected = config.match(/^version\s*=\s*(\d+\.\d+)/mu)?.[1];
522
+ if (!expected)
523
+ return false;
524
+ const sourceInterpreter = join(target.sourcePath, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
525
+ const fingerprintScript = "import platform, sys; print(f'{sys.version_info.major}.{sys.version_info.minor}|{platform.machine()}|{sys.implementation.cache_tag}')";
526
+ const [source, current] = await Promise.all([
527
+ execFileAsync(sourceInterpreter, ["-c", fingerprintScript]),
528
+ execFileAsync("python3", ["-c", fingerprintScript]),
529
+ ]);
530
+ const sourceFingerprint = source.stdout.trim();
531
+ const currentFingerprint = current.stdout.trim();
532
+ return (sourceFingerprint === currentFingerprint &&
533
+ sourceFingerprint.startsWith(`${expected}|`));
534
+ }
535
+ catch {
536
+ return false;
537
+ }
538
+ }
539
+ function uniquePaths(paths) {
540
+ return [...new Set(paths.filter((path) => Boolean(path)))];
541
+ }
542
+ async function isAllowedSourceRoot(repoRoot, sourceRoot) {
543
+ try {
544
+ const [resolvedRepoRoot, resolvedSourceRoot] = await Promise.all([
545
+ realpath(repoRoot),
546
+ realpath(sourceRoot),
547
+ ]);
548
+ if (resolvedRepoRoot === resolvedSourceRoot)
549
+ return true;
550
+ const gitFile = await readFile(join(resolvedSourceRoot, ".git"), "utf8");
551
+ const gitDirectory = gitFile.match(/^gitdir:\s*(.+)$/mu)?.[1];
552
+ if (!gitDirectory)
553
+ return false;
554
+ return isWithin(join(resolvedRepoRoot, ".git", "worktrees"), resolve(resolvedSourceRoot, gitDirectory));
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ }
560
+ function isWithin(root, candidate) {
561
+ const distance = relative(root, candidate);
562
+ return (distance === "" || (distance !== ".." && !distance.startsWith(`..${sep}`)));
563
+ }
564
+ function toErrorMessage(error) {
565
+ return error instanceof Error ? error.message : String(error);
566
+ }
@@ -0,0 +1,33 @@
1
+ export interface CloneDirResult {
2
+ bytes?: number;
3
+ ms: number;
4
+ }
5
+ export interface CloneRequestOptions {
6
+ destinationRoot?: string;
7
+ measureBytes?: boolean;
8
+ }
9
+ export interface CloneDirOptions extends CloneRequestOptions {
10
+ platform?: NodeJS.Platform;
11
+ runCommand?: (command: string, args: string[]) => Promise<void>;
12
+ copyDirectory?: (source: string, destination: string) => Promise<void>;
13
+ copyFile?: (source: string, destination: string) => Promise<void>;
14
+ }
15
+ export type CloneDirectory = (source: string, destination: string, options?: CloneRequestOptions) => Promise<CloneDirResult>;
16
+ export declare function waitForCloneLock(destination: string, timeoutMs?: number): Promise<boolean>;
17
+ export declare function cloneDir(source: string, destination: string, options?: CloneDirOptions): Promise<CloneDirResult>;
18
+ export declare class CloneDestinationExistsError extends Error {
19
+ readonly code = "GJI_CLONE_DESTINATION_EXISTS";
20
+ constructor(destination: string);
21
+ }
22
+ export declare class CloneUnsupportedError extends Error {
23
+ readonly code = "GJI_CLONE_UNSUPPORTED";
24
+ constructor(reason: string);
25
+ }
26
+ export declare class CloneInProgressError extends Error {
27
+ readonly code = "GJI_CLONE_IN_PROGRESS";
28
+ constructor(destination: string);
29
+ }
30
+ export declare function isCloneDestinationExistsError(error: unknown): error is CloneDestinationExistsError;
31
+ export declare function isCloneUnsupportedError(error: unknown): boolean;
32
+ export declare function isCloneInProgressError(error: unknown): error is CloneInProgressError;
33
+ export declare function directorySize(path: string): Promise<number>;