@solaqua/gji 0.12.1 → 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 -2365
  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
@@ -3462,7 +3462,7 @@ var require_commander = __commonJS({
3462
3462
  // node_modules/.pnpm/graceful-fs@4.2.11/node_modules/graceful-fs/polyfills.js
3463
3463
  var require_polyfills = __commonJS({
3464
3464
  "node_modules/.pnpm/graceful-fs@4.2.11/node_modules/graceful-fs/polyfills.js"(exports, module) {
3465
- var constants10 = __require("constants");
3465
+ var constants9 = __require("constants");
3466
3466
  var origCwd = process.cwd;
3467
3467
  var cwd = null;
3468
3468
  var platform3 = process.env.GRACEFUL_FS_PLATFORM || process.platform;
@@ -3486,7 +3486,7 @@ var require_polyfills = __commonJS({
3486
3486
  var chdir;
3487
3487
  module.exports = patch;
3488
3488
  function patch(fs5) {
3489
- if (constants10.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
3489
+ if (constants9.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
3490
3490
  patchLchmod(fs5);
3491
3491
  }
3492
3492
  if (!fs5.lutimes) {
@@ -3526,7 +3526,7 @@ var require_polyfills = __commonJS({
3526
3526
  }
3527
3527
  if (platform3 === "win32") {
3528
3528
  fs5.rename = typeof fs5.rename !== "function" ? fs5.rename : (function(fs$rename) {
3529
- function rename4(from, to, cb) {
3529
+ function rename3(from, to, cb) {
3530
3530
  var start = Date.now();
3531
3531
  var backoff = 0;
3532
3532
  fs$rename(from, to, function CB(er) {
@@ -3546,8 +3546,8 @@ var require_polyfills = __commonJS({
3546
3546
  if (cb) cb(er);
3547
3547
  });
3548
3548
  }
3549
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename4, fs$rename);
3550
- return rename4;
3549
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename3, fs$rename);
3550
+ return rename3;
3551
3551
  })(fs5.rename);
3552
3552
  }
3553
3553
  fs5.read = typeof fs5.read !== "function" ? fs5.read : (function(fs$read) {
@@ -3588,7 +3588,7 @@ var require_polyfills = __commonJS({
3588
3588
  fs6.lchmod = function(path9, mode, callback) {
3589
3589
  fs6.open(
3590
3590
  path9,
3591
- constants10.O_WRONLY | constants10.O_SYMLINK,
3591
+ constants9.O_WRONLY | constants9.O_SYMLINK,
3592
3592
  mode,
3593
3593
  function(err, fd) {
3594
3594
  if (err) {
@@ -3604,7 +3604,7 @@ var require_polyfills = __commonJS({
3604
3604
  );
3605
3605
  };
3606
3606
  fs6.lchmodSync = function(path9, mode) {
3607
- var fd = fs6.openSync(path9, constants10.O_WRONLY | constants10.O_SYMLINK, mode);
3607
+ var fd = fs6.openSync(path9, constants9.O_WRONLY | constants9.O_SYMLINK, mode);
3608
3608
  var threw = true;
3609
3609
  var ret;
3610
3610
  try {
@@ -3624,9 +3624,9 @@ var require_polyfills = __commonJS({
3624
3624
  };
3625
3625
  }
3626
3626
  function patchLutimes(fs6) {
3627
- if (constants10.hasOwnProperty("O_SYMLINK") && fs6.futimes) {
3627
+ if (constants9.hasOwnProperty("O_SYMLINK") && fs6.futimes) {
3628
3628
  fs6.lutimes = function(path9, at, mt, cb) {
3629
- fs6.open(path9, constants10.O_SYMLINK, function(er, fd) {
3629
+ fs6.open(path9, constants9.O_SYMLINK, function(er, fd) {
3630
3630
  if (er) {
3631
3631
  if (cb) cb(er);
3632
3632
  return;
@@ -3639,7 +3639,7 @@ var require_polyfills = __commonJS({
3639
3639
  });
3640
3640
  };
3641
3641
  fs6.lutimesSync = function(path9, at, mt) {
3642
- var fd = fs6.openSync(path9, constants10.O_SYMLINK);
3642
+ var fd = fs6.openSync(path9, constants9.O_SYMLINK);
3643
3643
  var ret;
3644
3644
  var threw = true;
3645
3645
  try {
@@ -3951,8 +3951,8 @@ var require_graceful_fs = __commonJS({
3951
3951
  fs6.createReadStream = createReadStream;
3952
3952
  fs6.createWriteStream = createWriteStream;
3953
3953
  var fs$readFile = fs6.readFile;
3954
- fs6.readFile = readFile11;
3955
- function readFile11(path9, options, cb) {
3954
+ fs6.readFile = readFile10;
3955
+ function readFile10(path9, options, cb) {
3956
3956
  if (typeof options === "function")
3957
3957
  cb = options, options = null;
3958
3958
  return go$readFile(path9, options, cb);
@@ -3968,8 +3968,8 @@ var require_graceful_fs = __commonJS({
3968
3968
  }
3969
3969
  }
3970
3970
  var fs$writeFile = fs6.writeFile;
3971
- fs6.writeFile = writeFile11;
3972
- function writeFile11(path9, data, options, cb) {
3971
+ fs6.writeFile = writeFile10;
3972
+ function writeFile10(path9, data, options, cb) {
3973
3973
  if (typeof options === "function")
3974
3974
  cb = options, options = null;
3975
3975
  return go$writeFile(path9, data, options, cb);
@@ -4023,9 +4023,9 @@ var require_graceful_fs = __commonJS({
4023
4023
  }
4024
4024
  }
4025
4025
  var fs$readdir = fs6.readdir;
4026
- fs6.readdir = readdir4;
4026
+ fs6.readdir = readdir2;
4027
4027
  var noReaddirOptionVersions = /^v[0-5]\./;
4028
- function readdir4(path9, options, cb) {
4028
+ function readdir2(path9, options, cb) {
4029
4029
  if (typeof options === "function")
4030
4030
  cb = options, options = null;
4031
4031
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path10, options2, cb2, startTime) {
@@ -5386,7 +5386,7 @@ var require_minimist = __commonJS({
5386
5386
  var require_rc = __commonJS({
5387
5387
  "node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
5388
5388
  var cc = require_utils();
5389
- var join18 = __require("path").join;
5389
+ var join16 = __require("path").join;
5390
5390
  var deepExtend = require_deep_extend();
5391
5391
  var etc = "/etc";
5392
5392
  var win = process.platform === "win32";
@@ -5411,15 +5411,15 @@ var require_rc = __commonJS({
5411
5411
  }
5412
5412
  if (!win)
5413
5413
  [
5414
- join18(etc, name, "config"),
5415
- join18(etc, name + "rc")
5414
+ join16(etc, name, "config"),
5415
+ join16(etc, name + "rc")
5416
5416
  ].forEach(addConfigFile);
5417
5417
  if (home)
5418
5418
  [
5419
- join18(home, ".config", name, "config"),
5420
- join18(home, ".config", name),
5421
- join18(home, "." + name, "config"),
5422
- join18(home, "." + name + "rc")
5419
+ join16(home, ".config", name, "config"),
5420
+ join16(home, ".config", name),
5421
+ join16(home, "." + name, "config"),
5422
+ join16(home, "." + name + "rc")
5423
5423
  ].forEach(addConfigFile);
5424
5424
  addConfigFile(cc.find("." + name + "rc"));
5425
5425
  if (env5.config) addConfigFile(env5.config);
@@ -5436,7 +5436,7 @@ var require_rc = __commonJS({
5436
5436
  // node_modules/.pnpm/graceful-fs@4.2.10/node_modules/graceful-fs/polyfills.js
5437
5437
  var require_polyfills2 = __commonJS({
5438
5438
  "node_modules/.pnpm/graceful-fs@4.2.10/node_modules/graceful-fs/polyfills.js"(exports, module) {
5439
- var constants10 = __require("constants");
5439
+ var constants9 = __require("constants");
5440
5440
  var origCwd = process.cwd;
5441
5441
  var cwd = null;
5442
5442
  var platform3 = process.env.GRACEFUL_FS_PLATFORM || process.platform;
@@ -5460,7 +5460,7 @@ var require_polyfills2 = __commonJS({
5460
5460
  var chdir;
5461
5461
  module.exports = patch;
5462
5462
  function patch(fs5) {
5463
- if (constants10.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
5463
+ if (constants9.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
5464
5464
  patchLchmod(fs5);
5465
5465
  }
5466
5466
  if (!fs5.lutimes) {
@@ -5500,7 +5500,7 @@ var require_polyfills2 = __commonJS({
5500
5500
  }
5501
5501
  if (platform3 === "win32") {
5502
5502
  fs5.rename = typeof fs5.rename !== "function" ? fs5.rename : (function(fs$rename) {
5503
- function rename4(from, to, cb) {
5503
+ function rename3(from, to, cb) {
5504
5504
  var start = Date.now();
5505
5505
  var backoff = 0;
5506
5506
  fs$rename(from, to, function CB(er) {
@@ -5520,8 +5520,8 @@ var require_polyfills2 = __commonJS({
5520
5520
  if (cb) cb(er);
5521
5521
  });
5522
5522
  }
5523
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename4, fs$rename);
5524
- return rename4;
5523
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename3, fs$rename);
5524
+ return rename3;
5525
5525
  })(fs5.rename);
5526
5526
  }
5527
5527
  fs5.read = typeof fs5.read !== "function" ? fs5.read : (function(fs$read) {
@@ -5562,7 +5562,7 @@ var require_polyfills2 = __commonJS({
5562
5562
  fs6.lchmod = function(path9, mode, callback) {
5563
5563
  fs6.open(
5564
5564
  path9,
5565
- constants10.O_WRONLY | constants10.O_SYMLINK,
5565
+ constants9.O_WRONLY | constants9.O_SYMLINK,
5566
5566
  mode,
5567
5567
  function(err, fd) {
5568
5568
  if (err) {
@@ -5578,7 +5578,7 @@ var require_polyfills2 = __commonJS({
5578
5578
  );
5579
5579
  };
5580
5580
  fs6.lchmodSync = function(path9, mode) {
5581
- var fd = fs6.openSync(path9, constants10.O_WRONLY | constants10.O_SYMLINK, mode);
5581
+ var fd = fs6.openSync(path9, constants9.O_WRONLY | constants9.O_SYMLINK, mode);
5582
5582
  var threw = true;
5583
5583
  var ret;
5584
5584
  try {
@@ -5598,9 +5598,9 @@ var require_polyfills2 = __commonJS({
5598
5598
  };
5599
5599
  }
5600
5600
  function patchLutimes(fs6) {
5601
- if (constants10.hasOwnProperty("O_SYMLINK") && fs6.futimes) {
5601
+ if (constants9.hasOwnProperty("O_SYMLINK") && fs6.futimes) {
5602
5602
  fs6.lutimes = function(path9, at, mt, cb) {
5603
- fs6.open(path9, constants10.O_SYMLINK, function(er, fd) {
5603
+ fs6.open(path9, constants9.O_SYMLINK, function(er, fd) {
5604
5604
  if (er) {
5605
5605
  if (cb) cb(er);
5606
5606
  return;
@@ -5613,7 +5613,7 @@ var require_polyfills2 = __commonJS({
5613
5613
  });
5614
5614
  };
5615
5615
  fs6.lutimesSync = function(path9, at, mt) {
5616
- var fd = fs6.openSync(path9, constants10.O_SYMLINK);
5616
+ var fd = fs6.openSync(path9, constants9.O_SYMLINK);
5617
5617
  var ret;
5618
5618
  var threw = true;
5619
5619
  try {
@@ -5925,8 +5925,8 @@ var require_graceful_fs2 = __commonJS({
5925
5925
  fs6.createReadStream = createReadStream;
5926
5926
  fs6.createWriteStream = createWriteStream;
5927
5927
  var fs$readFile = fs6.readFile;
5928
- fs6.readFile = readFile11;
5929
- function readFile11(path9, options, cb) {
5928
+ fs6.readFile = readFile10;
5929
+ function readFile10(path9, options, cb) {
5930
5930
  if (typeof options === "function")
5931
5931
  cb = options, options = null;
5932
5932
  return go$readFile(path9, options, cb);
@@ -5942,8 +5942,8 @@ var require_graceful_fs2 = __commonJS({
5942
5942
  }
5943
5943
  }
5944
5944
  var fs$writeFile = fs6.writeFile;
5945
- fs6.writeFile = writeFile11;
5946
- function writeFile11(path9, data, options, cb) {
5945
+ fs6.writeFile = writeFile10;
5946
+ function writeFile10(path9, data, options, cb) {
5947
5947
  if (typeof options === "function")
5948
5948
  cb = options, options = null;
5949
5949
  return go$writeFile(path9, data, options, cb);
@@ -5997,9 +5997,9 @@ var require_graceful_fs2 = __commonJS({
5997
5997
  }
5998
5998
  }
5999
5999
  var fs$readdir = fs6.readdir;
6000
- fs6.readdir = readdir4;
6000
+ fs6.readdir = readdir2;
6001
6001
  var noReaddirOptionVersions = /^v[0-5]\./;
6002
- function readdir4(path9, options, cb) {
6002
+ function readdir2(path9, options, cb) {
6003
6003
  if (typeof options === "function")
6004
6004
  cb = options, options = null;
6005
6005
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path10, options2, cb2, startTime) {
@@ -6605,13 +6605,13 @@ var require_envKeyToSetting = __commonJS({
6605
6605
  module.exports = function(x2) {
6606
6606
  const colonIndex = x2.indexOf(":");
6607
6607
  if (colonIndex === -1) {
6608
- return normalize3(x2);
6608
+ return normalize2(x2);
6609
6609
  }
6610
6610
  const firstPart = x2.substr(0, colonIndex);
6611
6611
  const secondPart = x2.substr(colonIndex + 1);
6612
- return `${firstPart}:${normalize3(secondPart)}`;
6612
+ return `${firstPart}:${normalize2(secondPart)}`;
6613
6613
  };
6614
- function normalize3(s) {
6614
+ function normalize2(s) {
6615
6615
  s = s.toLowerCase();
6616
6616
  if (s === "_authtoken") return "_authToken";
6617
6617
  let r2 = s[0];
@@ -6757,11 +6757,11 @@ var require_util = __commonJS({
6757
6757
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
6758
6758
  return name2;
6759
6759
  }
6760
- const dirname14 = path9.dirname(name2);
6761
- if (dirname14 === name2) {
6760
+ const dirname13 = path9.dirname(name2);
6761
+ if (dirname13 === name2) {
6762
6762
  return original;
6763
6763
  }
6764
- return find(dirname14, original);
6764
+ return find(dirname13, original);
6765
6765
  } catch (error) {
6766
6766
  if (name2 === original) {
6767
6767
  if (error.code === "ENOENT") {
@@ -8729,7 +8729,7 @@ var require_semver2 = __commonJS({
8729
8729
  "node_modules/.pnpm/semver@7.7.4/node_modules/semver/index.js"(exports, module) {
8730
8730
  "use strict";
8731
8731
  var internalRe = require_re();
8732
- var constants10 = require_constants();
8732
+ var constants9 = require_constants();
8733
8733
  var SemVer = require_semver();
8734
8734
  var identifiers = require_identifiers();
8735
8735
  var parse = require_parse();
@@ -8811,8 +8811,8 @@ var require_semver2 = __commonJS({
8811
8811
  re: internalRe.re,
8812
8812
  src: internalRe.src,
8813
8813
  tokens: internalRe.t,
8814
- SEMVER_SPEC_VERSION: constants10.SEMVER_SPEC_VERSION,
8815
- RELEASE_TYPES: constants10.RELEASE_TYPES,
8814
+ SEMVER_SPEC_VERSION: constants9.SEMVER_SPEC_VERSION,
8815
+ RELEASE_TYPES: constants9.RELEASE_TYPES,
8816
8816
  compareIdentifiers: identifiers.compareIdentifiers,
8817
8817
  rcompareIdentifiers: identifiers.rcompareIdentifiers
8818
8818
  };
@@ -9522,7 +9522,7 @@ var retryifyAsync = (fn, options) => {
9522
9522
  throw error;
9523
9523
  const delay2 = Math.round(interval * Math.random());
9524
9524
  if (delay2 > 0) {
9525
- const delayPromise = new Promise((resolve13) => setTimeout(resolve13, delay2));
9525
+ const delayPromise = new Promise((resolve12) => setTimeout(resolve12, delay2));
9526
9526
  return delayPromise.then(() => attempt.apply(void 0, args));
9527
9527
  } else {
9528
9528
  return attempt.apply(void 0, args);
@@ -9781,14 +9781,14 @@ var Temp = {
9781
9781
  }
9782
9782
  },
9783
9783
  truncate: (filePath) => {
9784
- const basename13 = path2.basename(filePath);
9785
- if (basename13.length <= LIMIT_BASENAME_LENGTH)
9784
+ const basename12 = path2.basename(filePath);
9785
+ if (basename12.length <= LIMIT_BASENAME_LENGTH)
9786
9786
  return filePath;
9787
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename13);
9787
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename12);
9788
9788
  if (!truncable)
9789
9789
  return filePath;
9790
- const truncationLength = basename13.length - LIMIT_BASENAME_LENGTH;
9791
- return `${filePath.slice(0, -basename13.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
9790
+ const truncationLength = basename12.length - LIMIT_BASENAME_LENGTH;
9791
+ return `${filePath.slice(0, -basename12.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
9792
9792
  }
9793
9793
  };
9794
9794
  node_default(Temp.purgeSyncAll);
@@ -11088,14 +11088,14 @@ var TimeoutError = class extends Error {
11088
11088
 
11089
11089
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
11090
11090
  async function timeout(request, init, abortController, options) {
11091
- return new Promise((resolve13, reject) => {
11091
+ return new Promise((resolve12, reject) => {
11092
11092
  const timeoutId = setTimeout(() => {
11093
11093
  if (abortController) {
11094
11094
  abortController.abort();
11095
11095
  }
11096
11096
  reject(new TimeoutError(request));
11097
11097
  }, options.timeout);
11098
- void options.fetch(request, init).then(resolve13).catch(reject).then(() => {
11098
+ void options.fetch(request, init).then(resolve12).catch(reject).then(() => {
11099
11099
  clearTimeout(timeoutId);
11100
11100
  });
11101
11101
  });
@@ -11103,7 +11103,7 @@ async function timeout(request, init, abortController, options) {
11103
11103
 
11104
11104
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
11105
11105
  async function delay(ms, { signal }) {
11106
- return new Promise((resolve13, reject) => {
11106
+ return new Promise((resolve12, reject) => {
11107
11107
  if (signal) {
11108
11108
  signal.throwIfAborted();
11109
11109
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -11114,7 +11114,7 @@ async function delay(ms, { signal }) {
11114
11114
  }
11115
11115
  const timeoutId = setTimeout(() => {
11116
11116
  signal?.removeEventListener("abort", abortHandler);
11117
- resolve13();
11117
+ resolve12();
11118
11118
  }, ms);
11119
11119
  });
11120
11120
  }
@@ -12817,14 +12817,7 @@ import { basename as basename2 } from "node:path";
12817
12817
  // src/config.ts
12818
12818
  import { mkdir, readFile, writeFile } from "node:fs/promises";
12819
12819
  import { homedir } from "node:os";
12820
- import {
12821
- dirname,
12822
- isAbsolute,
12823
- join,
12824
- normalize,
12825
- resolve,
12826
- win32
12827
- } from "node:path";
12820
+ import { dirname, join, resolve } from "node:path";
12828
12821
  var CONFIG_FILE_NAME = ".gji.json";
12829
12822
  var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
12830
12823
  var GLOBAL_CONFIG_NAME = "config.json";
@@ -12836,8 +12829,6 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
12836
12829
  "hooks",
12837
12830
  "installSaveTarget",
12838
12831
  "shellIntegration",
12839
- "skipInstallPrompt",
12840
- "syncDirs",
12841
12832
  "syncDefaultBranch",
12842
12833
  "syncFiles",
12843
12834
  "syncRemote",
@@ -12847,22 +12838,19 @@ var KNOWN_GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set([
12847
12838
  ...KNOWN_CONFIG_KEYS,
12848
12839
  "repos"
12849
12840
  ]);
12841
+ var REMOVED_CONFIG_KEYS = /* @__PURE__ */ new Set(["syncDirs"]);
12850
12842
  var DEFAULT_CONFIG = Object.freeze({});
12851
12843
  async function loadConfig(root) {
12852
12844
  const path9 = join(root, CONFIG_FILE_NAME);
12853
12845
  return loadConfigFile(path9);
12854
12846
  }
12855
12847
  async function loadEffectiveConfig(root, home = homedir(), onWarning) {
12856
- return (await loadEffectiveConfigResult(root, home, onWarning)).config;
12857
- }
12858
- async function loadEffectiveConfigResult(root, home = homedir(), onWarning) {
12859
12848
  const [globalConfig, localConfig] = await Promise.all([
12860
12849
  loadGlobalConfig(home),
12861
12850
  loadConfig(root)
12862
12851
  ]);
12863
12852
  const repos = globalConfig.config.repos;
12864
12853
  const perRepoConfig = isPlainObject(repos) ? findPerRepoConfig(repos, root, home) : {};
12865
- validateSyncDirsConfig(perRepoConfig.syncDirs);
12866
12854
  const globalBase = { ...globalConfig.config };
12867
12855
  delete globalBase.repos;
12868
12856
  if (onWarning) {
@@ -12905,10 +12893,7 @@ async function loadEffectiveConfigResult(root, home = homedir(), onWarning) {
12905
12893
  if (Object.keys(globalHooks).length > 0 || Object.keys(perRepoHooks).length > 0 || Object.keys(localHooks).length > 0) {
12906
12894
  merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
12907
12895
  }
12908
- return {
12909
- config: toEffectiveConfig(merged),
12910
- dependencyBootstrapExplicit: Object.hasOwn(globalBase, "dependencyBootstrap") || Object.hasOwn(perRepoConfig, "dependencyBootstrap") || Object.hasOwn(localConfig.config, "dependencyBootstrap")
12911
- };
12896
+ return toEffectiveConfig(merged);
12912
12897
  }
12913
12898
  async function loadGlobalConfig(home = homedir()) {
12914
12899
  return loadConfigFile(GLOBAL_CONFIG_FILE_PATH(home));
@@ -12920,15 +12905,6 @@ async function saveLocalConfig(root, config) {
12920
12905
  `, "utf8");
12921
12906
  return path9;
12922
12907
  }
12923
- async function updateLocalConfigKey(root, key, value) {
12924
- const loaded = await loadConfig(root);
12925
- const nextConfig = {
12926
- ...loaded.config,
12927
- [key]: value
12928
- };
12929
- await saveLocalConfig(root, nextConfig);
12930
- return nextConfig;
12931
- }
12932
12908
  async function saveGlobalConfig(config, home = homedir()) {
12933
12909
  validateConfigValues(config);
12934
12910
  const path9 = GLOBAL_CONFIG_FILE_PATH(home);
@@ -12945,6 +12921,7 @@ async function unsetGlobalConfigKey(key, home = homedir()) {
12945
12921
  return nextConfig;
12946
12922
  }
12947
12923
  async function updateGlobalConfigKey(key, value, home = homedir()) {
12924
+ assertConfigKeySupported(key);
12948
12925
  const loaded = await loadGlobalConfig(home);
12949
12926
  const nextConfig = {
12950
12927
  ...loaded.config,
@@ -12953,15 +12930,6 @@ async function updateGlobalConfigKey(key, value, home = homedir()) {
12953
12930
  await saveGlobalConfig(nextConfig, home);
12954
12931
  return nextConfig;
12955
12932
  }
12956
- async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
12957
- const loaded = await loadGlobalConfig(home);
12958
- const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
12959
- const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
12960
- repos[repoRoot] = { ...existing, [key]: value };
12961
- const nextConfig = { ...loaded.config, repos };
12962
- await saveGlobalConfig(nextConfig, home);
12963
- return nextConfig;
12964
- }
12965
12933
  function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
12966
12934
  const configDir = process.env.GJI_CONFIG_DIR;
12967
12935
  if (configDir) {
@@ -12980,32 +12948,19 @@ function resolveConfigString(config, key) {
12980
12948
  const value = config[key];
12981
12949
  return typeof value === "string" && value.length > 0 ? value : void 0;
12982
12950
  }
12983
- function validateSyncDirsConfig(value) {
12984
- normalizeSyncDirsConfig(value);
12951
+ function assertConfigKeySupported(key) {
12952
+ if (REMOVED_CONFIG_KEYS.has(key)) {
12953
+ throw new Error(`configuration key "${key}" is no longer supported`);
12954
+ }
12985
12955
  }
12986
12956
  function normalizeDependencyBootstrap(value) {
12987
12957
  if (value === void 0) return;
12988
- if (value !== "off" && value !== "cow-then-repair" && value !== "install-only") {
12989
- throw new Error(
12990
- 'dependencyBootstrap: expected "off", "cow-then-repair", or "install-only"'
12991
- );
12958
+ if (value !== "off") {
12959
+ throw new Error('dependencyBootstrap: expected "off"');
12992
12960
  }
12993
12961
  return value;
12994
12962
  }
12995
- function normalizeSyncDirsConfig(value) {
12996
- if (value === void 0) return;
12997
- if (!Array.isArray(value)) {
12998
- throw new Error("syncDirs: expected an array of relative directory paths");
12999
- }
13000
- return value.map((pattern) => {
13001
- if (typeof pattern !== "string") {
13002
- throw new Error("syncDirs: every entry must be a string");
13003
- }
13004
- return validateSyncDirPattern(pattern);
13005
- });
13006
- }
13007
12963
  function validateConfigValues(config) {
13008
- validateSyncDirsConfig(config.syncDirs);
13009
12964
  normalizeDependencyBootstrap(config.dependencyBootstrap);
13010
12965
  const repos = config.repos;
13011
12966
  if (!isPlainObject(repos)) return;
@@ -13015,29 +12970,6 @@ function validateConfigValues(config) {
13015
12970
  }
13016
12971
  }
13017
12972
  }
13018
- function validateSyncDirPattern(pattern) {
13019
- if (pattern.length === 0 || isAbsolute(pattern) || win32.isAbsolute(pattern)) {
13020
- throw new Error(
13021
- `syncDirs: pattern must be a relative path, got: ${pattern}`
13022
- );
13023
- }
13024
- const segments = pattern.split(/[\\/]+/u);
13025
- if (segments.includes("..")) {
13026
- throw new Error(
13027
- `syncDirs: pattern must not contain '..' segments, got: ${pattern}`
13028
- );
13029
- }
13030
- if (segments.some((segment) => segment.toLowerCase() === ".git")) {
13031
- throw new Error(`syncDirs: pattern must not include .git, got: ${pattern}`);
13032
- }
13033
- const normalized = normalize(pattern);
13034
- if (normalized === ".") {
13035
- throw new Error(
13036
- `syncDirs: pattern must name a directory below the repository root, got: ${pattern}`
13037
- );
13038
- }
13039
- return normalized;
13040
- }
13041
12973
  async function loadConfigFile(path9) {
13042
12974
  try {
13043
12975
  const rawConfig = await readFile(path9, "utf8");
@@ -13086,12 +13018,6 @@ function toEffectiveConfig(config) {
13086
13018
  delete effective[key];
13087
13019
  }
13088
13020
  }
13089
- if (effective.skipInstallPrompt !== void 0 && typeof effective.skipInstallPrompt !== "boolean") {
13090
- delete effective.skipInstallPrompt;
13091
- }
13092
- if (config.syncDirs !== void 0) {
13093
- effective.syncDirs = normalizeSyncDirsConfig(config.syncDirs);
13094
- }
13095
13021
  if (Array.isArray(config.syncFiles)) {
13096
13022
  effective.syncFiles = config.syncFiles.filter(
13097
13023
  (value) => typeof value === "string"
@@ -13219,7 +13145,7 @@ async function runArgvHook(hookCmd, cwd, context, stderr, stdout3) {
13219
13145
  stderr("gji: hook argv command must include a non-empty command\n");
13220
13146
  return;
13221
13147
  }
13222
- await new Promise((resolve13) => {
13148
+ await new Promise((resolve12) => {
13223
13149
  const child = spawn2(command, args, {
13224
13150
  cwd,
13225
13151
  shell: false,
@@ -13239,18 +13165,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr, stdout3) {
13239
13165
  `
13240
13166
  );
13241
13167
  }
13242
- resolve13();
13168
+ resolve12();
13243
13169
  });
13244
13170
  child.on("error", (err) => {
13245
13171
  stderr(`gji: hook failed to start: ${err.message}
13246
13172
  `);
13247
- resolve13();
13173
+ resolve12();
13248
13174
  });
13249
13175
  });
13250
13176
  }
13251
13177
  async function runShellHook(hookCmd, cwd, context, stderr, stdout3) {
13252
13178
  const interpolated = interpolate(hookCmd, context);
13253
- await new Promise((resolve13) => {
13179
+ await new Promise((resolve12) => {
13254
13180
  const child = spawn2(interpolated, {
13255
13181
  cwd,
13256
13182
  shell: true,
@@ -13268,12 +13194,12 @@ async function runShellHook(hookCmd, cwd, context, stderr, stdout3) {
13268
13194
  stderr(`gji: hook exited with code ${code}: ${interpolated}
13269
13195
  `);
13270
13196
  }
13271
- resolve13();
13197
+ resolve12();
13272
13198
  });
13273
13199
  child.on("error", (err) => {
13274
13200
  stderr(`gji: hook failed to start: ${err.message}
13275
13201
  `);
13276
- resolve13();
13202
+ resolve12();
13277
13203
  });
13278
13204
  });
13279
13205
  }
@@ -13330,7 +13256,7 @@ function createNavigationRepository(name, root) {
13330
13256
  // src/repo.ts
13331
13257
  import { access } from "node:fs/promises";
13332
13258
  import { homedir as homedir3 } from "node:os";
13333
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "node:path";
13259
+ import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13334
13260
 
13335
13261
  // src/git.ts
13336
13262
  import { execFile } from "node:child_process";
@@ -13480,7 +13406,7 @@ function hasExitCode(error, code) {
13480
13406
  async function detectRepository(cwd) {
13481
13407
  const currentRoot = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
13482
13408
  const rawCommonDir = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
13483
- const gitCommonDir = isAbsolute2(rawCommonDir) ? rawCommonDir : resolve3(currentRoot, rawCommonDir);
13409
+ const gitCommonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve3(currentRoot, rawCommonDir);
13484
13410
  const repoRoot = dirname3(gitCommonDir);
13485
13411
  return {
13486
13412
  currentRoot,
@@ -13492,7 +13418,7 @@ async function detectRepository(cwd) {
13492
13418
  }
13493
13419
  async function worktreeGitDir(worktreePath) {
13494
13420
  const rawGitDir = await runGit(worktreePath, ["rev-parse", "--git-dir"]);
13495
- return isAbsolute2(rawGitDir) ? rawGitDir : resolve3(worktreePath, rawGitDir);
13421
+ return isAbsolute(rawGitDir) ? rawGitDir : resolve3(worktreePath, rawGitDir);
13496
13422
  }
13497
13423
  function resolveWorktreePath(repoRoot, branch, basePath) {
13498
13424
  const segments = branch.split("/").filter(Boolean);
@@ -14743,7 +14669,7 @@ async function acquireUndoLogLock(path9) {
14743
14669
  const remainingMs = deadline - Date.now();
14744
14670
  if (remainingMs > 0)
14745
14671
  await new Promise(
14746
- (resolve13) => setTimeout(resolve13, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs))
14672
+ (resolve12) => setTimeout(resolve12, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs))
14747
14673
  );
14748
14674
  }
14749
14675
  }
@@ -16935,7 +16861,7 @@ function normalizeShell(value) {
16935
16861
  var TOP_LEVEL_COMMANDS = [
16936
16862
  {
16937
16863
  name: "new",
16938
- description: "create a new branch or detached linked worktree and CoW-bootstrap configured directories"
16864
+ description: "create a new branch or detached linked worktree"
16939
16865
  },
16940
16866
  {
16941
16867
  name: "done",
@@ -17087,7 +17013,7 @@ _gji_completion() {
17087
17013
 
17088
17014
  case "$command_name" in
17089
17015
  new)
17090
- COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --json --help" -- "$cur") )
17016
+ COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --no-install --json --help" -- "$cur") )
17091
17017
  ;;
17092
17018
  done)
17093
17019
  COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
@@ -17125,7 +17051,7 @@ _gji_completion() {
17125
17051
  COMPREPLY=( $(compgen -W "$(__gji_pr_targets) --select --help" -- "$cur") )
17126
17052
  fi
17127
17053
  else
17128
- COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
17054
+ COMPREPLY=( $(compgen -W "--dry-run --no-install --json --help" -- "$cur") )
17129
17055
  fi
17130
17056
  ;;
17131
17057
  back)
@@ -17343,6 +17269,7 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and rec
17343
17269
  complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
17344
17270
  complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, \u2026)'
17345
17271
  complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
17272
+ complete -c gji -n '__fish_seen_subcommand_from new' -l no-install -d 'skip automatic dependency setup in the new worktree'
17346
17273
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
17347
17274
 
17348
17275
  complete -c gji -n '__fish_seen_subcommand_from init' -l write -d 'write the integration to the shell config file'
@@ -17358,6 +17285,7 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'shell'
17358
17285
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
17359
17286
 
17360
17287
  complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l dry-run -d 'show what would be created without executing any git commands or writing files'
17288
+ complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l no-install -d 'skip automatic dependency setup in the PR worktree'
17361
17289
  complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l json -d 'emit JSON on success or error instead of human-readable output'
17362
17290
  complete -c gji -n '__fish_seen_subcommand_from pr' -a 'open' -d 'open a pull request in the default browser'
17363
17291
  complete -c gji -n '__gji_should_complete_pr_select' -l select -d 'choose a pull request from any linked worktree'
@@ -17501,7 +17429,7 @@ fi
17501
17429
 
17502
17430
  case "\${words[2]}" in
17503
17431
  new)
17504
- _arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, \u2026)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
17432
+ _arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, \u2026)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--no-install[skip automatic dependency setup in the new worktree]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
17505
17433
  ;;
17506
17434
  done)
17507
17435
  _arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
@@ -17528,7 +17456,7 @@ case "\${words[2]}" in
17528
17456
  _arguments '--select[choose a pull request from any linked worktree]' '4:branch or PR number:->pr_targets'
17529
17457
  fi
17530
17458
  else
17531
- _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref:(open)'
17459
+ _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--no-install[skip automatic dependency setup in the PR worktree]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref:(open)'
17532
17460
  fi
17533
17461
  ;;
17534
17462
  back)
@@ -17706,9 +17634,9 @@ var EDITORS = [
17706
17634
  ];
17707
17635
  async function defaultSpawnEditor(cli, args) {
17708
17636
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
17709
- await new Promise((resolve13, reject) => {
17637
+ await new Promise((resolve12, reject) => {
17710
17638
  child.once("error", reject);
17711
- child.once("spawn", resolve13);
17639
+ child.once("spawn", resolve12);
17712
17640
  });
17713
17641
  child.unref();
17714
17642
  }
@@ -17797,8 +17725,8 @@ async function removeMissingRegistryEntries(paths, home = homedir6()) {
17797
17725
  async function withRegistryLock(registryPath, operation) {
17798
17726
  const previous = registryLocks.get(registryPath);
17799
17727
  let release;
17800
- const current = new Promise((resolve13) => {
17801
- release = resolve13;
17728
+ const current = new Promise((resolve12) => {
17729
+ release = resolve12;
17802
17730
  });
17803
17731
  registryLocks.set(registryPath, current);
17804
17732
  if (previous) await previous;
@@ -18685,7 +18613,7 @@ function toMessage3(error) {
18685
18613
  }
18686
18614
 
18687
18615
  // src/go.ts
18688
- import { basename as basename10 } from "node:path";
18616
+ import { basename as basename9 } from "node:path";
18689
18617
 
18690
18618
  // src/context-card.ts
18691
18619
  async function renderContextCard(worktreePath) {
@@ -18712,42 +18640,15 @@ function formatLastCommit2(info) {
18712
18640
  }
18713
18641
 
18714
18642
  // src/pr.ts
18715
- import { execFile as execFile7 } from "node:child_process";
18716
- import { mkdir as mkdir8 } from "node:fs/promises";
18717
- import { dirname as dirname11 } from "node:path";
18718
- import { promisify as promisify8 } from "node:util";
18719
-
18720
- // src/format-bytes.ts
18721
- function formatBytes(bytes) {
18722
- if (bytes === void 0) return "size unavailable";
18723
- if (bytes < 1024) return `${bytes} B`;
18724
- const units = ["KB", "MB", "GB", "TB"];
18725
- let value = bytes;
18726
- let unit = -1;
18727
- while (value >= 1024 && unit < units.length - 1) {
18728
- value /= 1024;
18729
- unit += 1;
18730
- }
18731
- return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
18732
- }
18643
+ import { execFile as execFile6 } from "node:child_process";
18644
+ import { mkdir as mkdir7 } from "node:fs/promises";
18645
+ import { dirname as dirname10 } from "node:path";
18646
+ import { promisify as promisify7 } from "node:util";
18733
18647
 
18734
18648
  // src/bootstrap-output.ts
18735
- function createBootstrapReporter(write, json, measureCloneSize = false) {
18649
+ function createBootstrapReporter(write, json) {
18736
18650
  return {
18737
- measureCloneSize: measureCloneSize && !json,
18738
18651
  write,
18739
- cloned: (directory) => {
18740
- if (json) return;
18741
- write(
18742
- `\u26A1 cloned ${directory.dir} (${formatBytes(directory.bytes)} \u2192 ${formatDuration(directory.ms)})
18743
- `
18744
- );
18745
- },
18746
- skipped: (directory) => {
18747
- if (json) return;
18748
- write(`gji: skipped ${directory.dir} \u2014 ${directory.reason}
18749
- `);
18750
- },
18751
18652
  dependency: (event) => {
18752
18653
  if (json) return;
18753
18654
  const target = event.target ? ` ${event.target}` : "";
@@ -18756,29 +18657,15 @@ function createBootstrapReporter(write, json, measureCloneSize = false) {
18756
18657
  }
18757
18658
  };
18758
18659
  }
18759
- function formatDuration(ms) {
18760
- return `${(ms / 1e3).toFixed(1)}s`;
18761
- }
18762
18660
 
18763
18661
  // src/dependency-bootstrap.ts
18764
- import { execFile as execFile6 } from "node:child_process";
18765
- import { constants as constants6 } from "node:fs";
18766
- import { lstat as lstat4, open as open3, readdir as readdir2, readFile as readFile9, realpath as realpath3, rm as rm4 } from "node:fs/promises";
18767
- import {
18768
- basename as basename7,
18769
- dirname as dirname9,
18770
- isAbsolute as isAbsolute4,
18771
- join as join12,
18772
- relative as relative3,
18773
- resolve as resolve9,
18774
- sep as sep3
18775
- } from "node:path";
18776
- import { promisify as promisify7 } from "node:util";
18662
+ import { readFile as readFile8, realpath as realpath3 } from "node:fs/promises";
18663
+ import { join as join12, relative as relative2, resolve as resolve9, sep as sep3 } from "node:path";
18777
18664
 
18778
18665
  // src/command-runner.ts
18779
18666
  import { spawn as spawn4 } from "node:child_process";
18780
18667
  var runCommand = async (command, cwd, stderr, stdout3 = (chunk) => process.stdout.write(chunk), options) => {
18781
- await new Promise((resolve13, reject) => {
18668
+ await new Promise((resolve12, reject) => {
18782
18669
  const shell = options?.shell ?? true;
18783
18670
  const [executable, args] = shell ? [command, []] : splitCommand(command);
18784
18671
  const child = spawn4(executable, args, {
@@ -18797,7 +18684,7 @@ var runCommand = async (command, cwd, stderr, stdout3 = (chunk) => process.stdou
18797
18684
  if (code !== 0) {
18798
18685
  reject(new Error(`exited with code ${code}`));
18799
18686
  } else {
18800
- resolve13();
18687
+ resolve12();
18801
18688
  }
18802
18689
  });
18803
18690
  child.on("error", reject);
@@ -18813,32 +18700,6 @@ function splitCommand(command) {
18813
18700
  ];
18814
18701
  }
18815
18702
 
18816
- // src/dir-clone.ts
18817
- import { execFile as execFile5 } from "node:child_process";
18818
- import { randomUUID as randomUUID2 } from "node:crypto";
18819
- import { constants as constants5 } from "node:fs";
18820
- import {
18821
- chmod,
18822
- cp,
18823
- lstat as lstat3,
18824
- mkdir as mkdir7,
18825
- mkdtemp,
18826
- opendir,
18827
- readdir,
18828
- readFile as readFile8,
18829
- readlink,
18830
- realpath as realpath2,
18831
- rename as rename3,
18832
- rm as rm3,
18833
- rmdir,
18834
- symlink,
18835
- unlink as unlink3,
18836
- utimes,
18837
- writeFile as writeFile8
18838
- } from "node:fs/promises";
18839
- import { basename as basename6, dirname as dirname8, join as join11, relative as relative2, resolve as resolve8, sep as sep2 } from "node:path";
18840
- import { promisify as promisify6 } from "node:util";
18841
-
18842
18703
  // src/fs-utils.ts
18843
18704
  import { lstat } from "node:fs/promises";
18844
18705
  async function pathExists2(path9) {
@@ -18866,7 +18727,7 @@ function hasErrorCode(error, code) {
18866
18727
  // src/safe-destination.ts
18867
18728
  import { constants as constants4 } from "node:fs";
18868
18729
  import { lstat as lstat2, mkdir as mkdir6, open as open2 } from "node:fs/promises";
18869
- import { isAbsolute as isAbsolute3, join as join10, relative, resolve as resolve7, sep } from "node:path";
18730
+ import { isAbsolute as isAbsolute2, join as join10, relative, resolve as resolve7, sep } from "node:path";
18870
18731
  async function openDestinationDirectory(root, path9) {
18871
18732
  const segments = destinationSegments(root, path9);
18872
18733
  if (process.platform === "darwin") {
@@ -18909,7 +18770,7 @@ async function inspectDestination(root, path9) {
18909
18770
  const resolvedPath = resolve7(path9);
18910
18771
  const resolvedRoot = resolve7(root);
18911
18772
  const distance = relative(resolvedRoot, resolvedPath);
18912
- if (isAbsolute3(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18773
+ if (isAbsolute2(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18913
18774
  return { kind: "unsafe", reason: "destination escapes the worktree" };
18914
18775
  }
18915
18776
  let current = resolvedRoot;
@@ -18968,7 +18829,7 @@ function destinationSegments(root, path9) {
18968
18829
  const resolvedRoot = resolve7(root);
18969
18830
  const resolvedPath = resolve7(path9);
18970
18831
  const distance = relative(resolvedRoot, resolvedPath);
18971
- if (isAbsolute3(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18832
+ if (isAbsolute2(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18972
18833
  throw new Error("destination escapes the worktree");
18973
18834
  }
18974
18835
  return distance.split(sep).filter(Boolean);
@@ -18987,7 +18848,7 @@ async function ensureDestinationDirectory(root, path9) {
18987
18848
  const resolvedRoot = resolve7(root);
18988
18849
  const resolvedPath = resolve7(path9);
18989
18850
  const distance = relative(resolvedRoot, resolvedPath);
18990
- if (isAbsolute3(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18851
+ if (isAbsolute2(distance) || distance === ".." || distance.startsWith(`..${sep}`)) {
18991
18852
  throw new Error("destination escapes the worktree");
18992
18853
  }
18993
18854
  let current = resolvedRoot;
@@ -19017,625 +18878,232 @@ async function ensureDestinationDirectory(root, path9) {
19017
18878
  }
19018
18879
  }
19019
18880
 
19020
- // src/dir-clone.ts
18881
+ // src/uv-validation.ts
18882
+ import { execFile as execFile5 } from "node:child_process";
18883
+ import { constants as constants5 } from "node:fs";
18884
+ import { lstat as lstat3, open as open3, readdir, realpath as realpath2 } from "node:fs/promises";
18885
+ import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute3, join as join11, resolve as resolve8, sep as sep2 } from "node:path";
18886
+ import { promisify as promisify6 } from "node:util";
19021
18887
  var execFileAsync5 = promisify6(execFile5);
19022
- var CLONE_LOCK_TTL_MS = 5 * 60 * 1e3;
19023
- var CLONE_GUARD_TTL_MS = 30 * 1e3;
19024
- var CLONE_LOCK_SUFFIX = ".gji-clone-lock";
19025
- var SIZE_ESTIMATE_MAX_ENTRIES = 1e6;
19026
- var SIZE_ESTIMATE_MAX_MS = 5e3;
19027
- async function cloneDir(source, destination, options = {}) {
19028
- const platform3 = options.platform ?? process.platform;
19029
- if (!isClonePlatformSupported(platform3)) {
19030
- throw new CloneUnsupportedError(`platform ${platform3} has no CoW strategy`);
19031
- }
19032
- const sourcePath = await realpath2(source);
19033
- const sourceStats = await lstat3(sourcePath);
19034
- if (!sourceStats.isDirectory()) {
19035
- throw new Error("source is not a directory");
19036
- }
19037
- const destinationPath = await resolveProspectivePath(destination);
19038
- const destinationDistance = relative2(sourcePath, destinationPath);
19039
- if (destinationDistance === "" || destinationDistance !== ".." && !destinationDistance.startsWith(`..${sep2}`)) {
19040
- throw new Error("clone destination must not be inside its source");
19041
- }
19042
- const startedAt = Date.now();
19043
- const parent = dirname8(destination);
19044
- let safeParent;
19045
- try {
19046
- if (options.destinationRoot) {
19047
- const parentInspection = await inspectDestination(
19048
- options.destinationRoot,
19049
- parent
19050
- );
19051
- if (parentInspection.kind === "unsafe") {
19052
- throw new Error(parentInspection.reason);
19053
- }
19054
- safeParent = await openDestinationDirectory(
19055
- options.destinationRoot,
19056
- parent
19057
- );
19058
- } else {
19059
- await mkdir7(parent, { recursive: true });
19060
- }
19061
- const operationParent = safeParent?.path ?? parent;
19062
- const operationDestination = join11(operationParent, basename6(destination));
19063
- const cloneLock = await acquireCloneLock(
19064
- `${operationDestination}${CLONE_LOCK_SUFFIX}`,
19065
- destination
18888
+ var UV_VALIDATION_MAX_ENTRIES = 1e4;
18889
+ var UV_VALIDATION_MAX_FILE_BYTES = 1024 * 1024;
18890
+ var UV_VALIDATION_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
18891
+ function virtualEnvironmentPrefixes(text) {
18892
+ const prefixes = [];
18893
+ for (const line of text.split(/\r?\n/u)) {
18894
+ if (!/\bVIRTUAL_ENV\b/u.test(line)) continue;
18895
+ const quotedAssignment = line.match(
18896
+ /\bVIRTUAL_ENV(?:\s*=|\s+)\s*(?:"([^"]+)"|'([^']+)')/u
19066
18897
  );
19067
- const stopLockHeartbeat = startLockHeartbeat(cloneLock);
19068
- let temporaryRoot;
19069
- let reservation;
19070
- const publishedEntries = [];
19071
- try {
19072
- if (await destinationExists(operationDestination)) {
19073
- throw new CloneDestinationExistsError(destination);
19074
- }
19075
- reservation = await reserveDestination(operationDestination);
19076
- temporaryRoot = await mkdtemp(
19077
- join11(operationParent, `.${basename6(destination)}.gji-clone-`)
19078
- );
19079
- const temporaryDestination = join11(temporaryRoot, basename6(destination));
19080
- try {
19081
- await (options.copyDirectory ?? ((sourcePath2, targetPath) => copyDirectoryWithCow(
19082
- sourcePath2,
19083
- targetPath,
19084
- platform3,
19085
- options.runLinuxCommand
19086
- )))(sourcePath, temporaryDestination);
19087
- } catch (error) {
19088
- if (error instanceof CloneUnsupportedError) throw error;
19089
- if (isUnsupportedCloneError(error)) {
19090
- throw new CloneUnsupportedError(toErrorMessage(error));
19091
- }
19092
- throw error;
19093
- }
19094
- await publishCloneContents(
19095
- temporaryDestination,
19096
- operationDestination,
19097
- reservation.path,
19098
- (entry) => publishedEntries.push(entry),
19099
- options.copyFile ?? runForcedCloneFileCopy
19100
- );
19101
- reservation = void 0;
19102
- } finally {
19103
- stopLockHeartbeat();
19104
- if (reservation) {
19105
- await cleanupReservedDestination(
19106
- operationDestination,
19107
- reservation,
19108
- publishedEntries
19109
- );
19110
- }
19111
- if (temporaryRoot) {
19112
- await rm3(temporaryRoot, { force: true, recursive: true }).catch(
19113
- () => void 0
19114
- );
19115
- }
19116
- await releaseCloneLock(cloneLock).catch(() => void 0);
19117
- }
19118
- return cloneResult(sourcePath, startedAt, options.measureBytes);
19119
- } finally {
19120
- await safeParent?.close().catch(() => void 0);
18898
+ const quotedPrefix = quotedAssignment?.[1] ?? quotedAssignment?.[2];
18899
+ if (quotedPrefix && isAbsolute3(quotedPrefix)) prefixes.push(quotedPrefix);
18900
+ const unquoted = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*([^\s"']+)/u)?.[1];
18901
+ if (unquoted && isAbsolute3(unquoted)) prefixes.push(unquoted);
19121
18902
  }
18903
+ return uniquePaths(prefixes);
19122
18904
  }
19123
- async function copyDirectoryWithCow(source, destination, platform3, runLinuxCommand) {
19124
- if (platform3 === "darwin") {
19125
- await cp(source, destination, {
19126
- errorOnExist: true,
19127
- force: false,
19128
- mode: constants5.COPYFILE_FICLONE_FORCE,
19129
- preserveTimestamps: true,
19130
- recursive: true
19131
- });
19132
- return;
18905
+ function pythonPrefixFromShebang(text) {
18906
+ const command = text.match(/^#!\s*([^\r\n]+)/u)?.[1]?.trim().split(/\s+/u)[0];
18907
+ if (!command || !isAbsolute3(command)) return void 0;
18908
+ if (!/^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(
18909
+ command.split(sep2).at(-1) ?? ""
18910
+ )) {
18911
+ return void 0;
19133
18912
  }
19134
- const strategy = cloneStrategy(platform3);
19135
- if (!strategy) {
19136
- throw new CloneUnsupportedError(`platform ${platform3} has no CoW strategy`);
18913
+ return dirname8(dirname8(command));
18914
+ }
18915
+ function pythonPrefixesFromText(text) {
18916
+ const interpreters = [];
18917
+ for (const match of text.matchAll(/["'](\/[^"']+)["']/gu)) {
18918
+ if (match[1]) interpreters.push(match[1]);
18919
+ }
18920
+ for (const match of text.matchAll(/(?:^|\s)(\/[^\s"']+)/gmu)) {
18921
+ if (match[1]) interpreters.push(match[1]);
19137
18922
  }
19138
- await (runLinuxCommand ?? runCloneCommand)(
19139
- "cp",
19140
- strategy(source, destination)
18923
+ return uniquePaths(
18924
+ interpreters.flatMap((interpreter) => {
18925
+ const name = interpreter.split(sep2).at(-1) ?? "";
18926
+ return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name) ? [dirname8(dirname8(interpreter))] : [];
18927
+ })
19141
18928
  );
19142
18929
  }
19143
- async function resolveProspectivePath(path9) {
19144
- let current = resolve8(path9);
19145
- const missing = [];
19146
- while (true) {
19147
- try {
19148
- return join11(await realpath2(current), ...missing);
19149
- } catch (error) {
19150
- if (!isNotFoundError(error)) throw error;
19151
- const parent = dirname8(current);
19152
- if (parent === current) throw error;
19153
- missing.unshift(basename6(current));
19154
- current = parent;
18930
+ async function validateUvRelocation(target, context) {
18931
+ if (context.input === "preserve") return;
18932
+ if (!target.targetPath || !target.sourcePath) {
18933
+ throw new Error("uv environment target is not project-local");
18934
+ }
18935
+ const scriptsDirectory = join11(
18936
+ target.targetPath,
18937
+ process.platform === "win32" ? "Scripts" : "bin"
18938
+ );
18939
+ const interpreter = join11(
18940
+ scriptsDirectory,
18941
+ process.platform === "win32" ? "python.exe" : "python"
18942
+ );
18943
+ const { stdout: stdout3 } = await execFileAsync5(interpreter, [
18944
+ "-c",
18945
+ "import os, sys; print(os.path.realpath(sys.prefix))"
18946
+ ]);
18947
+ const [actualPrefix, expectedPrefix] = await Promise.all([
18948
+ realpath2(stdout3.trim()),
18949
+ realpath2(target.targetPath)
18950
+ ]);
18951
+ if (actualPrefix !== expectedPrefix) {
18952
+ throw new Error(
18953
+ `uv environment still points to its source prefix: ${actualPrefix}`
18954
+ );
18955
+ }
18956
+ const sourcePaths = uniquePaths([
18957
+ target.sourcePath,
18958
+ await realpath2(target.sourcePath).catch(() => void 0) ?? ""
18959
+ ]).filter(Boolean);
18960
+ const entries = await readdir(scriptsDirectory, { withFileTypes: true });
18961
+ assertSafeUvScriptEntries(entries);
18962
+ const environmentName = basename6(target.targetPath);
18963
+ const acceptedPrefixes = /* @__PURE__ */ new Set([
18964
+ resolve8(target.targetPath),
18965
+ await realpath2(target.targetPath)
18966
+ ]);
18967
+ let validatedBytes = 0;
18968
+ for (const path9 of [
18969
+ join11(target.targetPath, "pyvenv.cfg"),
18970
+ ...entries.filter((entry) => entry.isFile()).map((entry) => join11(scriptsDirectory, entry.name))
18971
+ ]) {
18972
+ const validated = await readBoundedUvTextFile(
18973
+ path9,
18974
+ UV_VALIDATION_MAX_TOTAL_BYTES - validatedBytes
18975
+ );
18976
+ if (!validated) continue;
18977
+ validatedBytes += validated.bytes;
18978
+ if (validated.text === void 0) continue;
18979
+ const { text } = validated;
18980
+ if (sourcePaths.some((sourcePath) => text.includes(sourcePath))) {
18981
+ throw new Error(`uv environment contains a stale source path: ${path9}`);
18982
+ }
18983
+ const shebangPrefix = pythonPrefixFromShebang(text);
18984
+ if (shebangPrefix && basename6(shebangPrefix) === environmentName && !acceptedPrefixes.has(resolve8(shebangPrefix))) {
18985
+ throw new Error(`uv launcher points outside its environment: ${path9}`);
18986
+ }
18987
+ if (pythonPrefixesFromText(text).filter((prefix) => basename6(prefix) === environmentName).some((prefix) => !acceptedPrefixes.has(resolve8(prefix)))) {
18988
+ throw new Error(`uv launcher points outside its environment: ${path9}`);
18989
+ }
18990
+ if (virtualEnvironmentPrefixes(text).some(
18991
+ (prefix) => !acceptedPrefixes.has(resolve8(prefix))
18992
+ )) {
18993
+ throw new Error(
18994
+ `uv activation script points outside its environment: ${path9}`
18995
+ );
19155
18996
  }
19156
18997
  }
19157
18998
  }
19158
- async function cloneResult(source, startedAt, measureBytes) {
19159
- const bytes = measureBytes === false ? void 0 : await estimateCloneBytes(source);
19160
- return { bytes, ms: Date.now() - startedAt };
18999
+ async function validateUvInstallation(target, context) {
19000
+ await validateUvStructure(target, context);
19001
+ await validateUvRelocation(target, context);
19161
19002
  }
19162
- var CloneDestinationExistsError = class extends Error {
19163
- code = "GJI_CLONE_DESTINATION_EXISTS";
19164
- constructor(destination) {
19165
- super(`destination already exists: ${destination}`);
19166
- this.name = "CloneDestinationExistsError";
19167
- }
19168
- };
19169
- var CloneUnsupportedError = class extends Error {
19170
- code = "GJI_CLONE_UNSUPPORTED";
19171
- constructor(reason) {
19172
- super(`copy-on-write cloning is not supported: ${reason}`);
19173
- this.name = "CloneUnsupportedError";
19003
+ async function validateUvStructure(target, context) {
19004
+ if (context.input === "preserve") return;
19005
+ if (!target.targetPath)
19006
+ throw new Error("uv environment target is not project-local");
19007
+ const targetStats = await lstat3(target.targetPath).catch(() => void 0);
19008
+ if (!targetStats) return;
19009
+ if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
19010
+ throw new Error("uv environment must be a real directory");
19174
19011
  }
19175
- };
19176
- var CloneInProgressError = class extends Error {
19177
- code = "GJI_CLONE_IN_PROGRESS";
19178
- constructor(destination) {
19179
- super(`copy-on-write clone already in progress: ${destination}`);
19180
- this.name = "CloneInProgressError";
19012
+ const configStats = await lstat3(join11(target.targetPath, "pyvenv.cfg")).catch(
19013
+ () => void 0
19014
+ );
19015
+ if (configStats && (!configStats.isFile() || configStats.isSymbolicLink())) {
19016
+ throw new Error("uv pyvenv.cfg must be a regular file");
19181
19017
  }
19182
- };
19183
- function isCloneDestinationExistsError(error) {
19184
- return error instanceof CloneDestinationExistsError;
19185
- }
19186
- function isCloneInProgressError(error) {
19187
- return error instanceof CloneInProgressError;
19188
- }
19189
- async function directorySize(path9) {
19190
- const stats = await lstat3(path9);
19191
- if (!stats.isDirectory()) return stats.size;
19192
- const pending = [path9];
19193
- let total = 0;
19194
- let entryCount = 0;
19195
- const startedAt = Date.now();
19196
- while (pending.length > 0) {
19197
- const current = pending.pop();
19198
- if (!current) continue;
19199
- const directory = await opendir(current);
19200
- try {
19201
- for await (const entry of directory) {
19202
- entryCount += 1;
19203
- if (entryCount > SIZE_ESTIMATE_MAX_ENTRIES || Date.now() - startedAt > SIZE_ESTIMATE_MAX_MS) {
19204
- throw new Error("directory size estimate exceeded its safety limit");
19205
- }
19206
- const entryPath = join11(current, entry.name);
19207
- if (entry.isDirectory()) pending.push(entryPath);
19208
- else total += (await lstat3(entryPath)).size;
19209
- }
19210
- } finally {
19211
- await directory.close().catch(() => void 0);
19212
- }
19018
+ const scriptsDirectory = join11(
19019
+ target.targetPath,
19020
+ process.platform === "win32" ? "Scripts" : "bin"
19021
+ );
19022
+ const scriptsStats = await lstat3(scriptsDirectory).catch(() => void 0);
19023
+ if (!scriptsStats) return;
19024
+ if (!scriptsStats.isDirectory() || scriptsStats.isSymbolicLink()) {
19025
+ throw new Error("uv scripts path must be a real directory");
19213
19026
  }
19214
- return total;
19027
+ assertSafeUvScriptEntries(
19028
+ await readdir(scriptsDirectory, { withFileTypes: true })
19029
+ );
19215
19030
  }
19216
- function cloneStrategy(platform3) {
19217
- if (platform3 === "linux") {
19218
- return (source, destination) => [
19219
- "-a",
19220
- "--reflink=always",
19221
- source,
19222
- destination
19223
- ];
19031
+ function assertSafeUvScriptEntries(entries) {
19032
+ if (entries.length > UV_VALIDATION_MAX_ENTRIES) {
19033
+ throw new Error("uv scripts directory exceeds the validation entry limit");
19224
19034
  }
19225
- return null;
19226
- }
19227
- function isClonePlatformSupported(platform3) {
19228
- return platform3 === "darwin" || platform3 === "linux";
19229
- }
19230
- async function estimateCloneBytes(path9) {
19231
- try {
19232
- return await directorySize(path9);
19233
- } catch {
19234
- return void 0;
19035
+ for (const entry of entries) {
19036
+ if (entry.isSymbolicLink() && !isUvInterpreterName(entry.name)) {
19037
+ throw new Error(`uv script must not be a symbolic link: ${entry.name}`);
19038
+ }
19235
19039
  }
19236
19040
  }
19237
- async function runCloneCommand(command, args) {
19041
+ async function readBoundedUvTextFile(path9, remainingBytes) {
19042
+ let handle;
19238
19043
  try {
19239
- await execFileAsync5(command, args);
19044
+ handle = await open3(
19045
+ path9,
19046
+ constants5.O_RDONLY | (process.platform === "win32" ? 0 : constants5.O_NOFOLLOW)
19047
+ );
19240
19048
  } catch (error) {
19241
- if (isUnsupportedCloneError(error)) {
19242
- throw new CloneUnsupportedError(toErrorMessage(error));
19243
- }
19049
+ if (isNotFoundError(error)) return void 0;
19244
19050
  throw error;
19245
19051
  }
19246
- }
19247
- async function acquireCloneLock(lockPath, destination) {
19248
- const releaseGuard = await acquireCloneLockGuard(lockPath, destination);
19249
19052
  try {
19250
- return await acquireCloneLockWithGuard(lockPath, destination);
19251
- } finally {
19252
- await releaseGuard();
19253
- }
19254
- }
19255
- async function acquireCloneLockWithGuard(lockPath, destination) {
19256
- const lockToken = randomUUID2();
19257
- for (let attempt = 0; attempt < 3; attempt += 1) {
19258
- const published = await publishCloneLock(lockPath, lockToken);
19259
- if (published) return published;
19260
- let staleLock;
19261
- try {
19262
- staleLock = await inspectCloneLock(lockPath);
19263
- } catch (error) {
19264
- if (isNotFoundError(error)) continue;
19265
- throw error;
19053
+ const stats = await handle.stat();
19054
+ if (!stats.isFile()) return void 0;
19055
+ if (stats.size > UV_VALIDATION_MAX_FILE_BYTES) {
19056
+ throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
19266
19057
  }
19267
- if (staleLock.kind === "invalid" || Date.now() - staleLock.mtimeMs < CLONE_LOCK_TTL_MS) {
19268
- throw new CloneInProgressError(destination);
19058
+ if (stats.size > remainingBytes) {
19059
+ throw new Error("uv launchers exceed the total validation size limit");
19269
19060
  }
19270
- const stalePath = `${lockPath}.stale-${randomUUID2()}`;
19271
- try {
19272
- await rename3(lockPath, stalePath);
19273
- } catch (error) {
19274
- if (isNotFoundError(error)) continue;
19275
- throw error;
19061
+ const readLimit = Math.min(UV_VALIDATION_MAX_FILE_BYTES, remainingBytes);
19062
+ const contents = await readFilePrefix(handle, readLimit + 1);
19063
+ if (contents.byteLength > UV_VALIDATION_MAX_FILE_BYTES) {
19064
+ throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
19276
19065
  }
19277
- try {
19278
- const replacement = await publishCloneLock(lockPath, lockToken);
19279
- if (!replacement) {
19280
- await cleanupStaleCloneLock(stalePath, staleLock);
19281
- continue;
19282
- }
19283
- try {
19284
- await cleanupStaleCloneLock(stalePath, staleLock);
19285
- } catch (cleanupError) {
19286
- await releaseCloneLockWithGuard(replacement).catch(() => void 0);
19287
- throw cleanupError;
19288
- }
19289
- return replacement;
19290
- } catch (error) {
19291
- await cleanupStaleCloneLock(stalePath, staleLock).catch(() => void 0);
19292
- if (isAlreadyExistsError(error)) continue;
19293
- throw error;
19066
+ if (contents.byteLength > remainingBytes) {
19067
+ throw new Error("uv launchers exceed the total validation size limit");
19294
19068
  }
19069
+ const text = contents.toString("utf8");
19070
+ return Buffer.from(text, "utf8").equals(contents) ? { bytes: contents.byteLength, text } : { bytes: contents.byteLength };
19071
+ } finally {
19072
+ await handle.close();
19295
19073
  }
19296
- throw new CloneInProgressError(destination);
19297
19074
  }
19298
- async function acquireCloneLockGuard(lockPath, destination) {
19299
- const guardPath = `${lockPath}.guard`;
19300
- const deadline = Date.now() + 5e3;
19301
- while (true) {
19302
- try {
19303
- await mkdir7(guardPath, { mode: 448 });
19304
- const markerName = `${process.pid}-${randomUUID2()}`;
19305
- const markerPath = join11(guardPath, markerName);
19306
- try {
19307
- await writeFile8(markerPath, `${markerName}
19308
- `, {
19309
- flag: "wx",
19310
- mode: 384
19311
- });
19312
- } catch (error) {
19313
- await rmdir(guardPath).catch(() => void 0);
19314
- throw error;
19315
- }
19316
- return async () => {
19317
- await unlink3(markerPath).catch((error) => {
19318
- if (!isNotFoundError(error)) throw error;
19319
- });
19320
- await rmdir(guardPath).catch((error) => {
19321
- if (!isNotFoundError(error)) throw error;
19322
- });
19323
- };
19324
- } catch (error) {
19325
- if (!isAlreadyExistsError(error)) throw error;
19326
- if (await reclaimAbandonedCloneGuard(guardPath)) continue;
19327
- if (Date.now() >= deadline) {
19328
- throw new CloneInProgressError(destination);
19329
- }
19330
- await new Promise((resolve13) => setTimeout(resolve13, 10));
19331
- }
19332
- }
19333
- }
19334
- async function reclaimAbandonedCloneGuard(guardPath) {
19335
- let expectedMarker;
19336
- try {
19337
- const entries = await readdir(guardPath);
19338
- if (entries.length > 1) return false;
19339
- expectedMarker = entries[0];
19340
- const freshnessPath = expectedMarker ? join11(guardPath, expectedMarker) : guardPath;
19341
- const stats = await lstat3(freshnessPath);
19342
- if (Date.now() - stats.mtimeMs < CLONE_GUARD_TTL_MS) return false;
19343
- if (expectedMarker) {
19344
- const ownerPid = Number(expectedMarker.split("-", 1)[0]);
19345
- if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0) return false;
19346
- if (processIsAlive(ownerPid)) return false;
19347
- }
19348
- } catch (error) {
19349
- return isNotFoundError(error);
19350
- }
19351
- const stalePath = `${guardPath}.stale-${randomUUID2()}`;
19352
- try {
19353
- await rename3(guardPath, stalePath);
19354
- } catch (error) {
19355
- if (isNotFoundError(error)) return true;
19356
- throw error;
19357
- }
19358
- const movedEntries = await readdir(stalePath).catch(() => []);
19359
- if (movedEntries.length !== (expectedMarker ? 1 : 0) || expectedMarker && movedEntries[0] !== expectedMarker) {
19360
- await rename3(stalePath, guardPath).catch(() => void 0);
19361
- return false;
19362
- }
19363
- if (expectedMarker) await unlink3(join11(stalePath, expectedMarker));
19364
- await rmdir(stalePath);
19365
- return true;
19366
- }
19367
- function processIsAlive(pid) {
19368
- try {
19369
- process.kill(pid, 0);
19370
- return true;
19371
- } catch (error) {
19372
- return error.code === "EPERM";
19373
- }
19374
- }
19375
- async function publishCloneLock(lockPath, lockToken) {
19376
- try {
19377
- await mkdir7(lockPath, { mode: 448 });
19378
- } catch (error) {
19379
- if (isAlreadyExistsError(error)) return void 0;
19380
- throw error;
19381
- }
19382
- const markerPath = join11(lockPath, lockToken);
19383
- try {
19384
- await writeFile8(markerPath, `${lockToken}
19385
- `, { flag: "wx", mode: 384 });
19386
- return {
19387
- lockPath,
19388
- markerName: lockToken,
19389
- markerPath,
19390
- token: lockToken
19391
- };
19392
- } catch (error) {
19393
- await rmdir(lockPath).catch(() => void 0);
19394
- throw error;
19395
- }
19396
- }
19397
- async function inspectCloneLock(lockPath) {
19398
- try {
19399
- const stats = await lstat3(lockPath);
19400
- if (!stats.isDirectory() || stats.isSymbolicLink()) {
19401
- return { kind: "invalid", mtimeMs: stats.mtimeMs };
19402
- }
19403
- const entries = await readdir(lockPath);
19404
- if (entries.length === 0) return { kind: "empty", mtimeMs: stats.mtimeMs };
19405
- if (entries.length !== 1) {
19406
- return { kind: "invalid", mtimeMs: stats.mtimeMs };
19407
- }
19408
- const markerName = entries[0];
19409
- const token = markerName?.startsWith("owner-") ? markerName.slice("owner-".length) : markerName;
19410
- if (!token || !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(token)) {
19411
- return { kind: "invalid", mtimeMs: stats.mtimeMs };
19412
- }
19413
- const markerPath = join11(lockPath, markerName);
19414
- const markerStats = await lstat3(markerPath);
19415
- if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
19416
- return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
19417
- }
19418
- if ((await readFile8(markerPath, "utf8")).trim() !== token) {
19419
- return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
19420
- }
19421
- return {
19422
- kind: "owned",
19423
- lockPath,
19424
- markerName,
19425
- markerPath,
19426
- mtimeMs: markerStats.mtimeMs,
19427
- token
19428
- };
19429
- } catch (error) {
19430
- if (isNotFoundError(error)) throw error;
19431
- return { kind: "invalid", mtimeMs: Date.now() };
19432
- }
19433
- }
19434
- async function cleanupStaleCloneLock(lockPath, inspection) {
19435
- if (inspection.kind === "owned") {
19436
- await unlink3(join11(lockPath, inspection.markerName));
19437
- }
19438
- await rmdir(lockPath);
19439
- }
19440
- async function publishCloneContents(temporaryDestination, destination, reservationPath, onEntryPublished, copyFileEntry) {
19441
- const reservationName = basename6(reservationPath);
19442
- const destinationEntries = await readdir(destination);
19443
- if (destinationEntries.length !== 1 || destinationEntries[0] !== reservationName) {
19444
- throw new CloneDestinationExistsError(destination);
19445
- }
19446
- const temporaryEntries = await readdir(temporaryDestination);
19447
- for (const entry of temporaryEntries) {
19448
- await publishCloneEntry(
19449
- join11(temporaryDestination, entry),
19450
- join11(destination, entry),
19451
- (entry2) => onEntryPublished(entry2),
19452
- copyFileEntry
19075
+ async function readFilePrefix(handle, maxBytes) {
19076
+ const contents = Buffer.allocUnsafe(maxBytes);
19077
+ let offset = 0;
19078
+ while (offset < contents.byteLength) {
19079
+ const { bytesRead } = await handle.read(
19080
+ contents,
19081
+ offset,
19082
+ contents.byteLength - offset,
19083
+ offset
19453
19084
  );
19085
+ if (bytesRead === 0) break;
19086
+ offset += bytesRead;
19454
19087
  }
19455
- await unlink3(reservationPath);
19456
- }
19457
- async function publishCloneEntry(source, destination, onCreated, copyFileEntry) {
19458
- const sourceStats = await lstat3(source);
19459
- if (sourceStats.isDirectory()) {
19460
- try {
19461
- await mkdir7(destination);
19462
- } catch (error) {
19463
- if (isAlreadyExistsError(error)) {
19464
- throw new CloneDestinationExistsError(destination);
19465
- }
19466
- throw error;
19467
- }
19468
- onCreated(await inspectOwnedCloneEntry(destination, "directory"));
19469
- for (const entry of await readdir(source)) {
19470
- await publishCloneEntry(
19471
- join11(source, entry),
19472
- join11(destination, entry),
19473
- () => void 0,
19474
- copyFileEntry
19475
- );
19476
- }
19477
- await copyCloneMetadata(sourceStats, destination);
19478
- return;
19479
- }
19480
- if (sourceStats.isSymbolicLink()) {
19481
- try {
19482
- await symlink(await readlink(source), destination);
19483
- } catch (error) {
19484
- if (isAlreadyExistsError(error)) {
19485
- throw new CloneDestinationExistsError(destination);
19486
- }
19487
- throw error;
19488
- }
19489
- onCreated(await inspectOwnedCloneEntry(destination, "symlink"));
19490
- return;
19491
- }
19492
- if (!sourceStats.isFile()) {
19493
- throw new Error(`unsupported clone entry type: ${source}`);
19494
- }
19495
- try {
19496
- await copyFileEntry(source, destination);
19497
- } catch (error) {
19498
- if (isAlreadyExistsError(error)) {
19499
- throw new CloneDestinationExistsError(destination);
19500
- }
19501
- if (isUnsupportedCloneError(error)) {
19502
- throw new CloneUnsupportedError(toErrorMessage(error));
19503
- }
19504
- throw error;
19505
- }
19506
- onCreated(await inspectOwnedCloneEntry(destination, "file"));
19507
- await copyCloneMetadata(sourceStats, destination);
19508
- }
19509
- async function runForcedCloneFileCopy(source, destination) {
19510
- await cp(source, destination, {
19511
- errorOnExist: true,
19512
- force: false,
19513
- mode: constants5.COPYFILE_FICLONE_FORCE,
19514
- preserveTimestamps: true
19515
- });
19516
- }
19517
- async function copyCloneMetadata(sourceStats, destination) {
19518
- await chmod(destination, Number(sourceStats.mode) & 4095);
19519
- await utimes(destination, sourceStats.atime, sourceStats.mtime);
19520
- }
19521
- async function reserveDestination(destination) {
19522
- try {
19523
- await mkdir7(destination);
19524
- } catch (error) {
19525
- if (isAlreadyExistsError(error)) {
19526
- throw new CloneDestinationExistsError(destination);
19527
- }
19528
- throw error;
19529
- }
19530
- const reservationPath = join11(
19531
- destination,
19532
- `.gji-clone-reservation-${randomUUID2()}`
19533
- );
19534
- try {
19535
- await writeFile8(reservationPath, "gji clone reservation\n", {
19536
- flag: "wx"
19537
- });
19538
- return await inspectOwnedCloneEntry(reservationPath, "file");
19539
- } catch (error) {
19540
- await rmdir(destination).catch(() => void 0);
19541
- throw error;
19542
- }
19543
- }
19544
- async function cleanupReservedDestination(destination, reservation, publishedEntries) {
19545
- for (const entry of [...publishedEntries, reservation].reverse()) {
19546
- await removeOwnedCloneEntry(entry);
19547
- }
19548
- await rmdir(destination).catch(() => void 0);
19549
- }
19550
- async function inspectOwnedCloneEntry(path9, kind) {
19551
- const stats = await lstat3(path9, { bigint: true });
19552
- return { dev: stats.dev, ino: stats.ino, kind, path: path9 };
19553
- }
19554
- async function removeOwnedCloneEntry(entry) {
19555
- try {
19556
- const current = await lstat3(entry.path, { bigint: true });
19557
- if (current.dev !== entry.dev || current.ino !== entry.ino) return;
19558
- if (entry.kind === "directory") await rmdir(entry.path);
19559
- else await unlink3(entry.path);
19560
- } catch {
19561
- }
19562
- }
19563
- async function destinationExists(path9) {
19564
- try {
19565
- await lstat3(path9);
19566
- return true;
19567
- } catch (error) {
19568
- if (isNotFoundError(error)) return false;
19569
- throw error;
19570
- }
19571
- }
19572
- function startLockHeartbeat(lock) {
19573
- const timer = setInterval(() => {
19574
- void refreshCloneLock(lock);
19575
- }, CLONE_LOCK_TTL_MS / 3);
19576
- timer.unref?.();
19577
- return () => clearInterval(timer);
19578
- }
19579
- async function refreshCloneLock(lock) {
19580
- try {
19581
- const now = /* @__PURE__ */ new Date();
19582
- await utimes(lock.markerPath, now, now);
19583
- } catch {
19584
- }
19585
- }
19586
- async function releaseCloneLock(lock) {
19587
- const releaseGuard = await acquireCloneLockGuard(
19588
- lock.lockPath,
19589
- lock.lockPath
19590
- );
19591
- try {
19592
- await releaseCloneLockWithGuard(lock);
19593
- } finally {
19594
- await releaseGuard();
19595
- }
19596
- }
19597
- async function releaseCloneLockWithGuard(lock) {
19598
- try {
19599
- await unlink3(join11(lock.lockPath, lock.token));
19600
- } catch (error) {
19601
- if (!isNotFoundError(error)) throw error;
19602
- }
19603
- try {
19604
- await rmdir(lock.lockPath);
19605
- } catch (error) {
19606
- const code = "code" in error ? error.code : void 0;
19607
- if (!isNotFoundError(error) && code !== "ENOTEMPTY" && code !== "EEXIST") {
19608
- throw error;
19609
- }
19610
- }
19088
+ return contents.subarray(0, offset);
19611
19089
  }
19612
- function isUnsupportedCloneError(error) {
19613
- if (!(error instanceof Error)) return false;
19614
- const code = "code" in error ? error.code : "";
19615
- if (["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EXDEV"].includes(code ?? "")) {
19616
- return true;
19617
- }
19618
- return /reflink|unsupported|operation not supported|not supported|invalid cross-device|cross-device/iu.test(
19619
- error.message
19620
- );
19090
+ function isUvInterpreterName(name) {
19091
+ return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name);
19621
19092
  }
19622
- function toErrorMessage(error) {
19623
- return error instanceof Error ? error.message : String(error);
19093
+ function uniquePaths(paths) {
19094
+ return [...new Set(paths.filter((path9) => Boolean(path9)))];
19624
19095
  }
19625
19096
 
19626
19097
  // src/dependency-bootstrap.ts
19627
- var execFileAsync6 = promisify7(execFile6);
19628
- var UV_VALIDATION_MAX_ENTRIES = 1e4;
19629
- var UV_VALIDATION_MAX_FILE_BYTES = 1024 * 1024;
19630
- var UV_VALIDATION_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
19098
+ function resolveDependencyBootstrapMode(dependencyBootstrap, noInstall = false) {
19099
+ if (noInstall || dependencyBootstrap === "off") return "off";
19100
+ return "install";
19101
+ }
19631
19102
  async function prepareDependencyBootstrap(mode, context) {
19632
19103
  if (mode === "off") return { mode, targets: [] };
19633
- const adapters = createBootstrapAdapters(
19634
- context.checkUvRuntime ?? defaultCheckUvRuntime,
19635
- context.cargoBuildCommand
19636
- );
19104
+ const adapters = createBootstrapAdapters(context.cargoBuildCommand);
19637
19105
  const sourceRoots = [];
19638
- for (const sourceRoot of uniquePaths([
19106
+ for (const sourceRoot of uniquePaths2([
19639
19107
  context.repoRoot,
19640
19108
  context.currentRoot
19641
19109
  ])) {
@@ -19647,8 +19115,6 @@ async function prepareDependencyBootstrap(mode, context) {
19647
19115
  const plannedRelativePaths = /* @__PURE__ */ new Set();
19648
19116
  for (const adapter of adapters) {
19649
19117
  if (plannedRelativePaths.has(adapter.relativePath)) continue;
19650
- let firstDetected;
19651
- let selected;
19652
19118
  for (const sourceRoot of sourceRoots) {
19653
19119
  const target = await adapter.detect({
19654
19120
  detectionRoot: context.detectionRoot,
@@ -19656,82 +19122,37 @@ async function prepareDependencyBootstrap(mode, context) {
19656
19122
  worktreePath: context.worktreePath
19657
19123
  });
19658
19124
  if (!target) continue;
19659
- const seedable = mode !== "install-only" && await adapter.canSeed(target);
19660
- const candidate = { target, seedable };
19661
- firstDetected ??= candidate;
19662
- if (mode === "install-only" || seedable) {
19663
- selected = candidate;
19664
- break;
19665
- }
19666
- }
19667
- const plannedTarget = selected ?? firstDetected;
19668
- if (plannedTarget) {
19669
- targets.push(plannedTarget);
19125
+ targets.push({ adapter, target });
19670
19126
  plannedRelativePaths.add(adapter.relativePath);
19127
+ break;
19671
19128
  }
19672
19129
  }
19673
19130
  return { mode, targets };
19674
19131
  }
19675
- async function detectDependencyBootstrapCandidate(context) {
19676
- const plan = await prepareDependencyBootstrap("cow-then-repair", context);
19677
- const planned = plan.targets[0];
19678
- if (!planned) return null;
19679
- return {
19680
- adapter: planned.target.adapter,
19681
- kind: planned.target.kind,
19682
- lockfile: adapterForTarget(planned.target).lockfile,
19683
- target: planned.target.relativePath,
19684
- repairCommand: planned.target.repairCommand,
19685
- seedable: planned.seedable
19686
- };
19687
- }
19688
19132
  function previewDependencyBootstrap(plan) {
19689
19133
  return {
19690
19134
  mode: plan.mode,
19691
- targets: plan.targets.map(({ target, seedable }) => ({
19692
- adapter: target.adapter,
19693
- kind: target.kind,
19135
+ targets: plan.targets.map(({ adapter, target }) => ({
19136
+ adapter: adapter.name,
19137
+ kind: adapter.kind,
19694
19138
  target: target.relativePath,
19695
- repairCommand: target.repairCommand,
19696
- seedable,
19697
- strategy: bootstrapStrategy(plan.mode, target, seedable)
19139
+ command: target.installCommand
19698
19140
  }))
19699
19141
  };
19700
19142
  }
19701
19143
  async function executeDependencyBootstrap(plan, options) {
19702
19144
  if (plan.mode === "off") return { mode: plan.mode, ready: true, events: [] };
19703
19145
  const events = [];
19704
- const cloneDirectory = options.cloneDirectory ?? cloneDir;
19705
19146
  const execution = {
19706
19147
  runCommand: options.runCommand ?? runCommand,
19707
19148
  stderr: options.stderr ?? (() => void 0),
19708
19149
  stdout: options.stdout ?? ((chunk) => process.stdout.write(chunk))
19709
19150
  };
19710
- const seededDirectories = new Set(options.seededDirectories ?? []);
19711
19151
  if (plan.targets.length === 0) {
19712
- recordBootstrapEvent(events, options.reporter, {
19713
- adapter: "none",
19714
- kind: "dependency",
19715
- reason: "no-lockfile",
19716
- state: "skipped",
19717
- target: "",
19718
- message: "no supported dependency or build-state lockfile was detected"
19719
- });
19720
19152
  return { mode: plan.mode, ready: true, events };
19721
19153
  }
19722
- for (const { target, seedable } of plan.targets) {
19723
- const adapter = adapterForTarget(target);
19724
- await executeBootstrapTarget(
19725
- adapter,
19726
- target,
19727
- seedable,
19728
- plan.mode,
19729
- cloneDirectory,
19730
- execution,
19731
- seededDirectories.has(target.relativePath),
19732
- events,
19733
- options.reporter
19734
- );
19154
+ for (const { adapter, target } of plan.targets) {
19155
+ await installTarget(adapter, target, execution, events, options.reporter);
19735
19156
  }
19736
19157
  return {
19737
19158
  mode: plan.mode,
@@ -19739,263 +19160,43 @@ async function executeDependencyBootstrap(plan, options) {
19739
19160
  events
19740
19161
  };
19741
19162
  }
19742
- async function executeBootstrapTarget(adapter, target, seedable, mode, cloneDirectory, execution, seededBySyncDirs, events, reporter) {
19743
- const destinationInspection = await inspectDestination(
19744
- target.worktreePath,
19745
- target.targetPath
19746
- );
19747
- if (destinationInspection.kind === "unsafe") {
19163
+ async function installTarget(adapter, target, execution, events, reporter) {
19164
+ const beforeInstallInspection = target.targetPath ? await inspectDestination(target.worktreePath, target.targetPath) : void 0;
19165
+ if (beforeInstallInspection?.kind === "unsafe") {
19748
19166
  recordBootstrapFailure(
19749
19167
  events,
19750
19168
  reporter,
19751
19169
  adapter,
19752
19170
  target,
19753
- destinationInspection.reason,
19171
+ beforeInstallInspection.reason,
19754
19172
  "destination-unsafe"
19755
19173
  );
19756
19174
  return;
19757
19175
  }
19758
- if (mode === "install-only") {
19759
- await repairTarget(
19760
- adapter,
19761
- target,
19762
- execution,
19763
- destinationInspection.kind === "exists" ? "preserve" : "clean",
19764
- events,
19765
- reporter
19766
- );
19767
- return;
19768
- }
19769
- let input = destinationInspection.kind === "exists" ? "preserve" : "clean";
19770
- const destinationExistsNow = destinationInspection.kind === "exists";
19771
- if (target.initialInput === "preserve" && destinationExistsNow) {
19772
- recordBootstrapEvent(events, reporter, {
19773
- adapter: adapter.name,
19774
- kind: adapter.kind,
19775
- reason: "target-exists",
19776
- state: "skipped",
19777
- target: target.relativePath,
19778
- message: "target already existed; using it as the repair input"
19779
- });
19780
- } else if (seededBySyncDirs && destinationExistsNow) {
19781
- if (seedable) {
19782
- input = "seed";
19783
- recordBootstrapEvent(events, reporter, {
19784
- adapter: adapter.name,
19785
- kind: adapter.kind,
19786
- reason: "generic-seed",
19787
- state: "seeded",
19788
- target: target.relativePath,
19789
- message: "reusing a seed created by syncDirs"
19790
- });
19791
- } else {
19792
- input = "preserve";
19793
- recordBootstrapEvent(events, reporter, {
19794
- adapter: adapter.name,
19795
- kind: adapter.kind,
19796
- reason: "generic-seed",
19797
- state: "repair-only",
19798
- target: target.relativePath,
19799
- message: "syncDirs created a generic target; this adapter uses repair without CoW"
19800
- });
19801
- }
19802
- } else if (destinationExistsNow) {
19803
- input = "preserve";
19804
- recordBootstrapEvent(events, reporter, {
19805
- adapter: adapter.name,
19806
- kind: adapter.kind,
19807
- reason: "destination-race",
19808
- state: "skipped",
19809
- target: target.relativePath,
19810
- message: "target appeared during bootstrap; preserving it as the repair input"
19811
- });
19812
- } else if (seedable) {
19813
- try {
19814
- await cloneDirectory(adapter.seedPath(target), target.targetPath, {
19815
- destinationRoot: target.worktreePath,
19816
- measureBytes: reporter.measureCloneSize
19817
- });
19818
- input = "seed";
19819
- recordBootstrapEvent(events, reporter, {
19820
- adapter: adapter.name,
19821
- kind: adapter.kind,
19822
- state: "seeded",
19823
- target: target.relativePath,
19824
- message: "seeded with copy-on-write"
19825
- });
19826
- } catch (error) {
19827
- if (isCloneDestinationExistsError(error)) {
19828
- input = "preserve";
19829
- recordBootstrapEvent(events, reporter, {
19830
- adapter: adapter.name,
19831
- kind: adapter.kind,
19832
- state: "skipped",
19833
- target: target.relativePath,
19834
- message: "target appeared during CoW seeding; preserving it as the repair input"
19835
- });
19836
- await repairTarget(adapter, target, execution, input, events, reporter);
19837
- return;
19838
- }
19839
- if (isCloneInProgressError(error)) {
19840
- recordBootstrapFailure(
19841
- events,
19842
- reporter,
19843
- adapter,
19844
- target,
19845
- `CoW seed is already in progress: ${toErrorMessage2(error)}`,
19846
- "clone-in-progress"
19847
- );
19848
- return;
19849
- }
19850
- const failedCloneInspection = await inspectDestination(
19851
- target.worktreePath,
19852
- target.targetPath
19853
- );
19854
- if (failedCloneInspection.kind === "unsafe") {
19855
- recordBootstrapFailure(
19856
- events,
19857
- reporter,
19858
- adapter,
19859
- target,
19860
- failedCloneInspection.reason,
19861
- "destination-unsafe"
19862
- );
19863
- return;
19864
- }
19865
- input = failedCloneInspection.kind === "exists" ? "preserve" : "clean";
19866
- const repairInput = input === "preserve" ? "the preserved target" : "an empty target";
19867
- recordBootstrapEvent(events, reporter, {
19868
- adapter: adapter.name,
19869
- kind: adapter.kind,
19870
- reason: "cow-seed-failed",
19871
- state: "repair-only",
19872
- target: target.relativePath,
19873
- message: `CoW seed failed; repairing from ${repairInput} (${toErrorMessage2(error)})`
19874
- });
19875
- }
19876
- } else {
19176
+ const effectiveInput = beforeInstallInspection?.kind === "exists" ? "preserve" : "clean";
19177
+ try {
19178
+ await adapter.install(target, { ...execution, input: effectiveInput });
19877
19179
  recordBootstrapEvent(events, reporter, {
19878
19180
  adapter: adapter.name,
19879
19181
  kind: adapter.kind,
19880
- reason: "seed-unavailable",
19881
- state: "repair-only",
19182
+ state: "installed",
19882
19183
  target: target.relativePath,
19883
- message: "CoW seed is unavailable; repairing from an empty target"
19184
+ message: installSuccessMessage(target, effectiveInput)
19884
19185
  });
19885
- }
19886
- await repairTarget(adapter, target, execution, input, events, reporter);
19887
- }
19888
- async function repairTarget(adapter, target, execution, input, events, reporter) {
19889
- const beforeRepairInspection = await inspectDestination(
19890
- target.worktreePath,
19891
- target.targetPath
19892
- );
19893
- if (beforeRepairInspection.kind === "unsafe") {
19186
+ } catch (error) {
19894
19187
  recordBootstrapFailure(
19895
19188
  events,
19896
19189
  reporter,
19897
19190
  adapter,
19898
19191
  target,
19899
- beforeRepairInspection.reason,
19900
- "destination-unsafe"
19901
- );
19902
- return;
19903
- }
19904
- const effectiveInput = beforeRepairInspection.kind === "exists" ? input === "seed" ? "seed" : "preserve" : "clean";
19905
- try {
19906
- await adapter.repair(target, { ...execution, input: effectiveInput });
19907
- recordBootstrapEvent(events, reporter, {
19908
- adapter: adapter.name,
19909
- kind: adapter.kind,
19910
- state: target.repairState,
19911
- target: target.relativePath,
19912
- message: repairSuccessMessage(target, effectiveInput)
19913
- });
19914
- } catch (firstError) {
19915
- const firstFailureContext = {
19916
- input: effectiveInput,
19917
- target
19918
- };
19919
- const cleanupError = await adapter.cleanupAfterRepairFailure(firstFailureContext).then(() => void 0).catch((error) => toErrorMessage2(error));
19920
- if (!adapter.shouldRetryAfterRepairFailure(firstFailureContext)) {
19921
- recordBootstrapFailure(
19922
- events,
19923
- reporter,
19924
- adapter,
19925
- target,
19926
- formatRepairFailure(
19927
- firstError,
19928
- cleanupError === void 0 ? void 0 : cleanupError
19929
- ),
19930
- "repair-failed"
19931
- );
19932
- return;
19933
- }
19934
- if (cleanupError) {
19935
- recordBootstrapFailure(
19936
- events,
19937
- reporter,
19938
- adapter,
19939
- target,
19940
- formatRepairFailure(firstError, cleanupError),
19941
- "repair-cleanup-failed"
19942
- );
19943
- return;
19944
- }
19945
- recordBootstrapEvent(events, reporter, {
19946
- adapter: adapter.name,
19947
- kind: adapter.kind,
19948
- reason: "seed-repair-failed",
19949
- state: "repair-only",
19950
- target: target.relativePath,
19951
- message: `seed repair failed; removed the seed and retrying clean (${toErrorMessage2(firstError)})`
19952
- });
19953
- const beforeRetryInspection = await inspectDestination(
19954
- target.worktreePath,
19955
- target.targetPath
19192
+ formatInstallFailure(error),
19193
+ "install-failed"
19956
19194
  );
19957
- if (beforeRetryInspection.kind === "unsafe") {
19958
- recordBootstrapFailure(
19959
- events,
19960
- reporter,
19961
- adapter,
19962
- target,
19963
- beforeRetryInspection.reason,
19964
- "destination-unsafe"
19965
- );
19966
- return;
19967
- }
19968
- const retryInput = beforeRetryInspection.kind === "exists" ? "preserve" : "clean";
19969
- try {
19970
- await adapter.repair(target, { ...execution, input: retryInput });
19971
- recordBootstrapEvent(events, reporter, {
19972
- adapter: adapter.name,
19973
- kind: adapter.kind,
19974
- reason: "repair-retry",
19975
- state: target.repairState,
19976
- target: target.relativePath,
19977
- message: repairSuccessMessage(target, retryInput)
19978
- });
19979
- } catch (secondError) {
19980
- const cleanupError2 = await adapter.cleanupAfterRepairFailure({
19981
- input: retryInput,
19982
- target
19983
- }).then(() => void 0).catch((error) => toErrorMessage2(error));
19984
- recordBootstrapFailure(
19985
- events,
19986
- reporter,
19987
- adapter,
19988
- target,
19989
- formatRepairFailure(secondError, cleanupError2),
19990
- "repair-failed"
19991
- );
19992
- }
19993
19195
  }
19994
19196
  }
19995
- function repairSuccessMessage(target, input) {
19996
- if (input === "seed") return "reused and repaired";
19997
- if (input === "preserve") return "repaired the existing target";
19998
- return target.repairState === "installed" ? "installed from a clean target" : "repaired from a clean target";
19197
+ function installSuccessMessage(_target, input) {
19198
+ if (input === "preserve") return "installed into the existing target";
19199
+ return "installed into a clean target";
19999
19200
  }
20000
19201
  function recordBootstrapFailure(events, reporter, adapter, target, message, reason) {
20001
19202
  recordBootstrapEvent(events, reporter, {
@@ -20011,169 +19212,156 @@ function recordBootstrapEvent(events, reporter, event) {
20011
19212
  events.push(event);
20012
19213
  reporter.dependency(event);
20013
19214
  }
20014
- function formatRepairFailure(error, cleanupError) {
20015
- const message = `repair failed: ${toErrorMessage2(error)}`;
20016
- return cleanupError ? `${message}; cleanup failed: ${cleanupError}` : message;
20017
- }
20018
- function bootstrapStrategy(mode, target, seedable) {
20019
- if (mode === "install-only" || target.repairState === "installed") {
20020
- return "install-only";
20021
- }
20022
- return seedable ? "cow-then-repair" : "repair-only";
20023
- }
20024
- function adapterForTarget(target) {
20025
- const adapter = createBootstrapAdapters(defaultCheckUvRuntime).find(
20026
- (candidate) => candidate.name === target.adapter
20027
- );
20028
- if (!adapter)
20029
- throw new Error(`unsupported bootstrap adapter: ${target.adapter}`);
20030
- return adapter;
19215
+ function formatInstallFailure(error) {
19216
+ return `install failed: ${toErrorMessage(error)}`;
20031
19217
  }
20032
- function createBootstrapAdapters(checkUvRuntime, cargoBuildCommand) {
19218
+ function createBootstrapAdapters(cargoBuildCommand) {
20033
19219
  return [
20034
19220
  new LockfileBootstrapAdapter({
20035
19221
  name: "pnpm",
20036
19222
  kind: "dependency",
20037
- lockfile: "pnpm-lock.yaml",
19223
+ lockfiles: ["pnpm-lock.yaml"],
20038
19224
  relativePath: "node_modules",
20039
- repairCommand: "pnpm install --frozen-lockfile",
20040
- shell: false,
20041
- beforeRepair: async (target, context) => {
20042
- if (context.input === "seed") {
20043
- await rm4(join12(target.targetPath, ".modules.yaml"), {
20044
- force: true
20045
- });
20046
- }
20047
- }
19225
+ installCommand: "pnpm install --frozen-lockfile"
20048
19226
  }),
20049
19227
  new LockfileBootstrapAdapter({
20050
19228
  name: "yarn",
20051
19229
  kind: "dependency",
20052
- lockfile: "yarn.lock",
19230
+ lockfiles: ["yarn.lock"],
20053
19231
  relativePath: "node_modules",
20054
- repairCommand: "yarn install --immutable",
20055
- shell: false,
20056
- beforeRepair: async (target, context) => {
20057
- if (context.input === "seed") {
20058
- await rm4(join12(target.targetPath, ".yarn-state.yml"), {
20059
- force: true
20060
- });
20061
- }
20062
- }
19232
+ installCommand: "yarn install --frozen-lockfile",
19233
+ selectInstallCommand: async (_target, _input) => await isYarnBerry(_target.detectionRoot ?? _target.sourceRoot) ? "yarn install --immutable" : "yarn install --frozen-lockfile"
19234
+ }),
19235
+ new LockfileBootstrapAdapter({
19236
+ name: "bun",
19237
+ kind: "dependency",
19238
+ lockfiles: ["bun.lock", "bun.lockb"],
19239
+ relativePath: "node_modules",
19240
+ installCommand: "bun install --frozen-lockfile"
20063
19241
  }),
20064
19242
  new LockfileBootstrapAdapter({
20065
19243
  name: "npm",
20066
19244
  kind: "dependency",
20067
- lockfile: "package-lock.json",
19245
+ lockfiles: ["package-lock.json"],
20068
19246
  relativePath: "node_modules",
20069
- repairCommand: "npm ci",
20070
- shell: false,
20071
- seedPolicy: "never",
20072
- selectRepairCommand: (_target, input) => input === "preserve" ? "npm install" : "npm ci",
20073
- repairState: "installed"
19247
+ installCommand: "npm ci",
19248
+ selectInstallCommand: (_target, input) => input === "preserve" ? "npm install" : "npm ci"
20074
19249
  }),
20075
19250
  new LockfileBootstrapAdapter({
20076
19251
  name: "bundler",
20077
19252
  kind: "dependency",
20078
- lockfile: "Gemfile.lock",
19253
+ lockfiles: ["Gemfile.lock"],
20079
19254
  relativePath: "vendor/bundle",
20080
- repairCommand: "bundle install",
20081
- shell: false,
19255
+ installCommand: "bundle install",
20082
19256
  commandOptions: () => ({
20083
19257
  env: { BUNDLE_PATH: "vendor/bundle" }
20084
- }),
20085
- beforeRepair: async (target, context) => {
20086
- if (context.input === "seed") {
20087
- await removeBundlerExtensionMarkers(target.targetPath);
20088
- }
20089
- }
19258
+ })
19259
+ }),
19260
+ new LockfileBootstrapAdapter({
19261
+ name: "poetry",
19262
+ kind: "dependency",
19263
+ lockfiles: ["poetry.lock"],
19264
+ relativePath: ".venv",
19265
+ installCommand: "poetry install --no-interaction",
19266
+ commandOptions: () => ({
19267
+ env: { POETRY_VIRTUALENVS_IN_PROJECT: "true" }
19268
+ })
20090
19269
  }),
20091
19270
  new LockfileBootstrapAdapter({
20092
19271
  name: "uv",
20093
19272
  kind: "dependency",
20094
- lockfile: "uv.lock",
19273
+ lockfiles: ["uv.lock"],
20095
19274
  relativePath: ".venv",
20096
- repairCommand: "uv sync --locked",
20097
- shell: false,
20098
- canSeedOverride: checkUvRuntime,
20099
- beforeRepair: validateUvStructure,
20100
- afterRepair: validateUvRelocation
19275
+ installCommand: "uv sync --locked",
19276
+ beforeInstall: validateUvStructure,
19277
+ afterInstall: validateUvInstallation
19278
+ }),
19279
+ new LockfileBootstrapAdapter({
19280
+ name: "pipenv",
19281
+ kind: "dependency",
19282
+ lockfiles: ["Pipfile.lock"],
19283
+ relativePath: ".venv",
19284
+ installCommand: "pipenv sync",
19285
+ commandOptions: () => ({
19286
+ env: { PIPENV_VENV_IN_PROJECT: "1" }
19287
+ })
19288
+ }),
19289
+ new LockfileBootstrapAdapter({
19290
+ name: "go",
19291
+ kind: "dependency",
19292
+ lockfiles: ["go.mod"],
19293
+ relativePath: "",
19294
+ installCommand: "go mod download"
19295
+ }),
19296
+ new LockfileBootstrapAdapter({
19297
+ name: "composer",
19298
+ kind: "dependency",
19299
+ lockfiles: ["composer.lock"],
19300
+ relativePath: "vendor",
19301
+ installCommand: "composer install --no-interaction --prefer-dist"
20101
19302
  }),
20102
19303
  new LockfileBootstrapAdapter({
20103
19304
  name: "cargo",
20104
19305
  kind: "build-cache",
20105
- lockfile: "Cargo.lock",
19306
+ lockfiles: ["Cargo.lock"],
20106
19307
  relativePath: "target",
20107
- repairCommand: cargoBuildCommand?.trim() || "cargo check",
19308
+ installCommand: cargoBuildCommand?.trim() || "cargo check",
20108
19309
  shell: Boolean(cargoBuildCommand)
20109
19310
  })
20110
19311
  ];
20111
19312
  }
20112
19313
  var LockfileBootstrapAdapter = class {
20113
19314
  kind;
20114
- lockfile;
20115
19315
  name;
20116
19316
  relativePath;
20117
- defaultRepairCommand;
19317
+ defaultInstallCommand;
19318
+ lockfiles;
20118
19319
  shell;
20119
- seedPolicy;
20120
- canSeedOverride;
20121
- beforeRepair;
20122
- afterRepair;
19320
+ beforeInstall;
19321
+ afterInstall;
20123
19322
  commandOptions;
20124
- selectRepairCommand;
20125
- repairState;
19323
+ selectInstallCommand;
20126
19324
  constructor(spec) {
20127
19325
  this.name = spec.name;
20128
19326
  this.kind = spec.kind;
20129
- this.lockfile = spec.lockfile;
19327
+ this.lockfiles = spec.lockfiles;
20130
19328
  this.relativePath = spec.relativePath;
20131
- this.defaultRepairCommand = spec.repairCommand;
20132
- this.shell = spec.shell;
20133
- this.seedPolicy = spec.seedPolicy ?? "always";
20134
- this.canSeedOverride = spec.canSeedOverride;
20135
- this.beforeRepair = spec.beforeRepair;
20136
- this.afterRepair = spec.afterRepair;
19329
+ this.defaultInstallCommand = spec.installCommand;
19330
+ this.shell = spec.shell ?? false;
19331
+ this.beforeInstall = spec.beforeInstall;
19332
+ this.afterInstall = spec.afterInstall;
20137
19333
  this.commandOptions = spec.commandOptions;
20138
- this.selectRepairCommand = spec.selectRepairCommand;
20139
- this.repairState = spec.repairState ?? "repaired";
19334
+ this.selectInstallCommand = spec.selectInstallCommand;
20140
19335
  }
20141
19336
  async detect(context) {
20142
19337
  const detectionRoot = context.detectionRoot ?? context.sourceRoot;
20143
- if (!await pathExists2(join12(detectionRoot, this.lockfile))) return null;
20144
- const sourcePath = join12(context.sourceRoot, this.relativePath);
20145
- const targetPath = join12(context.worktreePath, this.relativePath);
20146
- const destinationInspection = await inspectDestination(
20147
- context.worktreePath,
20148
- targetPath
20149
- );
20150
- const initialInput = destinationInspection.kind === "exists" ? "preserve" : "clean";
19338
+ const hasLockfile = await hasExistingPath(detectionRoot, this.lockfiles);
19339
+ if (!hasLockfile) return null;
19340
+ const sourcePath = this.relativePath ? join12(context.sourceRoot, this.relativePath) : void 0;
19341
+ const targetPath = this.relativePath ? join12(context.worktreePath, this.relativePath) : void 0;
19342
+ const destinationInspection = targetPath ? await inspectDestination(context.worktreePath, targetPath) : void 0;
19343
+ const initialInput = destinationInspection?.kind === "exists" ? "preserve" : "clean";
20151
19344
  const target = {
20152
- adapter: this.name,
20153
19345
  kind: this.kind,
20154
19346
  relativePath: this.relativePath,
20155
19347
  sourceRoot: context.sourceRoot,
19348
+ detectionRoot,
20156
19349
  worktreePath: context.worktreePath,
20157
19350
  sourcePath,
20158
19351
  targetPath,
20159
- repairCommand: this.defaultRepairCommand,
20160
- repairState: this.repairState,
20161
- shell: this.shell,
20162
- initialInput
19352
+ installCommand: this.defaultInstallCommand,
19353
+ shell: this.shell
20163
19354
  };
20164
19355
  return {
20165
19356
  ...target,
20166
- repairCommand: this.selectRepairCommand?.(target, initialInput) ?? target.repairCommand
19357
+ installCommand: await this.selectCommand(target, initialInput)
20167
19358
  };
20168
19359
  }
20169
- seedPath(target) {
20170
- return target.sourcePath;
20171
- }
20172
- async repair(target, context) {
20173
- await this.beforeRepair?.(target, context);
20174
- const repairCommand = this.selectRepairCommand?.(target, context.input) ?? target.repairCommand;
19360
+ async install(target, context) {
19361
+ await this.beforeInstall?.(target, context);
19362
+ const installCommand = await this.selectCommand(target, context.input);
20175
19363
  await context.runCommand(
20176
- repairCommand,
19364
+ installCommand,
20177
19365
  target.worktreePath,
20178
19366
  context.stderr,
20179
19367
  context.stdout,
@@ -20182,275 +19370,32 @@ var LockfileBootstrapAdapter = class {
20182
19370
  shell: target.shell
20183
19371
  }
20184
19372
  );
20185
- await this.afterRepair?.(target, context);
20186
- }
20187
- async canSeed(target) {
20188
- if (this.seedPolicy === "never") return false;
20189
- if (!await safeSourceDirectory(target)) return false;
20190
- return await this.canSeedOverride?.(target) ?? true;
20191
- }
20192
- shouldRetryAfterRepairFailure(context) {
20193
- return context.input === "seed";
19373
+ await this.afterInstall?.(target, context);
20194
19374
  }
20195
- async cleanupAfterRepairFailure(context) {
20196
- if (context.input !== "seed") return;
20197
- await rm4(context.target.targetPath, { force: true, recursive: true });
19375
+ async selectCommand(target, input) {
19376
+ return await this.selectInstallCommand?.(target, input) ?? target.installCommand;
20198
19377
  }
20199
19378
  };
20200
- async function safeSourceDirectory(target) {
20201
- try {
20202
- const [root, source] = await Promise.all([
20203
- realpath3(target.sourceRoot),
20204
- realpath3(target.sourcePath)
20205
- ]);
20206
- const sourceStats = await lstat4(source);
20207
- return sourceStats.isDirectory() && isWithin(root, source);
20208
- } catch {
20209
- return false;
19379
+ async function hasExistingPath(root, paths) {
19380
+ for (const path9 of paths) {
19381
+ if (await pathExists2(join12(root, path9))) return true;
20210
19382
  }
19383
+ return false;
20211
19384
  }
20212
- async function defaultCheckUvRuntime(target) {
19385
+ async function isYarnBerry(root) {
19386
+ if (await pathExists2(join12(root, ".yarnrc.yml"))) return true;
20213
19387
  try {
20214
- const config = await readFile9(
20215
- join12(target.sourcePath, "pyvenv.cfg"),
20216
- "utf8"
19388
+ const packageJson3 = JSON.parse(
19389
+ await readFile8(join12(root, "package.json"), "utf8")
20217
19390
  );
20218
- const expected = config.match(/^version(?:_info)?\s*=\s*(\d+\.\d+)/mu)?.[1];
20219
- if (!expected) return false;
20220
- const expectedImplementation = config.match(/^implementation\s*=\s*([^\s#]+)/imu)?.[1]?.toLowerCase();
20221
- const sourceInterpreter = join12(
20222
- target.sourcePath,
20223
- process.platform === "win32" ? "Scripts/python.exe" : "bin/python"
20224
- );
20225
- const fingerprintScript = "import platform, sys; print(f'{sys.version_info.major}.{sys.version_info.minor}|{platform.machine()}|{sys.implementation.name}')";
20226
- const source = await execFileAsync6(sourceInterpreter, [
20227
- "-c",
20228
- fingerprintScript
20229
- ]);
20230
- const sourceFingerprint = source.stdout.trim();
20231
- const [sourceVersion, sourceMachine, sourceImplementation, extra] = sourceFingerprint.split("|");
20232
- return extra === void 0 && sourceVersion === expected && Boolean(sourceMachine) && Boolean(sourceImplementation) && (expectedImplementation === void 0 || sourceImplementation.toLowerCase() === expectedImplementation);
19391
+ const packageManager = packageJson3.packageManager;
19392
+ const match = typeof packageManager === "string" && packageManager.match(/^yarn@(\d+)/u);
19393
+ return match ? Number(match[1]) >= 2 : false;
20233
19394
  } catch {
20234
19395
  return false;
20235
19396
  }
20236
19397
  }
20237
- function virtualEnvironmentPrefixes(text) {
20238
- const prefixes = [];
20239
- for (const line of text.split(/\r?\n/u)) {
20240
- if (!/\bVIRTUAL_ENV\b/u.test(line)) continue;
20241
- for (const match of line.matchAll(/["']([^"']+)["']/gu)) {
20242
- if (match[1] && isAbsolute4(match[1])) prefixes.push(match[1]);
20243
- }
20244
- const unquoted = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*([^\s"']+)/u)?.[1];
20245
- if (unquoted && isAbsolute4(unquoted)) prefixes.push(unquoted);
20246
- }
20247
- return uniquePaths(prefixes);
20248
- }
20249
- function pythonPrefixFromShebang(text) {
20250
- const command = text.match(/^#!\s*([^\r\n]+)/u)?.[1]?.trim().split(/\s+/u)[0];
20251
- if (!command || !isAbsolute4(command)) return void 0;
20252
- if (!/^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(
20253
- command.split(sep3).at(-1) ?? ""
20254
- )) {
20255
- return void 0;
20256
- }
20257
- return dirname9(dirname9(command));
20258
- }
20259
- function pythonPrefixesFromText(text) {
20260
- const interpreters = [];
20261
- for (const match of text.matchAll(/["'](\/[^"']+)["']/gu)) {
20262
- if (match[1]) interpreters.push(match[1]);
20263
- }
20264
- for (const match of text.matchAll(/(?:^|\s)(\/[^\s"']+)/gmu)) {
20265
- if (match[1]) interpreters.push(match[1]);
20266
- }
20267
- return uniquePaths(
20268
- interpreters.flatMap((interpreter) => {
20269
- const name = interpreter.split(sep3).at(-1) ?? "";
20270
- return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name) ? [dirname9(dirname9(interpreter))] : [];
20271
- })
20272
- );
20273
- }
20274
- async function validateUvRelocation(target, context) {
20275
- if (context.input === "preserve") return;
20276
- const scriptsDirectory = join12(
20277
- target.targetPath,
20278
- process.platform === "win32" ? "Scripts" : "bin"
20279
- );
20280
- const interpreter = join12(
20281
- scriptsDirectory,
20282
- process.platform === "win32" ? "python.exe" : "python"
20283
- );
20284
- const { stdout: stdout3 } = await execFileAsync6(interpreter, [
20285
- "-c",
20286
- "import os, sys; print(os.path.realpath(sys.prefix))"
20287
- ]);
20288
- const [actualPrefix, expectedPrefix] = await Promise.all([
20289
- realpath3(stdout3.trim()),
20290
- realpath3(target.targetPath)
20291
- ]);
20292
- if (actualPrefix !== expectedPrefix) {
20293
- throw new Error(
20294
- `uv environment still points to its source prefix: ${actualPrefix}`
20295
- );
20296
- }
20297
- const sourcePaths = uniquePaths([
20298
- target.sourcePath,
20299
- await realpath3(target.sourcePath).catch(() => void 0)
20300
- ]);
20301
- const entries = await readdir2(scriptsDirectory, { withFileTypes: true });
20302
- assertSafeUvScriptEntries(entries);
20303
- const environmentName = basename7(target.targetPath);
20304
- const acceptedPrefixes = /* @__PURE__ */ new Set([
20305
- resolve9(target.targetPath),
20306
- await realpath3(target.targetPath)
20307
- ]);
20308
- let validatedBytes = 0;
20309
- for (const path9 of [
20310
- join12(target.targetPath, "pyvenv.cfg"),
20311
- ...entries.filter((entry) => entry.isFile()).map((entry) => join12(scriptsDirectory, entry.name))
20312
- ]) {
20313
- const validated = await readBoundedUvTextFile(
20314
- path9,
20315
- UV_VALIDATION_MAX_TOTAL_BYTES - validatedBytes
20316
- );
20317
- if (!validated) continue;
20318
- validatedBytes += validated.bytes;
20319
- if (validated.text === void 0) continue;
20320
- const { text } = validated;
20321
- if (sourcePaths.some((sourcePath) => text.includes(sourcePath))) {
20322
- throw new Error(`uv environment contains a stale source path: ${path9}`);
20323
- }
20324
- const shebangPrefix = pythonPrefixFromShebang(text);
20325
- if (shebangPrefix && basename7(shebangPrefix) === environmentName && !acceptedPrefixes.has(resolve9(shebangPrefix))) {
20326
- throw new Error(`uv launcher points outside its environment: ${path9}`);
20327
- }
20328
- if (pythonPrefixesFromText(text).filter((prefix) => basename7(prefix) === environmentName).some((prefix) => !acceptedPrefixes.has(resolve9(prefix)))) {
20329
- throw new Error(`uv launcher points outside its environment: ${path9}`);
20330
- }
20331
- if (virtualEnvironmentPrefixes(text).some(
20332
- (prefix) => !acceptedPrefixes.has(resolve9(prefix))
20333
- )) {
20334
- throw new Error(
20335
- `uv activation script points outside its environment: ${path9}`
20336
- );
20337
- }
20338
- }
20339
- }
20340
- async function validateUvStructure(target, context) {
20341
- if (context.input === "preserve") return;
20342
- const targetStats = await lstat4(target.targetPath).catch(() => void 0);
20343
- if (!targetStats) return;
20344
- if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
20345
- throw new Error("uv environment must be a real directory");
20346
- }
20347
- const configStats = await lstat4(join12(target.targetPath, "pyvenv.cfg")).catch(
20348
- () => void 0
20349
- );
20350
- if (configStats && (!configStats.isFile() || configStats.isSymbolicLink())) {
20351
- throw new Error("uv pyvenv.cfg must be a regular file");
20352
- }
20353
- const scriptsDirectory = join12(
20354
- target.targetPath,
20355
- process.platform === "win32" ? "Scripts" : "bin"
20356
- );
20357
- const scriptsStats = await lstat4(scriptsDirectory).catch(() => void 0);
20358
- if (!scriptsStats) return;
20359
- if (!scriptsStats.isDirectory() || scriptsStats.isSymbolicLink()) {
20360
- throw new Error("uv scripts path must be a real directory");
20361
- }
20362
- assertSafeUvScriptEntries(
20363
- await readdir2(scriptsDirectory, { withFileTypes: true })
20364
- );
20365
- }
20366
- function assertSafeUvScriptEntries(entries) {
20367
- if (entries.length > UV_VALIDATION_MAX_ENTRIES) {
20368
- throw new Error("uv scripts directory exceeds the validation entry limit");
20369
- }
20370
- for (const entry of entries) {
20371
- if (entry.isSymbolicLink() && !isUvInterpreterName(entry.name)) {
20372
- throw new Error(`uv script must not be a symbolic link: ${entry.name}`);
20373
- }
20374
- }
20375
- }
20376
- async function readBoundedUvTextFile(path9, remainingBytes) {
20377
- let handle;
20378
- try {
20379
- handle = await open3(
20380
- path9,
20381
- constants6.O_RDONLY | (process.platform === "win32" ? 0 : constants6.O_NOFOLLOW)
20382
- );
20383
- } catch (error) {
20384
- if (isNotFoundError(error)) return void 0;
20385
- throw error;
20386
- }
20387
- try {
20388
- const stats = await handle.stat();
20389
- if (!stats.isFile()) return void 0;
20390
- if (stats.size > UV_VALIDATION_MAX_FILE_BYTES) {
20391
- throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
20392
- }
20393
- if (stats.size > remainingBytes) {
20394
- throw new Error("uv launchers exceed the total validation size limit");
20395
- }
20396
- const readLimit = Math.min(UV_VALIDATION_MAX_FILE_BYTES, remainingBytes);
20397
- const contents = await readFilePrefix(handle, readLimit + 1);
20398
- if (contents.byteLength > UV_VALIDATION_MAX_FILE_BYTES) {
20399
- throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
20400
- }
20401
- if (contents.byteLength > remainingBytes) {
20402
- throw new Error("uv launchers exceed the total validation size limit");
20403
- }
20404
- const text = contents.toString("utf8");
20405
- return Buffer.from(text, "utf8").equals(contents) ? { bytes: contents.byteLength, text } : { bytes: contents.byteLength };
20406
- } finally {
20407
- await handle.close();
20408
- }
20409
- }
20410
- async function readFilePrefix(handle, maxBytes) {
20411
- const contents = Buffer.allocUnsafe(maxBytes);
20412
- let offset = 0;
20413
- while (offset < contents.byteLength) {
20414
- const { bytesRead } = await handle.read(
20415
- contents,
20416
- offset,
20417
- contents.byteLength - offset,
20418
- offset
20419
- );
20420
- if (bytesRead === 0) break;
20421
- offset += bytesRead;
20422
- }
20423
- return contents.subarray(0, offset);
20424
- }
20425
- function isUvInterpreterName(name) {
20426
- return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name);
20427
- }
20428
- async function removeBundlerExtensionMarkers(root) {
20429
- const pending = [root];
20430
- while (pending.length > 0) {
20431
- const current = pending.pop();
20432
- if (!current) continue;
20433
- let entries;
20434
- try {
20435
- entries = await readdir2(current, { withFileTypes: true });
20436
- } catch (error) {
20437
- if (isNotFoundError(error)) continue;
20438
- throw error;
20439
- }
20440
- for (const entry of entries) {
20441
- const path9 = join12(current, entry.name);
20442
- if (entry.isDirectory()) pending.push(path9);
20443
- else if (entry.isFile() && isBundlerExtensionMarker(root, path9)) {
20444
- await rm4(path9, { force: true });
20445
- }
20446
- }
20447
- }
20448
- }
20449
- function isBundlerExtensionMarker(root, path9) {
20450
- const segments = relative3(root, path9).split(sep3);
20451
- return segments.length >= 7 && segments[0] === "ruby" && segments[2] === "extensions" && segments.at(-1) === "gem.build_complete";
20452
- }
20453
- function uniquePaths(paths) {
19398
+ function uniquePaths2(paths) {
20454
19399
  return [...new Set(paths.filter((path9) => Boolean(path9)))];
20455
19400
  }
20456
19401
  async function isAllowedSourceRoot(repoRoot, sourceRoot) {
@@ -20460,7 +19405,7 @@ async function isAllowedSourceRoot(repoRoot, sourceRoot) {
20460
19405
  realpath3(sourceRoot)
20461
19406
  ]);
20462
19407
  if (resolvedRepoRoot === resolvedSourceRoot) return true;
20463
- const gitFile = await readFile9(join12(resolvedSourceRoot, ".git"), "utf8");
19408
+ const gitFile = await readFile8(join12(resolvedSourceRoot, ".git"), "utf8");
20464
19409
  const gitDirectory = gitFile.match(/^gitdir:\s*(.+)$/mu)?.[1];
20465
19410
  if (!gitDirectory) return false;
20466
19411
  return isWithin(
@@ -20472,10 +19417,10 @@ async function isAllowedSourceRoot(repoRoot, sourceRoot) {
20472
19417
  }
20473
19418
  }
20474
19419
  function isWithin(root, candidate) {
20475
- const distance = relative3(root, candidate);
19420
+ const distance = relative2(root, candidate);
20476
19421
  return distance === "" || distance !== ".." && !distance.startsWith(`..${sep3}`);
20477
19422
  }
20478
- function toErrorMessage2(error) {
19423
+ function toErrorMessage(error) {
20479
19424
  return error instanceof Error ? error.message : String(error);
20480
19425
  }
20481
19426
 
@@ -20488,27 +19433,17 @@ async function createDependencyBootstrapPreview(mode, context) {
20488
19433
  function formatDependencyBootstrapPreview(preview) {
20489
19434
  if (!preview) return "";
20490
19435
  return preview.targets.map(
20491
- ({ adapter, target, strategy }) => `Would ${formatBootstrapStrategy(strategy)} ${target} with ${adapter}
19436
+ ({ adapter, target, command }) => `Would install ${target || "dependencies"} with ${adapter}: ${command}
20492
19437
  `
20493
19438
  ).join("");
20494
19439
  }
20495
- function formatBootstrapStrategy(strategy) {
20496
- switch (strategy) {
20497
- case "cow-then-repair":
20498
- return "seed and repair";
20499
- case "repair-only":
20500
- return "repair";
20501
- case "install-only":
20502
- return "install";
20503
- }
20504
- }
20505
19440
 
20506
19441
  // src/conflict.ts
20507
- import { constants as constants7 } from "node:fs";
19442
+ import { constants as constants6 } from "node:fs";
20508
19443
  import { access as access7 } from "node:fs/promises";
20509
19444
  async function pathExists3(path9) {
20510
19445
  try {
20511
- await access7(path9, constants7.F_OK);
19446
+ await access7(path9, constants6.F_OK);
20512
19447
  return true;
20513
19448
  } catch {
20514
19449
  return false;
@@ -20523,257 +19458,41 @@ async function promptForPathConflict(path9) {
20523
19458
  label: "Abort",
20524
19459
  hint: "Keep the existing directory untouched"
20525
19460
  },
20526
- {
20527
- value: "reuse",
20528
- label: "Reuse path",
20529
- hint: "Print the existing path and stop"
20530
- }
20531
- ]
20532
- });
20533
- if (pD(choice)) {
20534
- return "abort";
20535
- }
20536
- return choice;
20537
- }
20538
-
20539
- // src/dependency-bootstrap-prompt.ts
20540
- async function resolveDependencyBootstrapPolicy(context, config, dependencyBootstrapExplicit, options) {
20541
- if (dependencyBootstrapExplicit) {
20542
- return {
20543
- mode: config.dependencyBootstrap ?? "off",
20544
- prompted: false,
20545
- source: "explicit"
20546
- };
20547
- }
20548
- if (options.dryRun || options.nonInteractive || isHeadless()) {
20549
- return { mode: "off", prompted: false, source: "default" };
20550
- }
20551
- if (options.legacyInstallPromptConfigured && !options.dependencies?.promptForDependencyBootstrap) {
20552
- return { mode: "off", prompted: false, source: "legacy" };
20553
- }
20554
- let candidate;
20555
- try {
20556
- candidate = await detectDependencyBootstrapCandidate(context);
20557
- } catch (error) {
20558
- options.stderr(
20559
- `gji: dependency setup detection failed: ${toErrorMessage3(error)}
20560
- `
20561
- );
20562
- return { mode: "off", prompted: false, source: "default" };
20563
- }
20564
- if (!candidate) return { mode: "off", prompted: false, source: "default" };
20565
- const prompt = options.dependencies?.promptForDependencyBootstrap ?? defaultPromptForDependencyBootstrap;
20566
- const choice = await prompt(candidate);
20567
- if (choice === null)
20568
- return { mode: "off", prompted: true, source: "prompted" };
20569
- const mode = choice;
20570
- await persistDependencyBootstrapPolicy(
20571
- context.repoRoot,
20572
- config,
20573
- mode,
20574
- options.dependencies,
20575
- options.stderr
20576
- );
20577
- return { mode, prompted: true, source: "prompted" };
20578
- }
20579
- async function persistDependencyBootstrapPolicy(repoRoot, config, mode, dependencies, stderr) {
20580
- const writeLocal = dependencies?.writeConfigKey ?? (async (root, key, value) => {
20581
- await updateLocalConfigKey(root, key, value);
20582
- });
20583
- const writeGlobal = dependencies?.writeGlobalRepoConfigKey ?? (async (root, key, value) => {
20584
- await updateGlobalRepoConfigKey(root, key, value);
20585
- });
20586
- try {
20587
- if (config.installSaveTarget === "global") {
20588
- await writeGlobal(repoRoot, "dependencyBootstrap", mode);
20589
- } else {
20590
- await writeLocal(repoRoot, "dependencyBootstrap", mode);
20591
- }
20592
- } catch (error) {
20593
- stderr(
20594
- `gji: failed to save dependencyBootstrap: ${toErrorMessage3(error)}
20595
- `
20596
- );
20597
- }
20598
- }
20599
- async function defaultPromptForDependencyBootstrap(candidate) {
20600
- const reuseHint = formatReuseHint(candidate);
20601
- const subject = candidate.kind === "build-cache" ? "dependencies and build state" : "dependencies";
20602
- const choice = await ve({
20603
- message: `Set up ${candidate.adapter} ${subject} for new worktrees? (${candidate.lockfile})`,
20604
- options: [
20605
- {
20606
- value: "cow-then-repair",
20607
- label: "Reuse and repair (recommended)",
20608
- hint: reuseHint
20609
- },
20610
- {
20611
- value: "install-only",
20612
- label: "Install fresh each time",
20613
- hint: candidate.repairCommand
20614
- },
20615
- {
20616
- value: "off",
20617
- label: "Skip dependency setup",
20618
- hint: "leave dependencies to project hooks or manual setup"
20619
- }
20620
- ]
20621
- });
20622
- if (pD(choice)) return null;
20623
- return choice;
20624
- }
20625
- function formatReuseHint(candidate) {
20626
- if (candidate.adapter === "pnpm") {
20627
- return "reuse local node_modules through CoW, then run pnpm install --frozen-lockfile";
20628
- }
20629
- if (candidate.adapter === "npm") {
20630
- return "npm uses a clean install because node_modules is not seeded";
20631
- }
20632
- if (!candidate.seedable) {
20633
- return `repair ${candidate.target} with ${candidate.repairCommand}`;
20634
- }
20635
- return `reuse ${candidate.target} through CoW, then run ${candidate.repairCommand}`;
20636
- }
20637
- function toErrorMessage3(error) {
20638
- return error instanceof Error ? error.message : String(error);
20639
- }
20640
-
20641
- // src/sync-plan.ts
20642
- import { lstat as lstat5, realpath as realpath4 } from "node:fs/promises";
20643
- import { isAbsolute as isAbsolute5, join as join13, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
20644
- async function prepareSyncDirectoryPlan(repoRoot, worktreePath, directories) {
20645
- const normalizedDirectories = directories.map(validateSyncDirPattern).sort(directoryDepthAscending);
20646
- const plan = [];
20647
- for (const directory of normalizedDirectories) {
20648
- const destination = join13(worktreePath, directory);
20649
- const destinationState = await inspectDestination(
20650
- worktreePath,
20651
- destination
20652
- );
20653
- const destinationWasPresent = destinationState.kind === "exists";
20654
- const destinationWarning = destinationState.kind === "unsafe" ? destinationState.reason : void 0;
20655
- try {
20656
- const source = await resolveSyncDirectorySource(repoRoot, directory);
20657
- if (!source) {
20658
- plan.push({
20659
- directory,
20660
- destination,
20661
- worktreePath,
20662
- destinationWasPresent,
20663
- destinationWarning
20664
- });
20665
- } else if ("warning" in source) {
20666
- plan.push({
20667
- directory,
20668
- destination,
20669
- worktreePath,
20670
- destinationWasPresent,
20671
- destinationWarning,
20672
- warning: source.warning
20673
- });
20674
- } else {
20675
- plan.push({
20676
- directory,
20677
- destination,
20678
- worktreePath,
20679
- destinationWasPresent,
20680
- destinationWarning,
20681
- source: source.path
20682
- });
20683
- }
20684
- } catch (error) {
20685
- plan.push({
20686
- directory,
20687
- destination,
20688
- worktreePath,
20689
- destinationWasPresent,
20690
- destinationWarning,
20691
- warning: `could not inspect ${directory}: ${toErrorMessage4(error)}`
20692
- });
20693
- }
20694
- }
20695
- return plan;
20696
- }
20697
- async function estimateSyncDirectoryPlan(plan) {
20698
- const estimates = [];
20699
- for (const entry of plan) {
20700
- if (entry.destinationWasPresent || entry.destinationWarning || !entry.source || entry.warning || isCoveredByCloneAncestor(entry, plan)) {
20701
- continue;
20702
- }
20703
- try {
20704
- estimates.push({
20705
- bytes: await directorySize(entry.source),
20706
- dir: entry.directory
20707
- });
20708
- } catch {
20709
- }
20710
- }
20711
- return estimates;
20712
- }
20713
- async function estimateSyncDirectories(repoRoot, worktreePath, directories) {
20714
- const plan = await prepareSyncDirectoryPlan(
20715
- repoRoot,
20716
- worktreePath,
20717
- directories
20718
- );
20719
- return estimateSyncDirectoryPlan(plan);
20720
- }
20721
- async function resolveSyncDirectorySource(repoRoot, directory) {
20722
- const source = join13(repoRoot, directory);
20723
- let resolvedSource;
20724
- try {
20725
- resolvedSource = await realpath4(source);
20726
- } catch (error) {
20727
- if (isNotFoundError(error)) return null;
20728
- throw error;
20729
- }
20730
- if (!isPathInside2(repoRoot, resolvedSource)) {
20731
- return {
20732
- warning: `source symlink resolves outside the repository (${resolvedSource})`
20733
- };
19461
+ {
19462
+ value: "reuse",
19463
+ label: "Reuse path",
19464
+ hint: "Print the existing path and stop"
19465
+ }
19466
+ ]
19467
+ });
19468
+ if (pD(choice)) {
19469
+ return "abort";
20734
19470
  }
20735
- const stats = await lstat5(resolvedSource);
20736
- if (!stats.isDirectory()) return { warning: "source is not a directory" };
20737
- return { path: resolvedSource };
20738
- }
20739
- function isCoveredByCloneAncestor(entry, plan) {
20740
- return plan.some(
20741
- (candidate) => candidate !== entry && !candidate.destinationWasPresent && !candidate.destinationWarning && !!candidate.source && isPathInside2(candidate.directory, entry.directory)
20742
- );
20743
- }
20744
- function directoryDepthAscending(left, right) {
20745
- return left.split(/[\\/]+/u).length - right.split(/[\\/]+/u).length;
20746
- }
20747
- function isPathInside2(parent, child) {
20748
- const relativePath = relative4(resolve10(parent), resolve10(child));
20749
- return relativePath !== "" && !isAbsolute5(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep4}`);
20750
- }
20751
- function toErrorMessage4(error) {
20752
- return error instanceof Error ? error.message : String(error);
19471
+ return choice;
20753
19472
  }
20754
19473
 
20755
19474
  // src/worktree-bootstrap.ts
20756
- import { basename as basename9 } from "node:path";
19475
+ import { basename as basename8 } from "node:path";
20757
19476
 
20758
19477
  // src/file-sync.ts
20759
- import { constants as constants8 } from "node:fs";
20760
- import { copyFile, lstat as lstat6, realpath as realpath5 } from "node:fs/promises";
19478
+ import { constants as constants7 } from "node:fs";
19479
+ import { copyFile, lstat as lstat4, realpath as realpath4 } from "node:fs/promises";
20761
19480
  import {
20762
- basename as basename8,
20763
- dirname as dirname10,
20764
- isAbsolute as isAbsolute6,
20765
- join as join14,
20766
- normalize as normalize2,
20767
- relative as relative5,
20768
- resolve as resolve11,
20769
- sep as sep5
19481
+ basename as basename7,
19482
+ dirname as dirname9,
19483
+ isAbsolute as isAbsolute4,
19484
+ join as join13,
19485
+ normalize,
19486
+ relative as relative3,
19487
+ resolve as resolve10,
19488
+ sep as sep4
20770
19489
  } from "node:path";
20771
19490
  async function syncFiles(mainRoot, targetPath, patterns) {
20772
19491
  for (const pattern of patterns) {
20773
19492
  const normalized = validateSyncFilePattern(pattern);
20774
19493
  const sourcePath = await resolveSyncFileSource(mainRoot, normalized);
20775
19494
  if (!sourcePath) continue;
20776
- const destPath = join14(targetPath, normalized);
19495
+ const destPath = join13(targetPath, normalized);
20777
19496
  const existingDestination = await readDestinationEntry(destPath);
20778
19497
  if (existingDestination?.isSymbolicLink()) {
20779
19498
  throw new Error(`destination is a symbolic link: ${destPath}`);
@@ -20781,7 +19500,7 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20781
19500
  if (existingDestination) {
20782
19501
  continue;
20783
19502
  }
20784
- const destinationParent = dirname10(destPath);
19503
+ const destinationParent = dirname9(destPath);
20785
19504
  const beforeCreate = await inspectDestination(
20786
19505
  targetPath,
20787
19506
  destinationParent
@@ -20794,14 +19513,14 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20794
19513
  destinationParent
20795
19514
  );
20796
19515
  try {
20797
- const safeDestination = join14(safeParent.path, basename8(destPath));
19516
+ const safeDestination = join13(safeParent.path, basename7(destPath));
20798
19517
  const safeExistingDestination = await readDestinationEntry(safeDestination);
20799
19518
  if (safeExistingDestination?.isSymbolicLink()) {
20800
19519
  throw new Error(`destination is a symbolic link: ${destPath}`);
20801
19520
  }
20802
19521
  if (safeExistingDestination) continue;
20803
19522
  try {
20804
- await copyFile(sourcePath, safeDestination, constants8.COPYFILE_EXCL);
19523
+ await copyFile(sourcePath, safeDestination, constants7.COPYFILE_EXCL);
20805
19524
  } catch (error) {
20806
19525
  if (!isAlreadyExistsError(error)) throw error;
20807
19526
  }
@@ -20811,12 +19530,12 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20811
19530
  }
20812
19531
  }
20813
19532
  function validateSyncFilePattern(pattern) {
20814
- if (isAbsolute6(pattern)) {
19533
+ if (isAbsolute4(pattern)) {
20815
19534
  throw new Error(
20816
19535
  `syncFiles: pattern must be a relative path, got: ${pattern}`
20817
19536
  );
20818
19537
  }
20819
- const normalized = normalize2(pattern);
19538
+ const normalized = normalize(pattern);
20820
19539
  if (normalized.startsWith("..")) {
20821
19540
  throw new Error(
20822
19541
  `syncFiles: pattern must not contain '..' segments, got: ${pattern}`
@@ -20829,424 +19548,42 @@ async function resolveSyncFileSource(mainRoot, pattern) {
20829
19548
  let resolvedSource;
20830
19549
  try {
20831
19550
  [resolvedRoot, resolvedSource] = await Promise.all([
20832
- realpath5(mainRoot),
20833
- realpath5(join14(mainRoot, pattern))
19551
+ realpath4(mainRoot),
19552
+ realpath4(join13(mainRoot, pattern))
20834
19553
  ]);
20835
19554
  } catch (error) {
20836
19555
  if (isNotFoundError(error)) return void 0;
20837
19556
  throw error;
20838
19557
  }
20839
- if (!isPathInside3(resolvedRoot, resolvedSource)) {
19558
+ if (!isPathInside2(resolvedRoot, resolvedSource)) {
20840
19559
  throw new Error(
20841
19560
  `syncFiles: source symlink resolves outside the repository: ${resolvedSource}`
20842
19561
  );
20843
19562
  }
20844
- const sourceStats = await lstat6(resolvedSource);
19563
+ const sourceStats = await lstat4(resolvedSource);
20845
19564
  if (!sourceStats.isFile()) {
20846
19565
  throw new Error(
20847
- `syncFiles: source is not a file: ${join14(mainRoot, pattern)}`
19566
+ `syncFiles: source is not a file: ${join13(mainRoot, pattern)}`
20848
19567
  );
20849
19568
  }
20850
19569
  return resolvedSource;
20851
19570
  }
20852
19571
  async function readDestinationEntry(path9) {
20853
19572
  try {
20854
- return await lstat6(path9);
19573
+ return await lstat4(path9);
20855
19574
  } catch (error) {
20856
19575
  if (isNotFoundError(error)) return void 0;
20857
19576
  throw error;
20858
19577
  }
20859
19578
  }
20860
- function isPathInside3(root, candidate) {
20861
- const distance = relative5(resolve11(root), resolve11(candidate));
20862
- return distance === "" || !isAbsolute6(distance) && distance !== ".." && !distance.startsWith(`..${sep5}`);
20863
- }
20864
-
20865
- // src/package-manager.ts
20866
- import { access as access8, readdir as readdir3 } from "node:fs/promises";
20867
- import { join as join15 } from "node:path";
20868
- var ENTRIES = [
20869
- // JavaScript / TypeScript
20870
- { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
20871
- { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
20872
- { name: "bun", signals: ["bun.lockb"], command: "bun install" },
20873
- { name: "npm", signals: ["package-lock.json"], command: "npm install" },
20874
- { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
20875
- // Python
20876
- { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
20877
- { name: "uv", signals: ["uv.lock"], command: "uv sync" },
20878
- { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
20879
- { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
20880
- {
20881
- name: "conda-lock",
20882
- signals: ["conda-lock.yml"],
20883
- command: "conda-lock install"
20884
- },
20885
- {
20886
- name: "conda",
20887
- signals: ["environment.yml"],
20888
- command: "conda env update --file environment.yml"
20889
- },
20890
- // R
20891
- {
20892
- name: "renv",
20893
- signals: ["renv.lock"],
20894
- command: "Rscript -e 'renv::restore()'"
20895
- },
20896
- // Rust
20897
- { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
20898
- // Go
20899
- { name: "go", signals: ["go.sum"], command: "go mod download" },
20900
- // Ruby
20901
- { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
20902
- // PHP
20903
- { name: "composer", signals: ["composer.lock"], command: "composer install" },
20904
- // Elixir / Erlang
20905
- { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
20906
- { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
20907
- // Dart / Flutter
20908
- { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
20909
- // Java / Kotlin / Scala
20910
- { name: "maven", signals: ["pom.xml"], command: "mvn install" },
20911
- { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
20912
- {
20913
- name: "gradle",
20914
- signals: ["build.gradle", "build.gradle.kts"],
20915
- command: "gradle build"
20916
- },
20917
- { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
20918
- // .NET (C# / F# / VB)
20919
- {
20920
- name: "dotnet",
20921
- signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
20922
- command: "dotnet restore",
20923
- glob: true
20924
- },
20925
- // Swift
20926
- {
20927
- name: "swift",
20928
- signals: ["Package.swift"],
20929
- command: "swift package resolve"
20930
- },
20931
- // Haskell
20932
- { name: "stack", signals: ["stack.yaml"], command: "stack build" },
20933
- {
20934
- name: "cabal",
20935
- signals: ["cabal.project"],
20936
- command: "cabal install --only-dependencies"
20937
- },
20938
- {
20939
- name: "cabal",
20940
- signals: ["*.cabal"],
20941
- command: "cabal install --only-dependencies",
20942
- glob: true
20943
- },
20944
- // Clojure
20945
- { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
20946
- { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
20947
- // OCaml
20948
- { name: "dune", signals: ["dune-project"], command: "dune build" },
20949
- // Julia
20950
- {
20951
- name: "julia",
20952
- signals: ["Manifest.toml"],
20953
- command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
20954
- },
20955
- // Nim
20956
- {
20957
- name: "nimble",
20958
- signals: ["*.nimble"],
20959
- command: "nimble install",
20960
- glob: true
20961
- },
20962
- // Crystal
20963
- { name: "shards", signals: ["shard.yml"], command: "shards install" },
20964
- // Perl
20965
- { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
20966
- // Zig
20967
- { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
20968
- // C / C++
20969
- { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
20970
- {
20971
- name: "conan",
20972
- signals: ["conanfile.py", "conanfile.txt"],
20973
- command: "conan install ."
20974
- },
20975
- // Nix
20976
- { name: "nix", signals: ["flake.nix"], command: "nix develop" },
20977
- { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
20978
- // Terraform / OpenTofu
20979
- {
20980
- name: "terraform",
20981
- signals: ["terraform.lock.hcl"],
20982
- command: "terraform init"
20983
- }
20984
- ];
20985
- async function detectPackageManager(repoRoot) {
20986
- for (const entry of ENTRIES) {
20987
- const matched = entry.glob ? await matchesGlob(repoRoot, entry.signals) : await matchesExact(repoRoot, entry.signals);
20988
- if (matched) {
20989
- return { name: entry.name, installCommand: entry.command };
20990
- }
20991
- }
20992
- return null;
20993
- }
20994
- async function matchesExact(repoRoot, signals) {
20995
- for (const signal of signals) {
20996
- try {
20997
- await access8(join15(repoRoot, signal));
20998
- return true;
20999
- } catch {
21000
- }
21001
- }
21002
- return false;
21003
- }
21004
- async function matchesGlob(repoRoot, patterns) {
21005
- let files;
21006
- try {
21007
- files = await readdir3(repoRoot);
21008
- } catch {
21009
- return false;
21010
- }
21011
- const regexes = patterns.map(patternToRegex);
21012
- return files.some((file) => regexes.some((re) => re.test(file)));
21013
- }
21014
- function patternToRegex(pattern) {
21015
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
21016
- return new RegExp(`^${escaped}$`);
21017
- }
21018
-
21019
- // src/install-prompt.ts
21020
- async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
21021
- if (isHeadless() || nonInteractive) {
21022
- return;
21023
- }
21024
- if (extractHooks(config)["after-create"]) {
21025
- return;
21026
- }
21027
- if (config.skipInstallPrompt === true) {
21028
- return;
21029
- }
21030
- const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
21031
- const pm = await detect(worktreePath);
21032
- if (!pm) {
21033
- return;
21034
- }
21035
- const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
21036
- const choice = await prompt(pm);
21037
- if (!choice || choice === "no") {
21038
- return;
21039
- }
21040
- if (choice === "yes" || choice === "always") {
21041
- const runner = dependencies.runInstallCommand ?? runInstallCommand;
21042
- try {
21043
- await runner(pm.installCommand, worktreePath, stderr);
21044
- } catch (error) {
21045
- stderr(
21046
- `gji: install command failed: ${error instanceof Error ? error.message : String(error)}
21047
- `
21048
- );
21049
- }
21050
- }
21051
- const saveGlobal = config.installSaveTarget === "global";
21052
- const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
21053
- const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
21054
- if (choice === "always") {
21055
- try {
21056
- if (saveGlobal) {
21057
- const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
21058
- const existing = extractHooks({ hooks: existingRaw });
21059
- await writeGlobalKey(repoRoot, "hooks", {
21060
- ...existing["after-enter"] !== void 0 && {
21061
- "after-enter": existing["after-enter"]
21062
- },
21063
- ...existing["before-remove"] !== void 0 && {
21064
- "before-remove": existing["before-remove"]
21065
- },
21066
- "after-create": pm.installCommand
21067
- });
21068
- } else {
21069
- const { config: localConfig } = await loadConfig(repoRoot);
21070
- const existing = extractHooks(localConfig);
21071
- await writeKey(repoRoot, "hooks", {
21072
- ...existing["after-enter"] !== void 0 && {
21073
- "after-enter": existing["after-enter"]
21074
- },
21075
- ...existing["before-remove"] !== void 0 && {
21076
- "before-remove": existing["before-remove"]
21077
- },
21078
- "after-create": pm.installCommand
21079
- });
21080
- }
21081
- } catch (error) {
21082
- stderr(
21083
- `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
21084
- `
21085
- );
21086
- }
21087
- }
21088
- if (choice === "never") {
21089
- try {
21090
- if (saveGlobal) {
21091
- await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
21092
- } else {
21093
- await writeKey(repoRoot, "skipInstallPrompt", true);
21094
- }
21095
- } catch (error) {
21096
- stderr(
21097
- `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
21098
- `
21099
- );
21100
- }
21101
- }
21102
- }
21103
- var runInstallCommand = runCommand;
21104
- async function defaultWriteConfigKey(root, key, value) {
21105
- await updateLocalConfigKey(root, key, value);
21106
- }
21107
- async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
21108
- await updateGlobalRepoConfigKey(repoRoot, key, value);
21109
- }
21110
- async function loadExistingGlobalRepoHooks(repoRoot) {
21111
- const { config: globalConfig } = await loadGlobalConfig();
21112
- const repos = isPlainObject2(globalConfig.repos) ? globalConfig.repos : {};
21113
- const perRepo = isPlainObject2(repos[repoRoot]) ? repos[repoRoot] : {};
21114
- return isPlainObject2(perRepo.hooks) ? perRepo.hooks : {};
21115
- }
21116
- async function defaultPromptForInstallChoice(pm) {
21117
- const choice = await ve({
21118
- message: `Run \`${pm.installCommand}\` in the new worktree?`,
21119
- options: [
21120
- { value: "yes", label: "Yes", hint: "run once" },
21121
- { value: "no", label: "No", hint: "skip this time" },
21122
- { value: "always", label: "Always", hint: "save as after-create hook" },
21123
- {
21124
- value: "never",
21125
- label: "Never",
21126
- hint: "disable this prompt for this repo"
21127
- }
21128
- ]
21129
- });
21130
- if (pD(choice)) {
21131
- return null;
21132
- }
21133
- return choice;
21134
- }
21135
- function isPlainObject2(value) {
21136
- return typeof value === "object" && value !== null && !Array.isArray(value);
21137
- }
21138
-
21139
- // src/sync-directories.ts
21140
- async function executeSyncDirectoryPlan(plan, options) {
21141
- const outcomes = [];
21142
- for (const entry of plan) {
21143
- if (entry.destinationWarning) {
21144
- recordSkipped(
21145
- outcomes,
21146
- options.reporter,
21147
- entry.directory,
21148
- entry.destinationWarning
21149
- );
21150
- continue;
21151
- }
21152
- const destinationState = await inspectDestination(
21153
- entry.worktreePath,
21154
- entry.destination
21155
- );
21156
- if (destinationState.kind === "exists") {
21157
- recordSkipped(
21158
- outcomes,
21159
- options.reporter,
21160
- entry.directory,
21161
- "destination already exists"
21162
- );
21163
- continue;
21164
- }
21165
- if (destinationState.kind === "unsafe") {
21166
- const reason = destinationState.reason;
21167
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
21168
- continue;
21169
- }
21170
- if (entry.warning) {
21171
- recordSkipped(outcomes, options.reporter, entry.directory, entry.warning);
21172
- continue;
21173
- }
21174
- if (!entry.source) {
21175
- recordSkipped(
21176
- outcomes,
21177
- options.reporter,
21178
- entry.directory,
21179
- "source does not exist"
21180
- );
21181
- continue;
21182
- }
21183
- const refreshedDestinationState = await inspectDestination(
21184
- entry.worktreePath,
21185
- entry.destination
21186
- );
21187
- if (refreshedDestinationState.kind !== "missing") {
21188
- recordSkipped(
21189
- outcomes,
21190
- options.reporter,
21191
- entry.directory,
21192
- refreshedDestinationState.kind === "unsafe" ? refreshedDestinationState.reason : "destination already exists"
21193
- );
21194
- continue;
21195
- }
21196
- let result;
21197
- try {
21198
- const cloneOptions = {
21199
- destinationRoot: entry.worktreePath,
21200
- measureBytes: options.reporter.measureCloneSize
21201
- };
21202
- result = await options.cloneDirectory(
21203
- entry.source,
21204
- entry.destination,
21205
- cloneOptions
21206
- );
21207
- } catch (error) {
21208
- if (isCloneDestinationExistsError(error)) {
21209
- recordSkipped(
21210
- outcomes,
21211
- options.reporter,
21212
- entry.directory,
21213
- "destination already exists"
21214
- );
21215
- continue;
21216
- }
21217
- if (isCloneInProgressError(error)) {
21218
- const reason2 = "copy-on-write clone already in progress";
21219
- recordSkipped(outcomes, options.reporter, entry.directory, reason2);
21220
- continue;
21221
- }
21222
- const reason = toErrorMessage5(error);
21223
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
21224
- continue;
21225
- }
21226
- const clonedDirectory = {
21227
- bytes: result.bytes,
21228
- dir: entry.directory,
21229
- ms: result.ms
21230
- };
21231
- const outcome = { kind: "cloned", directory: clonedDirectory };
21232
- outcomes.push(outcome);
21233
- options.reporter.cloned(clonedDirectory);
21234
- }
21235
- return outcomes;
21236
- }
21237
- function recordSkipped(outcomes, reporter, dir, reason) {
21238
- outcomes.push({ kind: "skipped", dir, reason });
21239
- if (reporter.skipped) reporter.skipped({ dir, reason });
21240
- else reporter.write(`syncDirs: ${reason}, skipped ${dir}
21241
- `);
21242
- }
21243
- function toErrorMessage5(error) {
21244
- return error instanceof Error ? error.message : String(error);
19579
+ function isPathInside2(root, candidate) {
19580
+ const distance = relative3(resolve10(root), resolve10(candidate));
19581
+ return distance === "" || !isAbsolute4(distance) && distance !== ".." && !distance.startsWith(`..${sep4}`);
21245
19582
  }
21246
19583
 
21247
19584
  // src/worktree-bootstrap.ts
21248
19585
  async function bootstrapWorktree(options) {
21249
- const dependencyMode = options.config.dependencyBootstrap ?? "off";
19586
+ const dependencyMode = options.dependencyMode;
21250
19587
  const dependencyPlan = await prepareDependencyBootstrap(dependencyMode, {
21251
19588
  currentRoot: options.currentRoot,
21252
19589
  detectionRoot: options.dependencyDetectionRoot,
@@ -21254,27 +19591,12 @@ async function bootstrapWorktree(options) {
21254
19591
  cargoBuildCommand: options.config.dependencyBuildCommand,
21255
19592
  worktreePath: options.worktreePath
21256
19593
  });
21257
- const syncPlan = await prepareSyncDirectoryPlan(
21258
- options.repoRoot,
21259
- options.worktreePath,
21260
- options.config.syncDirs ?? []
21261
- );
21262
- const outcomes = await executeSyncDirectoryPlan(syncPlan, {
21263
- cloneDirectory: options.cloneDirectory ?? cloneDir,
21264
- reporter: options.reporter
21265
- });
21266
- const clonedDirs = outcomes.flatMap(
21267
- (outcome) => outcome.kind === "cloned" ? [outcome.directory] : []
21268
- );
21269
- const skippedDirs = outcomes.flatMap(
21270
- (outcome) => outcome.kind === "skipped" ? [{ dir: outcome.dir, reason: outcome.reason }] : []
21271
- );
21272
19594
  const syncFileFailures = [];
21273
19595
  for (const pattern of options.config.syncFiles ?? []) {
21274
19596
  try {
21275
19597
  await syncFiles(options.repoRoot, options.worktreePath, [pattern]);
21276
19598
  } catch (error) {
21277
- const message = `failed to sync file "${pattern}": ${toErrorMessage6(error)}`;
19599
+ const message = `failed to sync file "${pattern}": ${toErrorMessage2(error)}`;
21278
19600
  if (!options.json) options.reporter.write(`Warning: ${message}
21279
19601
  `);
21280
19602
  syncFileFailures.push({
@@ -21291,34 +19613,18 @@ async function bootstrapWorktree(options) {
21291
19613
  for (const event of syncFileFailures) options.reporter.dependency(event);
21292
19614
  }
21293
19615
  const dependencyBootstrap = syncFileFailures.length > 0 ? { mode: dependencyMode, ready: false, events: [] } : await executeDependencyBootstrap(dependencyPlan, {
21294
- cloneDirectory: options.cloneDirectory,
21295
19616
  reporter: options.reporter,
21296
19617
  stderr: options.commandStderr ?? options.reporter.write,
21297
19618
  stdout: options.commandStdout,
21298
- seededDirectories: clonedDirs.map(({ dir }) => dir),
21299
19619
  runCommand: options.runCommand
21300
19620
  });
21301
19621
  if (!dependencyBootstrap.ready) {
21302
19622
  return {
21303
- clonedDirs,
21304
19623
  dependencyBootstrap,
21305
19624
  ready: false,
21306
- syncFileFailures,
21307
- skippedDirs
19625
+ syncFileFailures
21308
19626
  };
21309
19627
  }
21310
- if (dependencyMode === "off" && ["default", "legacy"].includes(
21311
- options.dependencyBootstrapPolicy?.source ?? "default"
21312
- )) {
21313
- await maybeRunInstallPrompt(
21314
- options.worktreePath,
21315
- options.repoRoot,
21316
- options.config,
21317
- options.reporter.write,
21318
- options.installDependencies,
21319
- options.nonInteractive
21320
- );
21321
- }
21322
19628
  const hooks = extractHooks(options.config);
21323
19629
  const slot = await assignWorktreeSlot(options.repoRoot, options.worktreePath);
21324
19630
  await runHook(
@@ -21327,26 +19633,24 @@ async function bootstrapWorktree(options) {
21327
19633
  {
21328
19634
  branch: options.branch,
21329
19635
  path: options.worktreePath,
21330
- repo: basename9(options.repoRoot),
19636
+ repo: basename8(options.repoRoot),
21331
19637
  slot
21332
19638
  },
21333
19639
  options.reporter.write,
21334
19640
  options.json ? () => void 0 : options.commandStdout ?? ((chunk) => process.stdout.write(chunk))
21335
19641
  );
21336
19642
  return {
21337
- clonedDirs,
21338
19643
  dependencyBootstrap,
21339
19644
  ready: true,
21340
- syncFileFailures: [],
21341
- skippedDirs
19645
+ syncFileFailures: []
21342
19646
  };
21343
19647
  }
21344
- function toErrorMessage6(error) {
19648
+ function toErrorMessage2(error) {
21345
19649
  return error instanceof Error ? error.message : String(error);
21346
19650
  }
21347
19651
 
21348
19652
  // src/pr.ts
21349
- var execFileAsync7 = promisify8(execFile7);
19653
+ var execFileAsync6 = promisify7(execFile6);
21350
19654
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
21351
19655
  function parsePrInput(input) {
21352
19656
  if (/^\d+$/.test(input)) return input;
@@ -21375,15 +19679,12 @@ function createPrCommand(dependencies = {}) {
21375
19679
  }
21376
19680
  const repository = await detectRepository(options.cwd);
21377
19681
  let config;
21378
- let dependencyBootstrapExplicit;
21379
19682
  try {
21380
- const loaded = await loadEffectiveConfigResult(
19683
+ config = await loadEffectiveConfig(
21381
19684
  repository.repoRoot,
21382
19685
  void 0,
21383
19686
  options.json ? void 0 : options.stderr
21384
19687
  );
21385
- config = loaded.config;
21386
- dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
21387
19688
  } catch (error) {
21388
19689
  const message = error instanceof Error ? error.message : String(error);
21389
19690
  if (options.json) {
@@ -21434,47 +19735,16 @@ function createPrCommand(dependencies = {}) {
21434
19735
  );
21435
19736
  return 1;
21436
19737
  }
21437
- let dependencyPolicy = {
21438
- mode: config.dependencyBootstrap ?? "off",
21439
- prompted: false,
21440
- source: dependencyBootstrapExplicit ? "explicit" : "default"
21441
- };
21442
- if (options.dryRun) {
21443
- dependencyPolicy = await resolveDependencyBootstrapPolicy(
21444
- {
21445
- currentRoot: repository.currentRoot,
21446
- repoRoot: repository.repoRoot,
21447
- worktreePath
21448
- },
21449
- config,
21450
- dependencyBootstrapExplicit,
21451
- {
21452
- dependencies,
21453
- dryRun: true,
21454
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
21455
- nonInteractive: !!options.json,
21456
- stderr: options.stderr
21457
- }
21458
- );
21459
- config = {
21460
- ...config,
21461
- dependencyBootstrap: dependencyPolicy.mode
21462
- };
21463
- }
21464
- const dryRunSyncDirs = options.dryRun ? await estimateSyncDirectories(
21465
- repository.repoRoot,
21466
- worktreePath,
21467
- config.syncDirs ?? []
21468
- ) : [];
21469
- const dryRunDependencyBootstrap = options.dryRun ? await createDependencyBootstrapPreview(
21470
- config.dependencyBootstrap ?? "off",
21471
- {
21472
- currentRoot: repository.currentRoot,
21473
- repoRoot: repository.repoRoot,
21474
- cargoBuildCommand: config.dependencyBuildCommand,
21475
- worktreePath
21476
- }
21477
- ) : void 0;
19738
+ const dependencyMode = resolveDependencyBootstrapMode(
19739
+ config.dependencyBootstrap,
19740
+ options.noInstall
19741
+ );
19742
+ const dryRunDependencyBootstrap = options.dryRun ? await createDependencyBootstrapPreview(dependencyMode, {
19743
+ currentRoot: repository.currentRoot,
19744
+ repoRoot: repository.repoRoot,
19745
+ cargoBuildCommand: config.dependencyBuildCommand,
19746
+ worktreePath
19747
+ }) : void 0;
21478
19748
  if (options.dryRun) {
21479
19749
  if (options.json) {
21480
19750
  const output = {
@@ -21488,7 +19758,6 @@ function createPrCommand(dependencies = {}) {
21488
19758
  ),
21489
19759
  dryRun: true
21490
19760
  };
21491
- if (dryRunSyncDirs.length > 0) output.syncDirs = dryRunSyncDirs;
21492
19761
  if (dryRunDependencyBootstrap?.targets.length)
21493
19762
  output.dependencyBootstrap = dryRunDependencyBootstrap;
21494
19763
  options.stdout(`${JSON.stringify(output, null, 2)}
@@ -21496,8 +19765,7 @@ function createPrCommand(dependencies = {}) {
21496
19765
  } else {
21497
19766
  options.stdout(
21498
19767
  `Would create worktree at ${worktreePath} (branch: ${branchName})
21499
- ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})
21500
- `).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
19768
+ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21501
19769
  );
21502
19770
  }
21503
19771
  return 0;
@@ -21524,57 +19792,31 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21524
19792
  }
21525
19793
  return 1;
21526
19794
  }
21527
- await mkdir8(dirname11(worktreePath), { recursive: true });
19795
+ await mkdir7(dirname10(worktreePath), { recursive: true });
21528
19796
  const branchAlreadyExists = await localBranchExists(
21529
19797
  repository.repoRoot,
21530
19798
  branchName
21531
19799
  );
21532
19800
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
21533
- await execFileAsync7("git", worktreeArgs, { cwd: repository.repoRoot });
21534
- if (!dependencyBootstrapExplicit) {
21535
- dependencyPolicy = await resolveDependencyBootstrapPolicy(
21536
- {
21537
- currentRoot: repository.currentRoot,
21538
- detectionRoot: worktreePath,
21539
- repoRoot: repository.repoRoot,
21540
- worktreePath
21541
- },
21542
- config,
21543
- false,
21544
- {
21545
- dependencies,
21546
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
21547
- nonInteractive: !!options.json,
21548
- stderr: options.stderr
21549
- }
21550
- );
21551
- config = {
21552
- ...config,
21553
- dependencyBootstrap: dependencyPolicy.mode
21554
- };
21555
- }
19801
+ await execFileAsync6("git", worktreeArgs, { cwd: repository.repoRoot });
21556
19802
  const bootstrap = await bootstrapWorktree({
21557
19803
  branch: branchName,
21558
- cloneDirectory: dependencies.cloneDir,
21559
19804
  config,
21560
19805
  currentRoot: repository.currentRoot,
21561
19806
  dependencyDetectionRoot: worktreePath,
21562
- nonInteractive: !!options.json,
19807
+ dependencyMode,
21563
19808
  repoRoot: repository.repoRoot,
21564
19809
  reporter: createBootstrapReporter(options.stderr, !!options.json),
21565
- runCommand: dependencies.runInstallCommand,
19810
+ runCommand: dependencies.runCommand,
21566
19811
  commandStdout: options.json ? () => void 0 : options.stdout,
21567
19812
  commandStderr: options.json ? () => void 0 : options.stderr,
21568
19813
  json: options.json,
21569
- worktreePath,
21570
- installDependencies: dependencies,
21571
- dependencyBootstrapPolicy: dependencyPolicy
19814
+ worktreePath
21572
19815
  });
21573
19816
  if (!bootstrap.ready) {
21574
19817
  const details = {
21575
19818
  dependencyBootstrap: bootstrap.dependencyBootstrap,
21576
19819
  path: worktreePath,
21577
- skipped: bootstrap.skippedDirs,
21578
19820
  syncFiles: bootstrap.syncFileFailures
21579
19821
  };
21580
19822
  if (options.json) {
@@ -21602,15 +19844,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21602
19844
  branchName
21603
19845
  )
21604
19846
  };
21605
- if (bootstrap.clonedDirs.length > 0) {
21606
- output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
21607
- dir,
21608
- ms
21609
- }));
21610
- }
21611
- if (bootstrap.skippedDirs.length > 0)
21612
- output.skipped = bootstrap.skippedDirs;
21613
- if (bootstrap.dependencyBootstrap.mode !== "off")
19847
+ if (bootstrap.dependencyBootstrap.events.length > 0)
21614
19848
  output.dependencyBootstrap = bootstrap.dependencyBootstrap;
21615
19849
  options.stdout(`${JSON.stringify(output, null, 2)}
21616
19850
  `);
@@ -21623,7 +19857,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21623
19857
  }
21624
19858
  async function localBranchExists(repoRoot, branchName) {
21625
19859
  try {
21626
- await execFileAsync7(
19860
+ await execFileAsync6(
21627
19861
  "git",
21628
19862
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
21629
19863
  { cwd: repoRoot }
@@ -21637,7 +19871,7 @@ var runPrCommand = createPrCommand();
21637
19871
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
21638
19872
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
21639
19873
  try {
21640
- await execFileAsync7(
19874
+ await execFileAsync6(
21641
19875
  "git",
21642
19876
  ["fetch", "origin", `${sourceRef}:${remoteRef}`],
21643
19877
  { cwd: repoRoot }
@@ -21940,15 +20174,14 @@ function normalizeRemoteUrl(value) {
21940
20174
  }
21941
20175
 
21942
20176
  // src/new.ts
21943
- import { execFile as execFile8 } from "node:child_process";
21944
- import { access as access9, mkdir as mkdir9 } from "node:fs/promises";
21945
- import { dirname as dirname12, resolve as resolve12 } from "node:path";
21946
- import { promisify as promisify9 } from "node:util";
21947
- var execFileAsync8 = promisify9(execFile8);
20177
+ import { execFile as execFile7 } from "node:child_process";
20178
+ import { access as access8, mkdir as mkdir8 } from "node:fs/promises";
20179
+ import { dirname as dirname11, resolve as resolve11 } from "node:path";
20180
+ import { promisify as promisify8 } from "node:util";
20181
+ var execFileAsync7 = promisify8(execFile7);
21948
20182
  var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
21949
20183
  function createNewCommand(dependencies = {}) {
21950
20184
  const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
21951
- const cloneDirectory = dependencies.cloneDir ?? cloneDir;
21952
20185
  const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
21953
20186
  const promptForFetchFailure = dependencies.promptForFetchFailure ?? defaultPromptForFetchFailure;
21954
20187
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
@@ -21969,15 +20202,12 @@ function createNewCommand(dependencies = {}) {
21969
20202
  }
21970
20203
  const repository = await detectRepository(options.cwd);
21971
20204
  let config;
21972
- let dependencyBootstrapExplicit;
21973
20205
  try {
21974
- const loaded = await loadEffectiveConfigResult(
20206
+ config = await loadEffectiveConfig(
21975
20207
  repository.repoRoot,
21976
20208
  void 0,
21977
20209
  options.json ? void 0 : options.stderr
21978
20210
  );
21979
- config = loaded.config;
21980
- dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
21981
20211
  } catch (error) {
21982
20212
  return emitNewError(
21983
20213
  options,
@@ -22042,7 +20272,7 @@ function createNewCommand(dependencies = {}) {
22042
20272
  if (options.force) {
22043
20273
  if (!options.dryRun) {
22044
20274
  try {
22045
- await execFileAsync8(
20275
+ await execFileAsync7(
22046
20276
  "git",
22047
20277
  ["worktree", "remove", "--force", worktreePath],
22048
20278
  { cwd: repository.repoRoot }
@@ -22063,7 +20293,7 @@ function createNewCommand(dependencies = {}) {
22063
20293
  }
22064
20294
  if (!options.detached) {
22065
20295
  try {
22066
- await execFileAsync8("git", ["branch", "-D", worktreeName], {
20296
+ await execFileAsync7("git", ["branch", "-D", worktreeName], {
22067
20297
  cwd: repository.repoRoot
22068
20298
  });
22069
20299
  } catch {
@@ -22104,26 +20334,10 @@ function createNewCommand(dependencies = {}) {
22104
20334
  return 1;
22105
20335
  }
22106
20336
  }
22107
- const dependencyPolicy = await resolveDependencyBootstrapPolicy(
22108
- {
22109
- currentRoot: repository.currentRoot,
22110
- repoRoot: repository.repoRoot,
22111
- worktreePath
22112
- },
22113
- config,
22114
- dependencyBootstrapExplicit,
22115
- {
22116
- dependencies,
22117
- dryRun: options.dryRun,
22118
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
22119
- nonInteractive: !!options.json,
22120
- stderr: options.stderr
22121
- }
20337
+ const dependencyMode = resolveDependencyBootstrapMode(
20338
+ config.dependencyBootstrap,
20339
+ options.noInstall
22122
20340
  );
22123
- config = {
22124
- ...config,
22125
- dependencyBootstrap: dependencyPolicy.mode
22126
- };
22127
20341
  if (options.dryRun) {
22128
20342
  if (options.take) {
22129
20343
  const changedFiles = await listTakeFiles(options.cwd);
@@ -22154,13 +20368,8 @@ ${transferableFiles.map((file) => ` ${file}
22154
20368
  }
22155
20369
  return 0;
22156
20370
  }
22157
- const dryRunSyncDirs = await estimateSyncDirectories(
22158
- repository.repoRoot,
22159
- worktreePath,
22160
- config.syncDirs ?? []
22161
- );
22162
20371
  const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(
22163
- config.dependencyBootstrap ?? "off",
20372
+ dependencyMode,
22164
20373
  {
22165
20374
  currentRoot: repository.currentRoot,
22166
20375
  repoRoot: repository.repoRoot,
@@ -22180,7 +20389,6 @@ ${transferableFiles.map((file) => ` ${file}
22180
20389
  ),
22181
20390
  dryRun: true
22182
20391
  };
22183
- if (dryRunSyncDirs.length > 0) output.syncDirs = dryRunSyncDirs;
22184
20392
  if (dryRunDependencyBootstrap.targets.length > 0)
22185
20393
  output.dependencyBootstrap = dryRunDependencyBootstrap;
22186
20394
  options.stdout(`${JSON.stringify(output, null, 2)}
@@ -22190,13 +20398,12 @@ ${transferableFiles.map((file) => ` ${file}
22190
20398
  const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
22191
20399
  options.stdout(
22192
20400
  `Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
22193
- ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})
22194
- `).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
20401
+ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
22195
20402
  );
22196
20403
  }
22197
20404
  return 0;
22198
20405
  }
22199
- await mkdir9(dirname12(worktreePath), { recursive: true });
20406
+ await mkdir8(dirname11(worktreePath), { recursive: true });
22200
20407
  const startPoint = options.fromCurrent && !options.detached ? await resolveCurrentWorktreeHead(options.cwd) : void 0;
22201
20408
  const takeStartPoint = options.take ? await resolveCurrentWorktreeHead(options.cwd) : void 0;
22202
20409
  const changedTakeFiles = options.take ? await listTakeFiles(options.cwd) : [];
@@ -22267,7 +20474,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22267
20474
  ...takeStartPoint ? [takeStartPoint] : startPoint ? [startPoint] : freshBaseRef ? [freshBaseRef] : []
22268
20475
  ];
22269
20476
  try {
22270
- await execFileAsync8("git", gitArgs, { cwd: repository.repoRoot });
20477
+ await execFileAsync7("git", gitArgs, { cwd: repository.repoRoot });
22271
20478
  } catch (error) {
22272
20479
  if (stashSha)
22273
20480
  await restoreTakeStash(options.cwd, stashSha, options.stderr);
@@ -22284,7 +20491,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22284
20491
  options.stderr
22285
20492
  )) {
22286
20493
  try {
22287
- await execFileAsync8(
20494
+ await execFileAsync7(
22288
20495
  "git",
22289
20496
  ["worktree", "remove", "--force", worktreePath],
22290
20497
  { cwd: repository.repoRoot }
@@ -22308,25 +20515,22 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22308
20515
  }
22309
20516
  const bootstrap = await bootstrapWorktree({
22310
20517
  branch: worktreeName,
22311
- cloneDirectory,
22312
20518
  config,
22313
20519
  currentRoot: repository.currentRoot,
22314
- nonInteractive: !!options.json,
20520
+ dependencyDetectionRoot: worktreePath,
20521
+ dependencyMode,
22315
20522
  repoRoot: repository.repoRoot,
22316
20523
  reporter: createBootstrapReporter(options.stderr, !!options.json),
22317
- runCommand: dependencies.runInstallCommand,
20524
+ runCommand: dependencies.runCommand,
22318
20525
  commandStdout: options.json ? () => void 0 : options.stdout,
22319
20526
  commandStderr: options.json ? () => void 0 : options.stderr,
22320
20527
  json: options.json,
22321
- worktreePath,
22322
- installDependencies: dependencies,
22323
- dependencyBootstrapPolicy: dependencyPolicy
20528
+ worktreePath
22324
20529
  });
22325
20530
  if (!bootstrap.ready) {
22326
20531
  return emitNewError(options, "worktree bootstrap failed", {
22327
20532
  dependencyBootstrap: bootstrap.dependencyBootstrap,
22328
20533
  path: worktreePath,
22329
- skipped: bootstrap.skippedDirs,
22330
20534
  syncFiles: bootstrap.syncFileFailures
22331
20535
  });
22332
20536
  }
@@ -22344,15 +20548,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22344
20548
  submodules: submoduleFiles
22345
20549
  }
22346
20550
  } : { ...navigation };
22347
- if (bootstrap.clonedDirs.length > 0) {
22348
- output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
22349
- dir,
22350
- ms
22351
- }));
22352
- }
22353
- if (bootstrap.skippedDirs.length > 0)
22354
- output.skipped = bootstrap.skippedDirs;
22355
- if (bootstrap.dependencyBootstrap.mode !== "off")
20551
+ if (bootstrap.dependencyBootstrap.events.length > 0)
22356
20552
  output.dependencyBootstrap = bootstrap.dependencyBootstrap;
22357
20553
  options.stdout(`${JSON.stringify(output, null, 2)}
22358
20554
  `);
@@ -22581,7 +20777,7 @@ async function defaultPromptForBranch(placeholder) {
22581
20777
  }
22582
20778
  async function localBranchExists2(repoRoot, branchName) {
22583
20779
  try {
22584
- await execFileAsync8(
20780
+ await execFileAsync7(
22585
20781
  "git",
22586
20782
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
22587
20783
  { cwd: repoRoot }
@@ -22592,18 +20788,18 @@ async function localBranchExists2(repoRoot, branchName) {
22592
20788
  }
22593
20789
  }
22594
20790
  async function resolveCurrentWorktreeHead(cwd) {
22595
- return execFileAsync8("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(
20791
+ return execFileAsync7("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(
22596
20792
  ({ stdout: stdout3 }) => stdout3.trim()
22597
20793
  );
22598
20794
  }
22599
20795
  async function createTakeStash(cwd, worktreeName) {
22600
20796
  const message = `gji-take: ${worktreeName}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22601
- await execFileAsync8(
20797
+ await execFileAsync7(
22602
20798
  "git",
22603
20799
  ["stash", "push", "--include-untracked", "-m", message],
22604
20800
  { cwd }
22605
20801
  );
22606
- const { stdout: stdout3 } = await execFileAsync8(
20802
+ const { stdout: stdout3 } = await execFileAsync7(
22607
20803
  "git",
22608
20804
  ["stash", "list", "--format=%H%x00%s"],
22609
20805
  { cwd }
@@ -22624,12 +20820,12 @@ async function detectInProgressGitState(cwd) {
22624
20820
  ];
22625
20821
  for (const [marker, name] of markers) {
22626
20822
  try {
22627
- const { stdout: stdout3 } = await execFileAsync8(
20823
+ const { stdout: stdout3 } = await execFileAsync7(
22628
20824
  "git",
22629
20825
  ["rev-parse", "--git-path", marker],
22630
20826
  { cwd }
22631
20827
  );
22632
- await access9(resolve12(cwd, stdout3.trim()));
20828
+ await access8(resolve11(cwd, stdout3.trim()));
22633
20829
  return name;
22634
20830
  } catch {
22635
20831
  }
@@ -22676,7 +20872,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
22676
20872
  }
22677
20873
  async function listTakeFiles(cwd) {
22678
20874
  try {
22679
- const { stdout: stdout3 } = await execFileAsync8(
20875
+ const { stdout: stdout3 } = await execFileAsync7(
22680
20876
  "git",
22681
20877
  ["status", "--porcelain=v1"],
22682
20878
  { cwd }
@@ -22688,7 +20884,7 @@ async function listTakeFiles(cwd) {
22688
20884
  }
22689
20885
  async function listIgnoredFiles(cwd) {
22690
20886
  try {
22691
- const { stdout: stdout3 } = await execFileAsync8(
20887
+ const { stdout: stdout3 } = await execFileAsync7(
22692
20888
  "git",
22693
20889
  ["status", "--porcelain=v1", "--ignored", "--untracked-files=all"],
22694
20890
  { cwd }
@@ -22701,7 +20897,7 @@ async function listIgnoredFiles(cwd) {
22701
20897
  async function listSubmoduleFiles(cwd, changedFiles) {
22702
20898
  if (changedFiles.length === 0) return [];
22703
20899
  try {
22704
- const { stdout: stdout3 } = await execFileAsync8("git", ["ls-files", "--stage"], {
20900
+ const { stdout: stdout3 } = await execFileAsync7("git", ["ls-files", "--stage"], {
22705
20901
  cwd
22706
20902
  });
22707
20903
  const submodules = new Set(
@@ -22714,7 +20910,7 @@ async function listSubmoduleFiles(cwd, changedFiles) {
22714
20910
  }
22715
20911
  async function countUntrackedFiles(cwd) {
22716
20912
  try {
22717
- const { stdout: stdout3 } = await execFileAsync8(
20913
+ const { stdout: stdout3 } = await execFileAsync7(
22718
20914
  "git",
22719
20915
  ["status", "--porcelain=v1", "--untracked-files=all"],
22720
20916
  { cwd }
@@ -22726,7 +20922,7 @@ async function countUntrackedFiles(cwd) {
22726
20922
  }
22727
20923
  async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr) {
22728
20924
  try {
22729
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20925
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22730
20926
  cwd: worktreePath
22731
20927
  });
22732
20928
  } catch (error) {
@@ -22737,7 +20933,7 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22737
20933
  }
22738
20934
  if (copy) {
22739
20935
  try {
22740
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20936
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22741
20937
  cwd: sourcePath
22742
20938
  });
22743
20939
  } catch (error) {
@@ -22749,7 +20945,7 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22749
20945
  }
22750
20946
  }
22751
20947
  try {
22752
- const { stdout: stdout3 } = await execFileAsync8(
20948
+ const { stdout: stdout3 } = await execFileAsync7(
22753
20949
  "git",
22754
20950
  ["stash", "list", "--format=%H %gd"],
22755
20951
  { cwd: sourcePath }
@@ -22757,14 +20953,14 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22757
20953
  const line = stdout3.split("\n").find((candidate) => candidate.startsWith(stashSha));
22758
20954
  const ref = line?.trim().split(/\s+/).at(1);
22759
20955
  if (ref)
22760
- await execFileAsync8("git", ["stash", "drop", ref], { cwd: sourcePath });
20956
+ await execFileAsync7("git", ["stash", "drop", ref], { cwd: sourcePath });
22761
20957
  } catch {
22762
20958
  }
22763
20959
  return true;
22764
20960
  }
22765
20961
  async function restoreTakeStash(sourcePath, stashSha, stderr) {
22766
20962
  try {
22767
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20963
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22768
20964
  cwd: sourcePath
22769
20965
  });
22770
20966
  } catch {
@@ -23043,7 +21239,7 @@ async function navigateToExistingWorktree(options, path9, worktree) {
23043
21239
  {
23044
21240
  branch: worktree?.branch ?? void 0,
23045
21241
  path: path9,
23046
- repo: basename10(repository.repoRoot),
21242
+ repo: basename9(repository.repoRoot),
23047
21243
  slot: await getWorktreeSlot(path9)
23048
21244
  },
23049
21245
  options.stderr
@@ -23181,7 +21377,7 @@ async function runHistoryCommand(options) {
23181
21377
  }
23182
21378
 
23183
21379
  // src/hub.ts
23184
- import { basename as basename11 } from "node:path";
21380
+ import { basename as basename10 } from "node:path";
23185
21381
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
23186
21382
  var MAX_HUB_REPOSITORY_CONCURRENCY = 4;
23187
21383
  var defaultDependencies = {
@@ -23204,7 +21400,7 @@ async function buildHubData(cwd, dependencies = {}) {
23204
21400
  existing.sources.push(source);
23205
21401
  } else {
23206
21402
  groups.set(repoRoot, {
23207
- name: source.repoName || basename11(repoRoot),
21403
+ name: source.repoName || basename10(repoRoot),
23208
21404
  sources: [source]
23209
21405
  });
23210
21406
  }
@@ -23466,10 +21662,10 @@ async function mapWithConcurrency4(items, limit, mapper) {
23466
21662
  }
23467
21663
 
23468
21664
  // src/init.ts
23469
- import { constants as constants9 } from "node:fs";
23470
- import { access as access10, mkdir as mkdir10, readFile as readFile10, writeFile as writeFile9 } from "node:fs/promises";
21665
+ import { constants as constants8 } from "node:fs";
21666
+ import { access as access9, mkdir as mkdir9, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
23471
21667
  import { homedir as homedir8 } from "node:os";
23472
- import { dirname as dirname13 } from "node:path";
21668
+ import { dirname as dirname12 } from "node:path";
23473
21669
  var ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
23474
21670
  var SHELL_WRAPPED_COMMANDS = [
23475
21671
  {
@@ -23593,8 +21789,8 @@ async function runDefaultOnboarding(context) {
23593
21789
  const installCompletion = await promptForCompletion(shell, completionPath);
23594
21790
  if (installCompletion === null) return abortDefaultOnboarding();
23595
21791
  if (installCompletion) {
23596
- await mkdir10(dirname13(completionPath), { recursive: true });
23597
- await writeFile9(completionPath, renderShellCompletion(shell), "utf8");
21792
+ await mkdir9(dirname12(completionPath), { recursive: true });
21793
+ await writeFile8(completionPath, renderShellCompletion(shell), "utf8");
23598
21794
  }
23599
21795
  if (shell === "zsh" && (completionAlreadyInstalled || installCompletion)) {
23600
21796
  await ensureZshCompletionPath(rcPath);
@@ -23686,8 +21882,8 @@ async function applyOnboardingResult(result, home) {
23686
21882
  const completionAlreadyInstalled = await fileExists(completionPath);
23687
21883
  await applyShellIntegration(result.shell, result.shellIntegration, home);
23688
21884
  if (result.installCompletion) {
23689
- await mkdir10(dirname13(completionPath), { recursive: true });
23690
- await writeFile9(
21885
+ await mkdir9(dirname12(completionPath), { recursive: true });
21886
+ await writeFile8(
23691
21887
  completionPath,
23692
21888
  renderShellCompletion(result.shell),
23693
21889
  "utf8"
@@ -23702,17 +21898,17 @@ async function applyOnboardingResult(result, home) {
23702
21898
  }
23703
21899
  async function installOnboardingShellIntegration(shell, home) {
23704
21900
  const rcPath = resolveShellConfigPath(shell, home);
23705
- await mkdir10(dirname13(rcPath), { recursive: true });
21901
+ await mkdir9(dirname12(rcPath), { recursive: true });
23706
21902
  const current = await readExistingConfig(rcPath);
23707
21903
  if (shell === "fish") {
23708
- await writeFile9(
21904
+ await writeFile8(
23709
21905
  rcPath,
23710
21906
  upsertShellIntegration(current, renderShellIntegration(shell)),
23711
21907
  "utf8"
23712
21908
  );
23713
21909
  return;
23714
21910
  }
23715
- await writeFile9(
21911
+ await writeFile8(
23716
21912
  rcPath,
23717
21913
  upsertOnboardingShellIntegration(current, shell),
23718
21914
  "utf8"
@@ -23746,7 +21942,7 @@ async function ensureZshCompletionPath(rcPath) {
23746
21942
  const current = await readExistingConfig(rcPath);
23747
21943
  const next = upsertZshCompletionPath(current);
23748
21944
  if (next !== current) {
23749
- await writeFile9(rcPath, next, "utf8");
21945
+ await writeFile8(rcPath, next, "utf8");
23750
21946
  }
23751
21947
  }
23752
21948
  function upsertZshCompletionPath(existingConfig) {
@@ -23769,7 +21965,7 @@ ${suffix}`
23769
21965
  }
23770
21966
  async function fileExists(path9) {
23771
21967
  try {
23772
- await access10(path9, constants9.F_OK);
21968
+ await access9(path9, constants8.F_OK);
23773
21969
  return true;
23774
21970
  } catch {
23775
21971
  return false;
@@ -23790,10 +21986,10 @@ async function runLegacyInitCommand(options) {
23790
21986
  return 0;
23791
21987
  }
23792
21988
  const rcPath = resolveShellConfigPath(shell, home);
23793
- await mkdir10(dirname13(rcPath), { recursive: true });
21989
+ await mkdir9(dirname12(rcPath), { recursive: true });
23794
21990
  const current = await readExistingConfig(rcPath);
23795
21991
  const next = upsertShellIntegration(current, script);
23796
- await writeFile9(rcPath, next, "utf8");
21992
+ await writeFile8(rcPath, next, "utf8");
23797
21993
  options.stdout(`${rcPath}
23798
21994
  `);
23799
21995
  const { config: globalConfig } = await loadGlobalConfig(home);
@@ -23884,7 +22080,7 @@ async function saveWizardConfig(result, cwd, home) {
23884
22080
  }
23885
22081
  async function readExistingConfig(path9) {
23886
22082
  try {
23887
- return await readFile10(path9, "utf8");
22083
+ return await readFile9(path9, "utf8");
23888
22084
  } catch (error) {
23889
22085
  if (isMissingFileError3(error)) {
23890
22086
  return "";
@@ -24133,11 +22329,11 @@ function sortWorktrees(worktrees) {
24133
22329
  }
24134
22330
 
24135
22331
  // src/open.ts
24136
- import { execFile as execFile9 } from "node:child_process";
24137
- import { access as access11, writeFile as writeFile10 } from "node:fs/promises";
24138
- import { join as join16 } from "node:path";
24139
- import { promisify as promisify10 } from "node:util";
24140
- var execFileAsync9 = promisify10(execFile9);
22332
+ import { execFile as execFile8 } from "node:child_process";
22333
+ import { access as access10, writeFile as writeFile9 } from "node:fs/promises";
22334
+ import { join as join14 } from "node:path";
22335
+ import { promisify as promisify9 } from "node:util";
22336
+ var execFileAsync8 = promisify9(execFile8);
24141
22337
  function createOpenCommand(dependencies = {}) {
24142
22338
  const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
24143
22339
  const promptForEditor2 = dependencies.promptForEditor ?? defaultPromptForEditor;
@@ -24284,7 +22480,7 @@ async function detectInstalledEditors() {
24284
22480
  }
24285
22481
  async function isCommandAvailable(command) {
24286
22482
  try {
24287
- await execFileAsync9("which", [command]);
22483
+ await execFileAsync8("which", [command]);
24288
22484
  return true;
24289
22485
  } catch {
24290
22486
  return false;
@@ -24302,14 +22498,14 @@ async function defaultPromptForEditor(editors) {
24302
22498
  return choice;
24303
22499
  }
24304
22500
  async function ensureWorkspaceFile(worktreePath, repoName) {
24305
- const workspacePath = join16(worktreePath, `${repoName}.code-workspace`);
22501
+ const workspacePath = join14(worktreePath, `${repoName}.code-workspace`);
24306
22502
  try {
24307
- await access11(workspacePath);
22503
+ await access10(workspacePath);
24308
22504
  return workspacePath;
24309
22505
  } catch {
24310
22506
  }
24311
22507
  const workspace = { folders: [{ path: "." }], settings: {} };
24312
- await writeFile10(
22508
+ await writeFile9(
24313
22509
  workspacePath,
24314
22510
  `${JSON.stringify(workspace, null, 2)}
24315
22511
  `,
@@ -24319,10 +22515,10 @@ async function ensureWorkspaceFile(worktreePath, repoName) {
24319
22515
  }
24320
22516
 
24321
22517
  // src/browser.ts
24322
- import { execFile as execFile10 } from "node:child_process";
22518
+ import { execFile as execFile9 } from "node:child_process";
24323
22519
  import { platform as platform2 } from "node:process";
24324
- import { promisify as promisify11 } from "node:util";
24325
- var execFileAsync10 = promisify11(execFile10);
22520
+ import { promisify as promisify10 } from "node:util";
22521
+ var execFileAsync9 = promisify10(execFile9);
24326
22522
  function createBrowserOpener(dependencies = {}) {
24327
22523
  const currentPlatform = dependencies.platform ?? platform2;
24328
22524
  const runCommand2 = dependencies.runCommand ?? defaultRunCommand2;
@@ -24352,7 +22548,7 @@ function validateBrowserUrl(url) {
24352
22548
  }
24353
22549
  }
24354
22550
  async function defaultRunCommand2(command, args) {
24355
- await execFileAsync10(command, args, { timeout: 1e4 });
22551
+ await execFileAsync9(command, args, { timeout: 1e4 });
24356
22552
  }
24357
22553
 
24358
22554
  // src/pr-open.ts
@@ -24717,7 +22913,7 @@ function formatError(error) {
24717
22913
  }
24718
22914
 
24719
22915
  // src/remove.ts
24720
- import { basename as basename12 } from "node:path";
22916
+ import { basename as basename11 } from "node:path";
24721
22917
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
24722
22918
  function createRemoveCommand(dependencies = {}) {
24723
22919
  const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree3;
@@ -24823,7 +23019,7 @@ function createRemoveCommand(dependencies = {}) {
24823
23019
  {
24824
23020
  branch: worktree.branch ?? void 0,
24825
23021
  path: worktree.path,
24826
- repo: basename12(repository.repoRoot)
23022
+ repo: basename11(repository.repoRoot)
24827
23023
  },
24828
23024
  options.stderr
24829
23025
  );
@@ -25222,7 +23418,7 @@ function resolveConfiguredString2(value) {
25222
23418
 
25223
23419
  // src/sync-files-command.ts
25224
23420
  import { homedir as homedir9 } from "node:os";
25225
- import { join as join17 } from "node:path";
23421
+ import { join as join15 } from "node:path";
25226
23422
  async function runSyncFilesCommand(options) {
25227
23423
  const repository = await detectRepository(options.cwd);
25228
23424
  const home = options.home ?? homedir9();
@@ -25269,9 +23465,9 @@ async function runSyncFilesCommand(options) {
25269
23465
  }
25270
23466
  function findRepoConfigEntry(config, repoRoot, home) {
25271
23467
  const repos = config.repos;
25272
- if (!isPlainObject3(repos)) return null;
23468
+ if (!isPlainObject2(repos)) return null;
25273
23469
  for (const [key, value] of Object.entries(repos)) {
25274
- if (expandTilde3(key, home) === repoRoot && isPlainObject3(value)) {
23470
+ if (expandTilde3(key, home) === repoRoot && isPlainObject2(value)) {
25275
23471
  return { config: value, key };
25276
23472
  }
25277
23473
  }
@@ -25328,8 +23524,8 @@ function removeSyncFiles(existing, removals) {
25328
23524
  return existing.filter((path9) => !removalSet.has(path9));
25329
23525
  }
25330
23526
  async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
25331
- const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
25332
- const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
23527
+ const repos = isPlainObject2(config.repos) ? { ...config.repos } : {};
23528
+ const repoConfig = isPlainObject2(repos[repoKey]) ? repos[repoKey] : {};
25333
23529
  const nextRepoConfig = { ...repoConfig };
25334
23530
  if (syncFiles2.length > 0) {
25335
23531
  nextRepoConfig.syncFiles = syncFiles2;
@@ -25343,7 +23539,7 @@ async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
25343
23539
  }
25344
23540
  await saveGlobalConfig({ ...config, repos }, home);
25345
23541
  }
25346
- function isPlainObject3(value) {
23542
+ function isPlainObject2(value) {
25347
23543
  return typeof value === "object" && value !== null && !Array.isArray(value);
25348
23544
  }
25349
23545
  function writeError(options, message) {
@@ -25357,7 +23553,7 @@ function writeError(options, message) {
25357
23553
  }
25358
23554
  function expandTilde3(value, home) {
25359
23555
  if (value === "~") return home;
25360
- if (value.startsWith("~/")) return join17(home, value.slice(2));
23556
+ if (value.startsWith("~/")) return join15(home, value.slice(2));
25361
23557
  return value;
25362
23558
  }
25363
23559
 
@@ -25622,9 +23818,7 @@ async function maybeRegisterCurrentRepo(cwd) {
25622
23818
  }
25623
23819
  }
25624
23820
  function registerCommands(program2) {
25625
- program2.command("new [branch]").description(
25626
- "create a new branch or detached linked worktree and CoW-bootstrap configured directories"
25627
- ).option(
23821
+ program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
25628
23822
  "-f, --force",
25629
23823
  "remove and recreate the worktree if the target path already exists"
25630
23824
  ).option("--detached", "create a detached worktree without a branch").option(
@@ -25642,6 +23836,9 @@ function registerCommands(program2) {
25642
23836
  ).option(
25643
23837
  "--dry-run",
25644
23838
  "show what would be created without executing any git commands or writing files"
23839
+ ).option(
23840
+ "--no-install",
23841
+ "skip automatic dependency setup in the new worktree"
25645
23842
  ).option(
25646
23843
  "--json",
25647
23844
  "emit JSON on success or error instead of human-readable output"
@@ -25664,6 +23861,9 @@ function registerCommands(program2) {
25664
23861
  ).option(
25665
23862
  "--dry-run",
25666
23863
  "show what would be created without executing any git commands or writing files"
23864
+ ).option(
23865
+ "--no-install",
23866
+ "skip automatic dependency setup in the PR worktree"
25667
23867
  ).option(
25668
23868
  "--json",
25669
23869
  "emit JSON on success or error instead of human-readable output"
@@ -25741,6 +23941,7 @@ function attachCommandActions(program2, options) {
25741
23941
  force: commandOptions2.force,
25742
23942
  noFetch: commandOptions2.fetch === false,
25743
23943
  json: commandOptions2.json,
23944
+ noInstall: commandOptions2.install === false,
25744
23945
  open: commandOptions2.open,
25745
23946
  take: commandOptions2.take,
25746
23947
  task: commandOptions2.task
@@ -25823,6 +24024,7 @@ function attachCommandActions(program2, options) {
25823
24024
  cwd: options.cwd,
25824
24025
  dryRun: commandOptions2.dryRun,
25825
24026
  json: commandOptions2.json,
24027
+ noInstall: commandOptions2.install === false,
25826
24028
  number,
25827
24029
  stderr: options.stderr,
25828
24030
  stdout: options.stdout