@solaqua/gji 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/bootstrap-output.d.ts +3 -2
  2. package/dist/bootstrap-output.js +1 -16
  3. package/dist/bootstrap-preview.d.ts +2 -3
  4. package/dist/bootstrap-preview.js +1 -11
  5. package/dist/cli.js +5 -1
  6. package/dist/config.d.ts +3 -13
  7. package/dist/config.js +12 -59
  8. package/dist/dependency-bootstrap.d.ts +11 -45
  9. package/dist/dependency-bootstrap.js +142 -608
  10. package/dist/gji-bundle.mjs +567 -2367
  11. package/dist/new.d.ts +4 -5
  12. package/dist/new.js +10 -45
  13. package/dist/pr.d.ts +4 -5
  14. package/dist/pr.js +9 -69
  15. package/dist/shell-completion.js +7 -5
  16. package/dist/uv-validation.d.ts +4 -0
  17. package/dist/uv-validation.js +210 -0
  18. package/dist/worktree-bootstrap.d.ts +5 -16
  19. package/dist/worktree-bootstrap.js +1 -24
  20. package/man/man1/gji-back.1 +1 -1
  21. package/man/man1/gji-clean.1 +1 -1
  22. package/man/man1/gji-completion.1 +1 -1
  23. package/man/man1/gji-config.1 +1 -1
  24. package/man/man1/gji-doctor.1 +1 -1
  25. package/man/man1/gji-done.1 +1 -1
  26. package/man/man1/gji-go.1 +1 -1
  27. package/man/man1/gji-history.1 +1 -1
  28. package/man/man1/gji-init.1 +1 -1
  29. package/man/man1/gji-ls.1 +1 -1
  30. package/man/man1/gji-new.1 +8 -5
  31. package/man/man1/gji-open.1 +1 -1
  32. package/man/man1/gji-pr.1 +6 -3
  33. package/man/man1/gji-remove.1 +1 -1
  34. package/man/man1/gji-root.1 +1 -1
  35. package/man/man1/gji-run-hook.1 +1 -1
  36. package/man/man1/gji-status.1 +1 -1
  37. package/man/man1/gji-sync-files.1 +1 -1
  38. package/man/man1/gji-sync.1 +1 -1
  39. package/man/man1/gji-task.1 +1 -1
  40. package/man/man1/gji-undo.1 +1 -1
  41. package/man/man1/gji-warp.1 +1 -1
  42. package/man/man1/gji.1 +2 -2
  43. package/package.json +1 -1
  44. package/dist/dependency-bootstrap-prompt.d.ts +0 -24
  45. package/dist/dependency-bootstrap-prompt.js +0 -103
  46. package/dist/dir-clone.d.ts +0 -32
  47. package/dist/dir-clone.js +0 -641
  48. package/dist/install-prompt.d.ts +0 -12
  49. package/dist/install-prompt.js +0 -127
  50. package/dist/package-manager.d.ts +0 -5
  51. package/dist/package-manager.js +0 -159
  52. package/dist/sync-directories.d.ts +0 -29
  53. package/dist/sync-directories.js +0 -77
  54. package/dist/sync-plan.d.ts +0 -16
  55. package/dist/sync-plan.js +0 -128
@@ -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;
18912
+ }
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]);
19133
18919
  }
19134
- const strategy = cloneStrategy(platform3);
19135
- if (!strategy) {
19136
- throw new CloneUnsupportedError(`platform ${platform3} has no CoW strategy`);
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
  }
19295
- }
19296
- throw new CloneInProgressError(destination);
19297
- }
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
19453
- );
19454
- }
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);
19069
+ const text = contents.toString("utf8");
19070
+ return Buffer.from(text, "utf8").equals(contents) ? { bytes: contents.byteLength, text } : { bytes: contents.byteLength };
19593
19071
  } finally {
19594
- await releaseGuard();
19072
+ await handle.close();
19595
19073
  }
19596
19074
  }
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
- }
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
19084
+ );
19085
+ if (bytesRead === 0) break;
19086
+ offset += bytesRead;
19610
19087
  }
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";
19215
+ function formatInstallFailure(error) {
19216
+ return `install failed: ${toErrorMessage(error)}`;
20023
19217
  }
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;
20031
- }
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,277 +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;
19373
+ await this.afterInstall?.(target, context);
20191
19374
  }
20192
- shouldRetryAfterRepairFailure(context) {
20193
- return context.input === "seed";
20194
- }
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"
20217
- );
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"
19388
+ const packageJson3 = JSON.parse(
19389
+ await readFile8(join12(root, "package.json"), "utf8")
20224
19390
  );
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
- const quotedAssignment = line.match(
20242
- /\bVIRTUAL_ENV(?:\s*=|\s+)\s*(?:"([^"]+)"|'([^']+)')/u
20243
- );
20244
- const quotedPrefix = quotedAssignment?.[1] ?? quotedAssignment?.[2];
20245
- if (quotedPrefix && isAbsolute4(quotedPrefix)) prefixes.push(quotedPrefix);
20246
- const unquoted = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*([^\s"']+)/u)?.[1];
20247
- if (unquoted && isAbsolute4(unquoted)) prefixes.push(unquoted);
20248
- }
20249
- return uniquePaths(prefixes);
20250
- }
20251
- function pythonPrefixFromShebang(text) {
20252
- const command = text.match(/^#!\s*([^\r\n]+)/u)?.[1]?.trim().split(/\s+/u)[0];
20253
- if (!command || !isAbsolute4(command)) return void 0;
20254
- if (!/^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(
20255
- command.split(sep3).at(-1) ?? ""
20256
- )) {
20257
- return void 0;
20258
- }
20259
- return dirname9(dirname9(command));
20260
- }
20261
- function pythonPrefixesFromText(text) {
20262
- const interpreters = [];
20263
- for (const match of text.matchAll(/["'](\/[^"']+)["']/gu)) {
20264
- if (match[1]) interpreters.push(match[1]);
20265
- }
20266
- for (const match of text.matchAll(/(?:^|\s)(\/[^\s"']+)/gmu)) {
20267
- if (match[1]) interpreters.push(match[1]);
20268
- }
20269
- return uniquePaths(
20270
- interpreters.flatMap((interpreter) => {
20271
- const name = interpreter.split(sep3).at(-1) ?? "";
20272
- return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name) ? [dirname9(dirname9(interpreter))] : [];
20273
- })
20274
- );
20275
- }
20276
- async function validateUvRelocation(target, context) {
20277
- if (context.input === "preserve") return;
20278
- const scriptsDirectory = join12(
20279
- target.targetPath,
20280
- process.platform === "win32" ? "Scripts" : "bin"
20281
- );
20282
- const interpreter = join12(
20283
- scriptsDirectory,
20284
- process.platform === "win32" ? "python.exe" : "python"
20285
- );
20286
- const { stdout: stdout3 } = await execFileAsync6(interpreter, [
20287
- "-c",
20288
- "import os, sys; print(os.path.realpath(sys.prefix))"
20289
- ]);
20290
- const [actualPrefix, expectedPrefix] = await Promise.all([
20291
- realpath3(stdout3.trim()),
20292
- realpath3(target.targetPath)
20293
- ]);
20294
- if (actualPrefix !== expectedPrefix) {
20295
- throw new Error(
20296
- `uv environment still points to its source prefix: ${actualPrefix}`
20297
- );
20298
- }
20299
- const sourcePaths = uniquePaths([
20300
- target.sourcePath,
20301
- await realpath3(target.sourcePath).catch(() => void 0)
20302
- ]);
20303
- const entries = await readdir2(scriptsDirectory, { withFileTypes: true });
20304
- assertSafeUvScriptEntries(entries);
20305
- const environmentName = basename7(target.targetPath);
20306
- const acceptedPrefixes = /* @__PURE__ */ new Set([
20307
- resolve9(target.targetPath),
20308
- await realpath3(target.targetPath)
20309
- ]);
20310
- let validatedBytes = 0;
20311
- for (const path9 of [
20312
- join12(target.targetPath, "pyvenv.cfg"),
20313
- ...entries.filter((entry) => entry.isFile()).map((entry) => join12(scriptsDirectory, entry.name))
20314
- ]) {
20315
- const validated = await readBoundedUvTextFile(
20316
- path9,
20317
- UV_VALIDATION_MAX_TOTAL_BYTES - validatedBytes
20318
- );
20319
- if (!validated) continue;
20320
- validatedBytes += validated.bytes;
20321
- if (validated.text === void 0) continue;
20322
- const { text } = validated;
20323
- if (sourcePaths.some((sourcePath) => text.includes(sourcePath))) {
20324
- throw new Error(`uv environment contains a stale source path: ${path9}`);
20325
- }
20326
- const shebangPrefix = pythonPrefixFromShebang(text);
20327
- if (shebangPrefix && basename7(shebangPrefix) === environmentName && !acceptedPrefixes.has(resolve9(shebangPrefix))) {
20328
- throw new Error(`uv launcher points outside its environment: ${path9}`);
20329
- }
20330
- if (pythonPrefixesFromText(text).filter((prefix) => basename7(prefix) === environmentName).some((prefix) => !acceptedPrefixes.has(resolve9(prefix)))) {
20331
- throw new Error(`uv launcher points outside its environment: ${path9}`);
20332
- }
20333
- if (virtualEnvironmentPrefixes(text).some(
20334
- (prefix) => !acceptedPrefixes.has(resolve9(prefix))
20335
- )) {
20336
- throw new Error(
20337
- `uv activation script points outside its environment: ${path9}`
20338
- );
20339
- }
20340
- }
20341
- }
20342
- async function validateUvStructure(target, context) {
20343
- if (context.input === "preserve") return;
20344
- const targetStats = await lstat4(target.targetPath).catch(() => void 0);
20345
- if (!targetStats) return;
20346
- if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
20347
- throw new Error("uv environment must be a real directory");
20348
- }
20349
- const configStats = await lstat4(join12(target.targetPath, "pyvenv.cfg")).catch(
20350
- () => void 0
20351
- );
20352
- if (configStats && (!configStats.isFile() || configStats.isSymbolicLink())) {
20353
- throw new Error("uv pyvenv.cfg must be a regular file");
20354
- }
20355
- const scriptsDirectory = join12(
20356
- target.targetPath,
20357
- process.platform === "win32" ? "Scripts" : "bin"
20358
- );
20359
- const scriptsStats = await lstat4(scriptsDirectory).catch(() => void 0);
20360
- if (!scriptsStats) return;
20361
- if (!scriptsStats.isDirectory() || scriptsStats.isSymbolicLink()) {
20362
- throw new Error("uv scripts path must be a real directory");
20363
- }
20364
- assertSafeUvScriptEntries(
20365
- await readdir2(scriptsDirectory, { withFileTypes: true })
20366
- );
20367
- }
20368
- function assertSafeUvScriptEntries(entries) {
20369
- if (entries.length > UV_VALIDATION_MAX_ENTRIES) {
20370
- throw new Error("uv scripts directory exceeds the validation entry limit");
20371
- }
20372
- for (const entry of entries) {
20373
- if (entry.isSymbolicLink() && !isUvInterpreterName(entry.name)) {
20374
- throw new Error(`uv script must not be a symbolic link: ${entry.name}`);
20375
- }
20376
- }
20377
- }
20378
- async function readBoundedUvTextFile(path9, remainingBytes) {
20379
- let handle;
20380
- try {
20381
- handle = await open3(
20382
- path9,
20383
- constants6.O_RDONLY | (process.platform === "win32" ? 0 : constants6.O_NOFOLLOW)
20384
- );
20385
- } catch (error) {
20386
- if (isNotFoundError(error)) return void 0;
20387
- throw error;
20388
- }
20389
- try {
20390
- const stats = await handle.stat();
20391
- if (!stats.isFile()) return void 0;
20392
- if (stats.size > UV_VALIDATION_MAX_FILE_BYTES) {
20393
- throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
20394
- }
20395
- if (stats.size > remainingBytes) {
20396
- throw new Error("uv launchers exceed the total validation size limit");
20397
- }
20398
- const readLimit = Math.min(UV_VALIDATION_MAX_FILE_BYTES, remainingBytes);
20399
- const contents = await readFilePrefix(handle, readLimit + 1);
20400
- if (contents.byteLength > UV_VALIDATION_MAX_FILE_BYTES) {
20401
- throw new Error(`uv launcher exceeds the validation size limit: ${path9}`);
20402
- }
20403
- if (contents.byteLength > remainingBytes) {
20404
- throw new Error("uv launchers exceed the total validation size limit");
20405
- }
20406
- const text = contents.toString("utf8");
20407
- return Buffer.from(text, "utf8").equals(contents) ? { bytes: contents.byteLength, text } : { bytes: contents.byteLength };
20408
- } finally {
20409
- await handle.close();
20410
- }
20411
- }
20412
- async function readFilePrefix(handle, maxBytes) {
20413
- const contents = Buffer.allocUnsafe(maxBytes);
20414
- let offset = 0;
20415
- while (offset < contents.byteLength) {
20416
- const { bytesRead } = await handle.read(
20417
- contents,
20418
- offset,
20419
- contents.byteLength - offset,
20420
- offset
20421
- );
20422
- if (bytesRead === 0) break;
20423
- offset += bytesRead;
20424
- }
20425
- return contents.subarray(0, offset);
20426
- }
20427
- function isUvInterpreterName(name) {
20428
- return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name);
20429
- }
20430
- async function removeBundlerExtensionMarkers(root) {
20431
- const pending = [root];
20432
- while (pending.length > 0) {
20433
- const current = pending.pop();
20434
- if (!current) continue;
20435
- let entries;
20436
- try {
20437
- entries = await readdir2(current, { withFileTypes: true });
20438
- } catch (error) {
20439
- if (isNotFoundError(error)) continue;
20440
- throw error;
20441
- }
20442
- for (const entry of entries) {
20443
- const path9 = join12(current, entry.name);
20444
- if (entry.isDirectory()) pending.push(path9);
20445
- else if (entry.isFile() && isBundlerExtensionMarker(root, path9)) {
20446
- await rm4(path9, { force: true });
20447
- }
20448
- }
20449
- }
20450
- }
20451
- function isBundlerExtensionMarker(root, path9) {
20452
- const segments = relative3(root, path9).split(sep3);
20453
- return segments.length >= 7 && segments[0] === "ruby" && segments[2] === "extensions" && segments.at(-1) === "gem.build_complete";
20454
- }
20455
- function uniquePaths(paths) {
19398
+ function uniquePaths2(paths) {
20456
19399
  return [...new Set(paths.filter((path9) => Boolean(path9)))];
20457
19400
  }
20458
19401
  async function isAllowedSourceRoot(repoRoot, sourceRoot) {
@@ -20462,7 +19405,7 @@ async function isAllowedSourceRoot(repoRoot, sourceRoot) {
20462
19405
  realpath3(sourceRoot)
20463
19406
  ]);
20464
19407
  if (resolvedRepoRoot === resolvedSourceRoot) return true;
20465
- const gitFile = await readFile9(join12(resolvedSourceRoot, ".git"), "utf8");
19408
+ const gitFile = await readFile8(join12(resolvedSourceRoot, ".git"), "utf8");
20466
19409
  const gitDirectory = gitFile.match(/^gitdir:\s*(.+)$/mu)?.[1];
20467
19410
  if (!gitDirectory) return false;
20468
19411
  return isWithin(
@@ -20474,10 +19417,10 @@ async function isAllowedSourceRoot(repoRoot, sourceRoot) {
20474
19417
  }
20475
19418
  }
20476
19419
  function isWithin(root, candidate) {
20477
- const distance = relative3(root, candidate);
19420
+ const distance = relative2(root, candidate);
20478
19421
  return distance === "" || distance !== ".." && !distance.startsWith(`..${sep3}`);
20479
19422
  }
20480
- function toErrorMessage2(error) {
19423
+ function toErrorMessage(error) {
20481
19424
  return error instanceof Error ? error.message : String(error);
20482
19425
  }
20483
19426
 
@@ -20490,27 +19433,17 @@ async function createDependencyBootstrapPreview(mode, context) {
20490
19433
  function formatDependencyBootstrapPreview(preview) {
20491
19434
  if (!preview) return "";
20492
19435
  return preview.targets.map(
20493
- ({ adapter, target, strategy }) => `Would ${formatBootstrapStrategy(strategy)} ${target} with ${adapter}
19436
+ ({ adapter, target, command }) => `Would install ${target || "dependencies"} with ${adapter}: ${command}
20494
19437
  `
20495
19438
  ).join("");
20496
19439
  }
20497
- function formatBootstrapStrategy(strategy) {
20498
- switch (strategy) {
20499
- case "cow-then-repair":
20500
- return "seed and repair";
20501
- case "repair-only":
20502
- return "repair";
20503
- case "install-only":
20504
- return "install";
20505
- }
20506
- }
20507
19440
 
20508
19441
  // src/conflict.ts
20509
- import { constants as constants7 } from "node:fs";
19442
+ import { constants as constants6 } from "node:fs";
20510
19443
  import { access as access7 } from "node:fs/promises";
20511
19444
  async function pathExists3(path9) {
20512
19445
  try {
20513
- await access7(path9, constants7.F_OK);
19446
+ await access7(path9, constants6.F_OK);
20514
19447
  return true;
20515
19448
  } catch {
20516
19449
  return false;
@@ -20525,257 +19458,41 @@ async function promptForPathConflict(path9) {
20525
19458
  label: "Abort",
20526
19459
  hint: "Keep the existing directory untouched"
20527
19460
  },
20528
- {
20529
- value: "reuse",
20530
- label: "Reuse path",
20531
- hint: "Print the existing path and stop"
20532
- }
20533
- ]
20534
- });
20535
- if (pD(choice)) {
20536
- return "abort";
20537
- }
20538
- return choice;
20539
- }
20540
-
20541
- // src/dependency-bootstrap-prompt.ts
20542
- async function resolveDependencyBootstrapPolicy(context, config, dependencyBootstrapExplicit, options) {
20543
- if (dependencyBootstrapExplicit) {
20544
- return {
20545
- mode: config.dependencyBootstrap ?? "off",
20546
- prompted: false,
20547
- source: "explicit"
20548
- };
20549
- }
20550
- if (options.dryRun || options.nonInteractive || isHeadless()) {
20551
- return { mode: "off", prompted: false, source: "default" };
20552
- }
20553
- if (options.legacyInstallPromptConfigured && !options.dependencies?.promptForDependencyBootstrap) {
20554
- return { mode: "off", prompted: false, source: "legacy" };
20555
- }
20556
- let candidate;
20557
- try {
20558
- candidate = await detectDependencyBootstrapCandidate(context);
20559
- } catch (error) {
20560
- options.stderr(
20561
- `gji: dependency setup detection failed: ${toErrorMessage3(error)}
20562
- `
20563
- );
20564
- return { mode: "off", prompted: false, source: "default" };
20565
- }
20566
- if (!candidate) return { mode: "off", prompted: false, source: "default" };
20567
- const prompt = options.dependencies?.promptForDependencyBootstrap ?? defaultPromptForDependencyBootstrap;
20568
- const choice = await prompt(candidate);
20569
- if (choice === null)
20570
- return { mode: "off", prompted: true, source: "prompted" };
20571
- const mode = choice;
20572
- await persistDependencyBootstrapPolicy(
20573
- context.repoRoot,
20574
- config,
20575
- mode,
20576
- options.dependencies,
20577
- options.stderr
20578
- );
20579
- return { mode, prompted: true, source: "prompted" };
20580
- }
20581
- async function persistDependencyBootstrapPolicy(repoRoot, config, mode, dependencies, stderr) {
20582
- const writeLocal = dependencies?.writeConfigKey ?? (async (root, key, value) => {
20583
- await updateLocalConfigKey(root, key, value);
20584
- });
20585
- const writeGlobal = dependencies?.writeGlobalRepoConfigKey ?? (async (root, key, value) => {
20586
- await updateGlobalRepoConfigKey(root, key, value);
20587
- });
20588
- try {
20589
- if (config.installSaveTarget === "global") {
20590
- await writeGlobal(repoRoot, "dependencyBootstrap", mode);
20591
- } else {
20592
- await writeLocal(repoRoot, "dependencyBootstrap", mode);
20593
- }
20594
- } catch (error) {
20595
- stderr(
20596
- `gji: failed to save dependencyBootstrap: ${toErrorMessage3(error)}
20597
- `
20598
- );
20599
- }
20600
- }
20601
- async function defaultPromptForDependencyBootstrap(candidate) {
20602
- const reuseHint = formatReuseHint(candidate);
20603
- const subject = candidate.kind === "build-cache" ? "dependencies and build state" : "dependencies";
20604
- const choice = await ve({
20605
- message: `Set up ${candidate.adapter} ${subject} for new worktrees? (${candidate.lockfile})`,
20606
- options: [
20607
- {
20608
- value: "cow-then-repair",
20609
- label: "Reuse and repair (recommended)",
20610
- hint: reuseHint
20611
- },
20612
- {
20613
- value: "install-only",
20614
- label: "Install fresh each time",
20615
- hint: candidate.repairCommand
20616
- },
20617
- {
20618
- value: "off",
20619
- label: "Skip dependency setup",
20620
- hint: "leave dependencies to project hooks or manual setup"
20621
- }
20622
- ]
20623
- });
20624
- if (pD(choice)) return null;
20625
- return choice;
20626
- }
20627
- function formatReuseHint(candidate) {
20628
- if (candidate.adapter === "pnpm") {
20629
- return "reuse local node_modules through CoW, then run pnpm install --frozen-lockfile";
20630
- }
20631
- if (candidate.adapter === "npm") {
20632
- return "npm uses a clean install because node_modules is not seeded";
20633
- }
20634
- if (!candidate.seedable) {
20635
- return `repair ${candidate.target} with ${candidate.repairCommand}`;
20636
- }
20637
- return `reuse ${candidate.target} through CoW, then run ${candidate.repairCommand}`;
20638
- }
20639
- function toErrorMessage3(error) {
20640
- return error instanceof Error ? error.message : String(error);
20641
- }
20642
-
20643
- // src/sync-plan.ts
20644
- import { lstat as lstat5, realpath as realpath4 } from "node:fs/promises";
20645
- import { isAbsolute as isAbsolute5, join as join13, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
20646
- async function prepareSyncDirectoryPlan(repoRoot, worktreePath, directories) {
20647
- const normalizedDirectories = directories.map(validateSyncDirPattern).sort(directoryDepthAscending);
20648
- const plan = [];
20649
- for (const directory of normalizedDirectories) {
20650
- const destination = join13(worktreePath, directory);
20651
- const destinationState = await inspectDestination(
20652
- worktreePath,
20653
- destination
20654
- );
20655
- const destinationWasPresent = destinationState.kind === "exists";
20656
- const destinationWarning = destinationState.kind === "unsafe" ? destinationState.reason : void 0;
20657
- try {
20658
- const source = await resolveSyncDirectorySource(repoRoot, directory);
20659
- if (!source) {
20660
- plan.push({
20661
- directory,
20662
- destination,
20663
- worktreePath,
20664
- destinationWasPresent,
20665
- destinationWarning
20666
- });
20667
- } else if ("warning" in source) {
20668
- plan.push({
20669
- directory,
20670
- destination,
20671
- worktreePath,
20672
- destinationWasPresent,
20673
- destinationWarning,
20674
- warning: source.warning
20675
- });
20676
- } else {
20677
- plan.push({
20678
- directory,
20679
- destination,
20680
- worktreePath,
20681
- destinationWasPresent,
20682
- destinationWarning,
20683
- source: source.path
20684
- });
20685
- }
20686
- } catch (error) {
20687
- plan.push({
20688
- directory,
20689
- destination,
20690
- worktreePath,
20691
- destinationWasPresent,
20692
- destinationWarning,
20693
- warning: `could not inspect ${directory}: ${toErrorMessage4(error)}`
20694
- });
20695
- }
20696
- }
20697
- return plan;
20698
- }
20699
- async function estimateSyncDirectoryPlan(plan) {
20700
- const estimates = [];
20701
- for (const entry of plan) {
20702
- if (entry.destinationWasPresent || entry.destinationWarning || !entry.source || entry.warning || isCoveredByCloneAncestor(entry, plan)) {
20703
- continue;
20704
- }
20705
- try {
20706
- estimates.push({
20707
- bytes: await directorySize(entry.source),
20708
- dir: entry.directory
20709
- });
20710
- } catch {
20711
- }
20712
- }
20713
- return estimates;
20714
- }
20715
- async function estimateSyncDirectories(repoRoot, worktreePath, directories) {
20716
- const plan = await prepareSyncDirectoryPlan(
20717
- repoRoot,
20718
- worktreePath,
20719
- directories
20720
- );
20721
- return estimateSyncDirectoryPlan(plan);
20722
- }
20723
- async function resolveSyncDirectorySource(repoRoot, directory) {
20724
- const source = join13(repoRoot, directory);
20725
- let resolvedSource;
20726
- try {
20727
- resolvedSource = await realpath4(source);
20728
- } catch (error) {
20729
- if (isNotFoundError(error)) return null;
20730
- throw error;
20731
- }
20732
- if (!isPathInside2(repoRoot, resolvedSource)) {
20733
- return {
20734
- warning: `source symlink resolves outside the repository (${resolvedSource})`
20735
- };
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";
20736
19470
  }
20737
- const stats = await lstat5(resolvedSource);
20738
- if (!stats.isDirectory()) return { warning: "source is not a directory" };
20739
- return { path: resolvedSource };
20740
- }
20741
- function isCoveredByCloneAncestor(entry, plan) {
20742
- return plan.some(
20743
- (candidate) => candidate !== entry && !candidate.destinationWasPresent && !candidate.destinationWarning && !!candidate.source && isPathInside2(candidate.directory, entry.directory)
20744
- );
20745
- }
20746
- function directoryDepthAscending(left, right) {
20747
- return left.split(/[\\/]+/u).length - right.split(/[\\/]+/u).length;
20748
- }
20749
- function isPathInside2(parent, child) {
20750
- const relativePath = relative4(resolve10(parent), resolve10(child));
20751
- return relativePath !== "" && !isAbsolute5(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep4}`);
20752
- }
20753
- function toErrorMessage4(error) {
20754
- return error instanceof Error ? error.message : String(error);
19471
+ return choice;
20755
19472
  }
20756
19473
 
20757
19474
  // src/worktree-bootstrap.ts
20758
- import { basename as basename9 } from "node:path";
19475
+ import { basename as basename8 } from "node:path";
20759
19476
 
20760
19477
  // src/file-sync.ts
20761
- import { constants as constants8 } from "node:fs";
20762
- 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";
20763
19480
  import {
20764
- basename as basename8,
20765
- dirname as dirname10,
20766
- isAbsolute as isAbsolute6,
20767
- join as join14,
20768
- normalize as normalize2,
20769
- relative as relative5,
20770
- resolve as resolve11,
20771
- 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
20772
19489
  } from "node:path";
20773
19490
  async function syncFiles(mainRoot, targetPath, patterns) {
20774
19491
  for (const pattern of patterns) {
20775
19492
  const normalized = validateSyncFilePattern(pattern);
20776
19493
  const sourcePath = await resolveSyncFileSource(mainRoot, normalized);
20777
19494
  if (!sourcePath) continue;
20778
- const destPath = join14(targetPath, normalized);
19495
+ const destPath = join13(targetPath, normalized);
20779
19496
  const existingDestination = await readDestinationEntry(destPath);
20780
19497
  if (existingDestination?.isSymbolicLink()) {
20781
19498
  throw new Error(`destination is a symbolic link: ${destPath}`);
@@ -20783,7 +19500,7 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20783
19500
  if (existingDestination) {
20784
19501
  continue;
20785
19502
  }
20786
- const destinationParent = dirname10(destPath);
19503
+ const destinationParent = dirname9(destPath);
20787
19504
  const beforeCreate = await inspectDestination(
20788
19505
  targetPath,
20789
19506
  destinationParent
@@ -20796,14 +19513,14 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20796
19513
  destinationParent
20797
19514
  );
20798
19515
  try {
20799
- const safeDestination = join14(safeParent.path, basename8(destPath));
19516
+ const safeDestination = join13(safeParent.path, basename7(destPath));
20800
19517
  const safeExistingDestination = await readDestinationEntry(safeDestination);
20801
19518
  if (safeExistingDestination?.isSymbolicLink()) {
20802
19519
  throw new Error(`destination is a symbolic link: ${destPath}`);
20803
19520
  }
20804
19521
  if (safeExistingDestination) continue;
20805
19522
  try {
20806
- await copyFile(sourcePath, safeDestination, constants8.COPYFILE_EXCL);
19523
+ await copyFile(sourcePath, safeDestination, constants7.COPYFILE_EXCL);
20807
19524
  } catch (error) {
20808
19525
  if (!isAlreadyExistsError(error)) throw error;
20809
19526
  }
@@ -20813,12 +19530,12 @@ async function syncFiles(mainRoot, targetPath, patterns) {
20813
19530
  }
20814
19531
  }
20815
19532
  function validateSyncFilePattern(pattern) {
20816
- if (isAbsolute6(pattern)) {
19533
+ if (isAbsolute4(pattern)) {
20817
19534
  throw new Error(
20818
19535
  `syncFiles: pattern must be a relative path, got: ${pattern}`
20819
19536
  );
20820
19537
  }
20821
- const normalized = normalize2(pattern);
19538
+ const normalized = normalize(pattern);
20822
19539
  if (normalized.startsWith("..")) {
20823
19540
  throw new Error(
20824
19541
  `syncFiles: pattern must not contain '..' segments, got: ${pattern}`
@@ -20831,424 +19548,42 @@ async function resolveSyncFileSource(mainRoot, pattern) {
20831
19548
  let resolvedSource;
20832
19549
  try {
20833
19550
  [resolvedRoot, resolvedSource] = await Promise.all([
20834
- realpath5(mainRoot),
20835
- realpath5(join14(mainRoot, pattern))
19551
+ realpath4(mainRoot),
19552
+ realpath4(join13(mainRoot, pattern))
20836
19553
  ]);
20837
19554
  } catch (error) {
20838
19555
  if (isNotFoundError(error)) return void 0;
20839
19556
  throw error;
20840
19557
  }
20841
- if (!isPathInside3(resolvedRoot, resolvedSource)) {
19558
+ if (!isPathInside2(resolvedRoot, resolvedSource)) {
20842
19559
  throw new Error(
20843
19560
  `syncFiles: source symlink resolves outside the repository: ${resolvedSource}`
20844
19561
  );
20845
19562
  }
20846
- const sourceStats = await lstat6(resolvedSource);
19563
+ const sourceStats = await lstat4(resolvedSource);
20847
19564
  if (!sourceStats.isFile()) {
20848
19565
  throw new Error(
20849
- `syncFiles: source is not a file: ${join14(mainRoot, pattern)}`
19566
+ `syncFiles: source is not a file: ${join13(mainRoot, pattern)}`
20850
19567
  );
20851
19568
  }
20852
19569
  return resolvedSource;
20853
19570
  }
20854
19571
  async function readDestinationEntry(path9) {
20855
19572
  try {
20856
- return await lstat6(path9);
19573
+ return await lstat4(path9);
20857
19574
  } catch (error) {
20858
19575
  if (isNotFoundError(error)) return void 0;
20859
19576
  throw error;
20860
19577
  }
20861
19578
  }
20862
- function isPathInside3(root, candidate) {
20863
- const distance = relative5(resolve11(root), resolve11(candidate));
20864
- return distance === "" || !isAbsolute6(distance) && distance !== ".." && !distance.startsWith(`..${sep5}`);
20865
- }
20866
-
20867
- // src/package-manager.ts
20868
- import { access as access8, readdir as readdir3 } from "node:fs/promises";
20869
- import { join as join15 } from "node:path";
20870
- var ENTRIES = [
20871
- // JavaScript / TypeScript
20872
- { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
20873
- { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
20874
- { name: "bun", signals: ["bun.lockb"], command: "bun install" },
20875
- { name: "npm", signals: ["package-lock.json"], command: "npm install" },
20876
- { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
20877
- // Python
20878
- { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
20879
- { name: "uv", signals: ["uv.lock"], command: "uv sync" },
20880
- { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
20881
- { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
20882
- {
20883
- name: "conda-lock",
20884
- signals: ["conda-lock.yml"],
20885
- command: "conda-lock install"
20886
- },
20887
- {
20888
- name: "conda",
20889
- signals: ["environment.yml"],
20890
- command: "conda env update --file environment.yml"
20891
- },
20892
- // R
20893
- {
20894
- name: "renv",
20895
- signals: ["renv.lock"],
20896
- command: "Rscript -e 'renv::restore()'"
20897
- },
20898
- // Rust
20899
- { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
20900
- // Go
20901
- { name: "go", signals: ["go.sum"], command: "go mod download" },
20902
- // Ruby
20903
- { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
20904
- // PHP
20905
- { name: "composer", signals: ["composer.lock"], command: "composer install" },
20906
- // Elixir / Erlang
20907
- { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
20908
- { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
20909
- // Dart / Flutter
20910
- { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
20911
- // Java / Kotlin / Scala
20912
- { name: "maven", signals: ["pom.xml"], command: "mvn install" },
20913
- { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
20914
- {
20915
- name: "gradle",
20916
- signals: ["build.gradle", "build.gradle.kts"],
20917
- command: "gradle build"
20918
- },
20919
- { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
20920
- // .NET (C# / F# / VB)
20921
- {
20922
- name: "dotnet",
20923
- signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
20924
- command: "dotnet restore",
20925
- glob: true
20926
- },
20927
- // Swift
20928
- {
20929
- name: "swift",
20930
- signals: ["Package.swift"],
20931
- command: "swift package resolve"
20932
- },
20933
- // Haskell
20934
- { name: "stack", signals: ["stack.yaml"], command: "stack build" },
20935
- {
20936
- name: "cabal",
20937
- signals: ["cabal.project"],
20938
- command: "cabal install --only-dependencies"
20939
- },
20940
- {
20941
- name: "cabal",
20942
- signals: ["*.cabal"],
20943
- command: "cabal install --only-dependencies",
20944
- glob: true
20945
- },
20946
- // Clojure
20947
- { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
20948
- { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
20949
- // OCaml
20950
- { name: "dune", signals: ["dune-project"], command: "dune build" },
20951
- // Julia
20952
- {
20953
- name: "julia",
20954
- signals: ["Manifest.toml"],
20955
- command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
20956
- },
20957
- // Nim
20958
- {
20959
- name: "nimble",
20960
- signals: ["*.nimble"],
20961
- command: "nimble install",
20962
- glob: true
20963
- },
20964
- // Crystal
20965
- { name: "shards", signals: ["shard.yml"], command: "shards install" },
20966
- // Perl
20967
- { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
20968
- // Zig
20969
- { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
20970
- // C / C++
20971
- { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
20972
- {
20973
- name: "conan",
20974
- signals: ["conanfile.py", "conanfile.txt"],
20975
- command: "conan install ."
20976
- },
20977
- // Nix
20978
- { name: "nix", signals: ["flake.nix"], command: "nix develop" },
20979
- { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
20980
- // Terraform / OpenTofu
20981
- {
20982
- name: "terraform",
20983
- signals: ["terraform.lock.hcl"],
20984
- command: "terraform init"
20985
- }
20986
- ];
20987
- async function detectPackageManager(repoRoot) {
20988
- for (const entry of ENTRIES) {
20989
- const matched = entry.glob ? await matchesGlob(repoRoot, entry.signals) : await matchesExact(repoRoot, entry.signals);
20990
- if (matched) {
20991
- return { name: entry.name, installCommand: entry.command };
20992
- }
20993
- }
20994
- return null;
20995
- }
20996
- async function matchesExact(repoRoot, signals) {
20997
- for (const signal of signals) {
20998
- try {
20999
- await access8(join15(repoRoot, signal));
21000
- return true;
21001
- } catch {
21002
- }
21003
- }
21004
- return false;
21005
- }
21006
- async function matchesGlob(repoRoot, patterns) {
21007
- let files;
21008
- try {
21009
- files = await readdir3(repoRoot);
21010
- } catch {
21011
- return false;
21012
- }
21013
- const regexes = patterns.map(patternToRegex);
21014
- return files.some((file) => regexes.some((re) => re.test(file)));
21015
- }
21016
- function patternToRegex(pattern) {
21017
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
21018
- return new RegExp(`^${escaped}$`);
21019
- }
21020
-
21021
- // src/install-prompt.ts
21022
- async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
21023
- if (isHeadless() || nonInteractive) {
21024
- return;
21025
- }
21026
- if (extractHooks(config)["after-create"]) {
21027
- return;
21028
- }
21029
- if (config.skipInstallPrompt === true) {
21030
- return;
21031
- }
21032
- const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
21033
- const pm = await detect(worktreePath);
21034
- if (!pm) {
21035
- return;
21036
- }
21037
- const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
21038
- const choice = await prompt(pm);
21039
- if (!choice || choice === "no") {
21040
- return;
21041
- }
21042
- if (choice === "yes" || choice === "always") {
21043
- const runner = dependencies.runInstallCommand ?? runInstallCommand;
21044
- try {
21045
- await runner(pm.installCommand, worktreePath, stderr);
21046
- } catch (error) {
21047
- stderr(
21048
- `gji: install command failed: ${error instanceof Error ? error.message : String(error)}
21049
- `
21050
- );
21051
- }
21052
- }
21053
- const saveGlobal = config.installSaveTarget === "global";
21054
- const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
21055
- const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
21056
- if (choice === "always") {
21057
- try {
21058
- if (saveGlobal) {
21059
- const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
21060
- const existing = extractHooks({ hooks: existingRaw });
21061
- await writeGlobalKey(repoRoot, "hooks", {
21062
- ...existing["after-enter"] !== void 0 && {
21063
- "after-enter": existing["after-enter"]
21064
- },
21065
- ...existing["before-remove"] !== void 0 && {
21066
- "before-remove": existing["before-remove"]
21067
- },
21068
- "after-create": pm.installCommand
21069
- });
21070
- } else {
21071
- const { config: localConfig } = await loadConfig(repoRoot);
21072
- const existing = extractHooks(localConfig);
21073
- await writeKey(repoRoot, "hooks", {
21074
- ...existing["after-enter"] !== void 0 && {
21075
- "after-enter": existing["after-enter"]
21076
- },
21077
- ...existing["before-remove"] !== void 0 && {
21078
- "before-remove": existing["before-remove"]
21079
- },
21080
- "after-create": pm.installCommand
21081
- });
21082
- }
21083
- } catch (error) {
21084
- stderr(
21085
- `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
21086
- `
21087
- );
21088
- }
21089
- }
21090
- if (choice === "never") {
21091
- try {
21092
- if (saveGlobal) {
21093
- await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
21094
- } else {
21095
- await writeKey(repoRoot, "skipInstallPrompt", true);
21096
- }
21097
- } catch (error) {
21098
- stderr(
21099
- `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
21100
- `
21101
- );
21102
- }
21103
- }
21104
- }
21105
- var runInstallCommand = runCommand;
21106
- async function defaultWriteConfigKey(root, key, value) {
21107
- await updateLocalConfigKey(root, key, value);
21108
- }
21109
- async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
21110
- await updateGlobalRepoConfigKey(repoRoot, key, value);
21111
- }
21112
- async function loadExistingGlobalRepoHooks(repoRoot) {
21113
- const { config: globalConfig } = await loadGlobalConfig();
21114
- const repos = isPlainObject2(globalConfig.repos) ? globalConfig.repos : {};
21115
- const perRepo = isPlainObject2(repos[repoRoot]) ? repos[repoRoot] : {};
21116
- return isPlainObject2(perRepo.hooks) ? perRepo.hooks : {};
21117
- }
21118
- async function defaultPromptForInstallChoice(pm) {
21119
- const choice = await ve({
21120
- message: `Run \`${pm.installCommand}\` in the new worktree?`,
21121
- options: [
21122
- { value: "yes", label: "Yes", hint: "run once" },
21123
- { value: "no", label: "No", hint: "skip this time" },
21124
- { value: "always", label: "Always", hint: "save as after-create hook" },
21125
- {
21126
- value: "never",
21127
- label: "Never",
21128
- hint: "disable this prompt for this repo"
21129
- }
21130
- ]
21131
- });
21132
- if (pD(choice)) {
21133
- return null;
21134
- }
21135
- return choice;
21136
- }
21137
- function isPlainObject2(value) {
21138
- return typeof value === "object" && value !== null && !Array.isArray(value);
21139
- }
21140
-
21141
- // src/sync-directories.ts
21142
- async function executeSyncDirectoryPlan(plan, options) {
21143
- const outcomes = [];
21144
- for (const entry of plan) {
21145
- if (entry.destinationWarning) {
21146
- recordSkipped(
21147
- outcomes,
21148
- options.reporter,
21149
- entry.directory,
21150
- entry.destinationWarning
21151
- );
21152
- continue;
21153
- }
21154
- const destinationState = await inspectDestination(
21155
- entry.worktreePath,
21156
- entry.destination
21157
- );
21158
- if (destinationState.kind === "exists") {
21159
- recordSkipped(
21160
- outcomes,
21161
- options.reporter,
21162
- entry.directory,
21163
- "destination already exists"
21164
- );
21165
- continue;
21166
- }
21167
- if (destinationState.kind === "unsafe") {
21168
- const reason = destinationState.reason;
21169
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
21170
- continue;
21171
- }
21172
- if (entry.warning) {
21173
- recordSkipped(outcomes, options.reporter, entry.directory, entry.warning);
21174
- continue;
21175
- }
21176
- if (!entry.source) {
21177
- recordSkipped(
21178
- outcomes,
21179
- options.reporter,
21180
- entry.directory,
21181
- "source does not exist"
21182
- );
21183
- continue;
21184
- }
21185
- const refreshedDestinationState = await inspectDestination(
21186
- entry.worktreePath,
21187
- entry.destination
21188
- );
21189
- if (refreshedDestinationState.kind !== "missing") {
21190
- recordSkipped(
21191
- outcomes,
21192
- options.reporter,
21193
- entry.directory,
21194
- refreshedDestinationState.kind === "unsafe" ? refreshedDestinationState.reason : "destination already exists"
21195
- );
21196
- continue;
21197
- }
21198
- let result;
21199
- try {
21200
- const cloneOptions = {
21201
- destinationRoot: entry.worktreePath,
21202
- measureBytes: options.reporter.measureCloneSize
21203
- };
21204
- result = await options.cloneDirectory(
21205
- entry.source,
21206
- entry.destination,
21207
- cloneOptions
21208
- );
21209
- } catch (error) {
21210
- if (isCloneDestinationExistsError(error)) {
21211
- recordSkipped(
21212
- outcomes,
21213
- options.reporter,
21214
- entry.directory,
21215
- "destination already exists"
21216
- );
21217
- continue;
21218
- }
21219
- if (isCloneInProgressError(error)) {
21220
- const reason2 = "copy-on-write clone already in progress";
21221
- recordSkipped(outcomes, options.reporter, entry.directory, reason2);
21222
- continue;
21223
- }
21224
- const reason = toErrorMessage5(error);
21225
- recordSkipped(outcomes, options.reporter, entry.directory, reason);
21226
- continue;
21227
- }
21228
- const clonedDirectory = {
21229
- bytes: result.bytes,
21230
- dir: entry.directory,
21231
- ms: result.ms
21232
- };
21233
- const outcome = { kind: "cloned", directory: clonedDirectory };
21234
- outcomes.push(outcome);
21235
- options.reporter.cloned(clonedDirectory);
21236
- }
21237
- return outcomes;
21238
- }
21239
- function recordSkipped(outcomes, reporter, dir, reason) {
21240
- outcomes.push({ kind: "skipped", dir, reason });
21241
- if (reporter.skipped) reporter.skipped({ dir, reason });
21242
- else reporter.write(`syncDirs: ${reason}, skipped ${dir}
21243
- `);
21244
- }
21245
- function toErrorMessage5(error) {
21246
- 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}`);
21247
19582
  }
21248
19583
 
21249
19584
  // src/worktree-bootstrap.ts
21250
19585
  async function bootstrapWorktree(options) {
21251
- const dependencyMode = options.config.dependencyBootstrap ?? "off";
19586
+ const dependencyMode = options.dependencyMode;
21252
19587
  const dependencyPlan = await prepareDependencyBootstrap(dependencyMode, {
21253
19588
  currentRoot: options.currentRoot,
21254
19589
  detectionRoot: options.dependencyDetectionRoot,
@@ -21256,27 +19591,12 @@ async function bootstrapWorktree(options) {
21256
19591
  cargoBuildCommand: options.config.dependencyBuildCommand,
21257
19592
  worktreePath: options.worktreePath
21258
19593
  });
21259
- const syncPlan = await prepareSyncDirectoryPlan(
21260
- options.repoRoot,
21261
- options.worktreePath,
21262
- options.config.syncDirs ?? []
21263
- );
21264
- const outcomes = await executeSyncDirectoryPlan(syncPlan, {
21265
- cloneDirectory: options.cloneDirectory ?? cloneDir,
21266
- reporter: options.reporter
21267
- });
21268
- const clonedDirs = outcomes.flatMap(
21269
- (outcome) => outcome.kind === "cloned" ? [outcome.directory] : []
21270
- );
21271
- const skippedDirs = outcomes.flatMap(
21272
- (outcome) => outcome.kind === "skipped" ? [{ dir: outcome.dir, reason: outcome.reason }] : []
21273
- );
21274
19594
  const syncFileFailures = [];
21275
19595
  for (const pattern of options.config.syncFiles ?? []) {
21276
19596
  try {
21277
19597
  await syncFiles(options.repoRoot, options.worktreePath, [pattern]);
21278
19598
  } catch (error) {
21279
- const message = `failed to sync file "${pattern}": ${toErrorMessage6(error)}`;
19599
+ const message = `failed to sync file "${pattern}": ${toErrorMessage2(error)}`;
21280
19600
  if (!options.json) options.reporter.write(`Warning: ${message}
21281
19601
  `);
21282
19602
  syncFileFailures.push({
@@ -21293,34 +19613,18 @@ async function bootstrapWorktree(options) {
21293
19613
  for (const event of syncFileFailures) options.reporter.dependency(event);
21294
19614
  }
21295
19615
  const dependencyBootstrap = syncFileFailures.length > 0 ? { mode: dependencyMode, ready: false, events: [] } : await executeDependencyBootstrap(dependencyPlan, {
21296
- cloneDirectory: options.cloneDirectory,
21297
19616
  reporter: options.reporter,
21298
19617
  stderr: options.commandStderr ?? options.reporter.write,
21299
19618
  stdout: options.commandStdout,
21300
- seededDirectories: clonedDirs.map(({ dir }) => dir),
21301
19619
  runCommand: options.runCommand
21302
19620
  });
21303
19621
  if (!dependencyBootstrap.ready) {
21304
19622
  return {
21305
- clonedDirs,
21306
19623
  dependencyBootstrap,
21307
19624
  ready: false,
21308
- syncFileFailures,
21309
- skippedDirs
19625
+ syncFileFailures
21310
19626
  };
21311
19627
  }
21312
- if (dependencyMode === "off" && ["default", "legacy"].includes(
21313
- options.dependencyBootstrapPolicy?.source ?? "default"
21314
- )) {
21315
- await maybeRunInstallPrompt(
21316
- options.worktreePath,
21317
- options.repoRoot,
21318
- options.config,
21319
- options.reporter.write,
21320
- options.installDependencies,
21321
- options.nonInteractive
21322
- );
21323
- }
21324
19628
  const hooks = extractHooks(options.config);
21325
19629
  const slot = await assignWorktreeSlot(options.repoRoot, options.worktreePath);
21326
19630
  await runHook(
@@ -21329,26 +19633,24 @@ async function bootstrapWorktree(options) {
21329
19633
  {
21330
19634
  branch: options.branch,
21331
19635
  path: options.worktreePath,
21332
- repo: basename9(options.repoRoot),
19636
+ repo: basename8(options.repoRoot),
21333
19637
  slot
21334
19638
  },
21335
19639
  options.reporter.write,
21336
19640
  options.json ? () => void 0 : options.commandStdout ?? ((chunk) => process.stdout.write(chunk))
21337
19641
  );
21338
19642
  return {
21339
- clonedDirs,
21340
19643
  dependencyBootstrap,
21341
19644
  ready: true,
21342
- syncFileFailures: [],
21343
- skippedDirs
19645
+ syncFileFailures: []
21344
19646
  };
21345
19647
  }
21346
- function toErrorMessage6(error) {
19648
+ function toErrorMessage2(error) {
21347
19649
  return error instanceof Error ? error.message : String(error);
21348
19650
  }
21349
19651
 
21350
19652
  // src/pr.ts
21351
- var execFileAsync7 = promisify8(execFile7);
19653
+ var execFileAsync6 = promisify7(execFile6);
21352
19654
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
21353
19655
  function parsePrInput(input) {
21354
19656
  if (/^\d+$/.test(input)) return input;
@@ -21377,15 +19679,12 @@ function createPrCommand(dependencies = {}) {
21377
19679
  }
21378
19680
  const repository = await detectRepository(options.cwd);
21379
19681
  let config;
21380
- let dependencyBootstrapExplicit;
21381
19682
  try {
21382
- const loaded = await loadEffectiveConfigResult(
19683
+ config = await loadEffectiveConfig(
21383
19684
  repository.repoRoot,
21384
19685
  void 0,
21385
19686
  options.json ? void 0 : options.stderr
21386
19687
  );
21387
- config = loaded.config;
21388
- dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
21389
19688
  } catch (error) {
21390
19689
  const message = error instanceof Error ? error.message : String(error);
21391
19690
  if (options.json) {
@@ -21436,47 +19735,16 @@ function createPrCommand(dependencies = {}) {
21436
19735
  );
21437
19736
  return 1;
21438
19737
  }
21439
- let dependencyPolicy = {
21440
- mode: config.dependencyBootstrap ?? "off",
21441
- prompted: false,
21442
- source: dependencyBootstrapExplicit ? "explicit" : "default"
21443
- };
21444
- if (options.dryRun) {
21445
- dependencyPolicy = await resolveDependencyBootstrapPolicy(
21446
- {
21447
- currentRoot: repository.currentRoot,
21448
- repoRoot: repository.repoRoot,
21449
- worktreePath
21450
- },
21451
- config,
21452
- dependencyBootstrapExplicit,
21453
- {
21454
- dependencies,
21455
- dryRun: true,
21456
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
21457
- nonInteractive: !!options.json,
21458
- stderr: options.stderr
21459
- }
21460
- );
21461
- config = {
21462
- ...config,
21463
- dependencyBootstrap: dependencyPolicy.mode
21464
- };
21465
- }
21466
- const dryRunSyncDirs = options.dryRun ? await estimateSyncDirectories(
21467
- repository.repoRoot,
21468
- worktreePath,
21469
- config.syncDirs ?? []
21470
- ) : [];
21471
- const dryRunDependencyBootstrap = options.dryRun ? await createDependencyBootstrapPreview(
21472
- config.dependencyBootstrap ?? "off",
21473
- {
21474
- currentRoot: repository.currentRoot,
21475
- repoRoot: repository.repoRoot,
21476
- cargoBuildCommand: config.dependencyBuildCommand,
21477
- worktreePath
21478
- }
21479
- ) : 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;
21480
19748
  if (options.dryRun) {
21481
19749
  if (options.json) {
21482
19750
  const output = {
@@ -21490,7 +19758,6 @@ function createPrCommand(dependencies = {}) {
21490
19758
  ),
21491
19759
  dryRun: true
21492
19760
  };
21493
- if (dryRunSyncDirs.length > 0) output.syncDirs = dryRunSyncDirs;
21494
19761
  if (dryRunDependencyBootstrap?.targets.length)
21495
19762
  output.dependencyBootstrap = dryRunDependencyBootstrap;
21496
19763
  options.stdout(`${JSON.stringify(output, null, 2)}
@@ -21498,8 +19765,7 @@ function createPrCommand(dependencies = {}) {
21498
19765
  } else {
21499
19766
  options.stdout(
21500
19767
  `Would create worktree at ${worktreePath} (branch: ${branchName})
21501
- ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})
21502
- `).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
19768
+ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21503
19769
  );
21504
19770
  }
21505
19771
  return 0;
@@ -21526,57 +19792,31 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21526
19792
  }
21527
19793
  return 1;
21528
19794
  }
21529
- await mkdir8(dirname11(worktreePath), { recursive: true });
19795
+ await mkdir7(dirname10(worktreePath), { recursive: true });
21530
19796
  const branchAlreadyExists = await localBranchExists(
21531
19797
  repository.repoRoot,
21532
19798
  branchName
21533
19799
  );
21534
19800
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
21535
- await execFileAsync7("git", worktreeArgs, { cwd: repository.repoRoot });
21536
- if (!dependencyBootstrapExplicit) {
21537
- dependencyPolicy = await resolveDependencyBootstrapPolicy(
21538
- {
21539
- currentRoot: repository.currentRoot,
21540
- detectionRoot: worktreePath,
21541
- repoRoot: repository.repoRoot,
21542
- worktreePath
21543
- },
21544
- config,
21545
- false,
21546
- {
21547
- dependencies,
21548
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
21549
- nonInteractive: !!options.json,
21550
- stderr: options.stderr
21551
- }
21552
- );
21553
- config = {
21554
- ...config,
21555
- dependencyBootstrap: dependencyPolicy.mode
21556
- };
21557
- }
19801
+ await execFileAsync6("git", worktreeArgs, { cwd: repository.repoRoot });
21558
19802
  const bootstrap = await bootstrapWorktree({
21559
19803
  branch: branchName,
21560
- cloneDirectory: dependencies.cloneDir,
21561
19804
  config,
21562
19805
  currentRoot: repository.currentRoot,
21563
19806
  dependencyDetectionRoot: worktreePath,
21564
- nonInteractive: !!options.json,
19807
+ dependencyMode,
21565
19808
  repoRoot: repository.repoRoot,
21566
19809
  reporter: createBootstrapReporter(options.stderr, !!options.json),
21567
- runCommand: dependencies.runInstallCommand,
19810
+ runCommand: dependencies.runCommand,
21568
19811
  commandStdout: options.json ? () => void 0 : options.stdout,
21569
19812
  commandStderr: options.json ? () => void 0 : options.stderr,
21570
19813
  json: options.json,
21571
- worktreePath,
21572
- installDependencies: dependencies,
21573
- dependencyBootstrapPolicy: dependencyPolicy
19814
+ worktreePath
21574
19815
  });
21575
19816
  if (!bootstrap.ready) {
21576
19817
  const details = {
21577
19818
  dependencyBootstrap: bootstrap.dependencyBootstrap,
21578
19819
  path: worktreePath,
21579
- skipped: bootstrap.skippedDirs,
21580
19820
  syncFiles: bootstrap.syncFileFailures
21581
19821
  };
21582
19822
  if (options.json) {
@@ -21604,15 +19844,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21604
19844
  branchName
21605
19845
  )
21606
19846
  };
21607
- if (bootstrap.clonedDirs.length > 0) {
21608
- output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
21609
- dir,
21610
- ms
21611
- }));
21612
- }
21613
- if (bootstrap.skippedDirs.length > 0)
21614
- output.skipped = bootstrap.skippedDirs;
21615
- if (bootstrap.dependencyBootstrap.mode !== "off")
19847
+ if (bootstrap.dependencyBootstrap.events.length > 0)
21616
19848
  output.dependencyBootstrap = bootstrap.dependencyBootstrap;
21617
19849
  options.stdout(`${JSON.stringify(output, null, 2)}
21618
19850
  `);
@@ -21625,7 +19857,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
21625
19857
  }
21626
19858
  async function localBranchExists(repoRoot, branchName) {
21627
19859
  try {
21628
- await execFileAsync7(
19860
+ await execFileAsync6(
21629
19861
  "git",
21630
19862
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
21631
19863
  { cwd: repoRoot }
@@ -21639,7 +19871,7 @@ var runPrCommand = createPrCommand();
21639
19871
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
21640
19872
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
21641
19873
  try {
21642
- await execFileAsync7(
19874
+ await execFileAsync6(
21643
19875
  "git",
21644
19876
  ["fetch", "origin", `${sourceRef}:${remoteRef}`],
21645
19877
  { cwd: repoRoot }
@@ -21942,15 +20174,14 @@ function normalizeRemoteUrl(value) {
21942
20174
  }
21943
20175
 
21944
20176
  // src/new.ts
21945
- import { execFile as execFile8 } from "node:child_process";
21946
- import { access as access9, mkdir as mkdir9 } from "node:fs/promises";
21947
- import { dirname as dirname12, resolve as resolve12 } from "node:path";
21948
- import { promisify as promisify9 } from "node:util";
21949
- 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);
21950
20182
  var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
21951
20183
  function createNewCommand(dependencies = {}) {
21952
20184
  const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
21953
- const cloneDirectory = dependencies.cloneDir ?? cloneDir;
21954
20185
  const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
21955
20186
  const promptForFetchFailure = dependencies.promptForFetchFailure ?? defaultPromptForFetchFailure;
21956
20187
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
@@ -21971,15 +20202,12 @@ function createNewCommand(dependencies = {}) {
21971
20202
  }
21972
20203
  const repository = await detectRepository(options.cwd);
21973
20204
  let config;
21974
- let dependencyBootstrapExplicit;
21975
20205
  try {
21976
- const loaded = await loadEffectiveConfigResult(
20206
+ config = await loadEffectiveConfig(
21977
20207
  repository.repoRoot,
21978
20208
  void 0,
21979
20209
  options.json ? void 0 : options.stderr
21980
20210
  );
21981
- config = loaded.config;
21982
- dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
21983
20211
  } catch (error) {
21984
20212
  return emitNewError(
21985
20213
  options,
@@ -22044,7 +20272,7 @@ function createNewCommand(dependencies = {}) {
22044
20272
  if (options.force) {
22045
20273
  if (!options.dryRun) {
22046
20274
  try {
22047
- await execFileAsync8(
20275
+ await execFileAsync7(
22048
20276
  "git",
22049
20277
  ["worktree", "remove", "--force", worktreePath],
22050
20278
  { cwd: repository.repoRoot }
@@ -22065,7 +20293,7 @@ function createNewCommand(dependencies = {}) {
22065
20293
  }
22066
20294
  if (!options.detached) {
22067
20295
  try {
22068
- await execFileAsync8("git", ["branch", "-D", worktreeName], {
20296
+ await execFileAsync7("git", ["branch", "-D", worktreeName], {
22069
20297
  cwd: repository.repoRoot
22070
20298
  });
22071
20299
  } catch {
@@ -22106,26 +20334,10 @@ function createNewCommand(dependencies = {}) {
22106
20334
  return 1;
22107
20335
  }
22108
20336
  }
22109
- const dependencyPolicy = await resolveDependencyBootstrapPolicy(
22110
- {
22111
- currentRoot: repository.currentRoot,
22112
- repoRoot: repository.repoRoot,
22113
- worktreePath
22114
- },
22115
- config,
22116
- dependencyBootstrapExplicit,
22117
- {
22118
- dependencies,
22119
- dryRun: options.dryRun,
22120
- legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== void 0,
22121
- nonInteractive: !!options.json,
22122
- stderr: options.stderr
22123
- }
20337
+ const dependencyMode = resolveDependencyBootstrapMode(
20338
+ config.dependencyBootstrap,
20339
+ options.noInstall
22124
20340
  );
22125
- config = {
22126
- ...config,
22127
- dependencyBootstrap: dependencyPolicy.mode
22128
- };
22129
20341
  if (options.dryRun) {
22130
20342
  if (options.take) {
22131
20343
  const changedFiles = await listTakeFiles(options.cwd);
@@ -22156,13 +20368,8 @@ ${transferableFiles.map((file) => ` ${file}
22156
20368
  }
22157
20369
  return 0;
22158
20370
  }
22159
- const dryRunSyncDirs = await estimateSyncDirectories(
22160
- repository.repoRoot,
22161
- worktreePath,
22162
- config.syncDirs ?? []
22163
- );
22164
20371
  const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(
22165
- config.dependencyBootstrap ?? "off",
20372
+ dependencyMode,
22166
20373
  {
22167
20374
  currentRoot: repository.currentRoot,
22168
20375
  repoRoot: repository.repoRoot,
@@ -22182,7 +20389,6 @@ ${transferableFiles.map((file) => ` ${file}
22182
20389
  ),
22183
20390
  dryRun: true
22184
20391
  };
22185
- if (dryRunSyncDirs.length > 0) output.syncDirs = dryRunSyncDirs;
22186
20392
  if (dryRunDependencyBootstrap.targets.length > 0)
22187
20393
  output.dependencyBootstrap = dryRunDependencyBootstrap;
22188
20394
  options.stdout(`${JSON.stringify(output, null, 2)}
@@ -22192,13 +20398,12 @@ ${transferableFiles.map((file) => ` ${file}
22192
20398
  const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
22193
20399
  options.stdout(
22194
20400
  `Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
22195
- ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(bytes)})
22196
- `).join("")}${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
20401
+ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
22197
20402
  );
22198
20403
  }
22199
20404
  return 0;
22200
20405
  }
22201
- await mkdir9(dirname12(worktreePath), { recursive: true });
20406
+ await mkdir8(dirname11(worktreePath), { recursive: true });
22202
20407
  const startPoint = options.fromCurrent && !options.detached ? await resolveCurrentWorktreeHead(options.cwd) : void 0;
22203
20408
  const takeStartPoint = options.take ? await resolveCurrentWorktreeHead(options.cwd) : void 0;
22204
20409
  const changedTakeFiles = options.take ? await listTakeFiles(options.cwd) : [];
@@ -22269,7 +20474,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22269
20474
  ...takeStartPoint ? [takeStartPoint] : startPoint ? [startPoint] : freshBaseRef ? [freshBaseRef] : []
22270
20475
  ];
22271
20476
  try {
22272
- await execFileAsync8("git", gitArgs, { cwd: repository.repoRoot });
20477
+ await execFileAsync7("git", gitArgs, { cwd: repository.repoRoot });
22273
20478
  } catch (error) {
22274
20479
  if (stashSha)
22275
20480
  await restoreTakeStash(options.cwd, stashSha, options.stderr);
@@ -22286,7 +20491,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22286
20491
  options.stderr
22287
20492
  )) {
22288
20493
  try {
22289
- await execFileAsync8(
20494
+ await execFileAsync7(
22290
20495
  "git",
22291
20496
  ["worktree", "remove", "--force", worktreePath],
22292
20497
  { cwd: repository.repoRoot }
@@ -22310,25 +20515,22 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22310
20515
  }
22311
20516
  const bootstrap = await bootstrapWorktree({
22312
20517
  branch: worktreeName,
22313
- cloneDirectory,
22314
20518
  config,
22315
20519
  currentRoot: repository.currentRoot,
22316
- nonInteractive: !!options.json,
20520
+ dependencyDetectionRoot: worktreePath,
20521
+ dependencyMode,
22317
20522
  repoRoot: repository.repoRoot,
22318
20523
  reporter: createBootstrapReporter(options.stderr, !!options.json),
22319
- runCommand: dependencies.runInstallCommand,
20524
+ runCommand: dependencies.runCommand,
22320
20525
  commandStdout: options.json ? () => void 0 : options.stdout,
22321
20526
  commandStderr: options.json ? () => void 0 : options.stderr,
22322
20527
  json: options.json,
22323
- worktreePath,
22324
- installDependencies: dependencies,
22325
- dependencyBootstrapPolicy: dependencyPolicy
20528
+ worktreePath
22326
20529
  });
22327
20530
  if (!bootstrap.ready) {
22328
20531
  return emitNewError(options, "worktree bootstrap failed", {
22329
20532
  dependencyBootstrap: bootstrap.dependencyBootstrap,
22330
20533
  path: worktreePath,
22331
- skipped: bootstrap.skippedDirs,
22332
20534
  syncFiles: bootstrap.syncFileFailures
22333
20535
  });
22334
20536
  }
@@ -22346,15 +20548,7 @@ ${dryRunSyncDirs.map(({ dir, bytes }) => `Would clone ${dir} (${formatBytes(byte
22346
20548
  submodules: submoduleFiles
22347
20549
  }
22348
20550
  } : { ...navigation };
22349
- if (bootstrap.clonedDirs.length > 0) {
22350
- output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
22351
- dir,
22352
- ms
22353
- }));
22354
- }
22355
- if (bootstrap.skippedDirs.length > 0)
22356
- output.skipped = bootstrap.skippedDirs;
22357
- if (bootstrap.dependencyBootstrap.mode !== "off")
20551
+ if (bootstrap.dependencyBootstrap.events.length > 0)
22358
20552
  output.dependencyBootstrap = bootstrap.dependencyBootstrap;
22359
20553
  options.stdout(`${JSON.stringify(output, null, 2)}
22360
20554
  `);
@@ -22583,7 +20777,7 @@ async function defaultPromptForBranch(placeholder) {
22583
20777
  }
22584
20778
  async function localBranchExists2(repoRoot, branchName) {
22585
20779
  try {
22586
- await execFileAsync8(
20780
+ await execFileAsync7(
22587
20781
  "git",
22588
20782
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
22589
20783
  { cwd: repoRoot }
@@ -22594,18 +20788,18 @@ async function localBranchExists2(repoRoot, branchName) {
22594
20788
  }
22595
20789
  }
22596
20790
  async function resolveCurrentWorktreeHead(cwd) {
22597
- return execFileAsync8("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(
20791
+ return execFileAsync7("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(
22598
20792
  ({ stdout: stdout3 }) => stdout3.trim()
22599
20793
  );
22600
20794
  }
22601
20795
  async function createTakeStash(cwd, worktreeName) {
22602
20796
  const message = `gji-take: ${worktreeName}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22603
- await execFileAsync8(
20797
+ await execFileAsync7(
22604
20798
  "git",
22605
20799
  ["stash", "push", "--include-untracked", "-m", message],
22606
20800
  { cwd }
22607
20801
  );
22608
- const { stdout: stdout3 } = await execFileAsync8(
20802
+ const { stdout: stdout3 } = await execFileAsync7(
22609
20803
  "git",
22610
20804
  ["stash", "list", "--format=%H%x00%s"],
22611
20805
  { cwd }
@@ -22626,12 +20820,12 @@ async function detectInProgressGitState(cwd) {
22626
20820
  ];
22627
20821
  for (const [marker, name] of markers) {
22628
20822
  try {
22629
- const { stdout: stdout3 } = await execFileAsync8(
20823
+ const { stdout: stdout3 } = await execFileAsync7(
22630
20824
  "git",
22631
20825
  ["rev-parse", "--git-path", marker],
22632
20826
  { cwd }
22633
20827
  );
22634
- await access9(resolve12(cwd, stdout3.trim()));
20828
+ await access8(resolve11(cwd, stdout3.trim()));
22635
20829
  return name;
22636
20830
  } catch {
22637
20831
  }
@@ -22678,7 +20872,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
22678
20872
  }
22679
20873
  async function listTakeFiles(cwd) {
22680
20874
  try {
22681
- const { stdout: stdout3 } = await execFileAsync8(
20875
+ const { stdout: stdout3 } = await execFileAsync7(
22682
20876
  "git",
22683
20877
  ["status", "--porcelain=v1"],
22684
20878
  { cwd }
@@ -22690,7 +20884,7 @@ async function listTakeFiles(cwd) {
22690
20884
  }
22691
20885
  async function listIgnoredFiles(cwd) {
22692
20886
  try {
22693
- const { stdout: stdout3 } = await execFileAsync8(
20887
+ const { stdout: stdout3 } = await execFileAsync7(
22694
20888
  "git",
22695
20889
  ["status", "--porcelain=v1", "--ignored", "--untracked-files=all"],
22696
20890
  { cwd }
@@ -22703,7 +20897,7 @@ async function listIgnoredFiles(cwd) {
22703
20897
  async function listSubmoduleFiles(cwd, changedFiles) {
22704
20898
  if (changedFiles.length === 0) return [];
22705
20899
  try {
22706
- const { stdout: stdout3 } = await execFileAsync8("git", ["ls-files", "--stage"], {
20900
+ const { stdout: stdout3 } = await execFileAsync7("git", ["ls-files", "--stage"], {
22707
20901
  cwd
22708
20902
  });
22709
20903
  const submodules = new Set(
@@ -22716,7 +20910,7 @@ async function listSubmoduleFiles(cwd, changedFiles) {
22716
20910
  }
22717
20911
  async function countUntrackedFiles(cwd) {
22718
20912
  try {
22719
- const { stdout: stdout3 } = await execFileAsync8(
20913
+ const { stdout: stdout3 } = await execFileAsync7(
22720
20914
  "git",
22721
20915
  ["status", "--porcelain=v1", "--untracked-files=all"],
22722
20916
  { cwd }
@@ -22728,7 +20922,7 @@ async function countUntrackedFiles(cwd) {
22728
20922
  }
22729
20923
  async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr) {
22730
20924
  try {
22731
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20925
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22732
20926
  cwd: worktreePath
22733
20927
  });
22734
20928
  } catch (error) {
@@ -22739,7 +20933,7 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22739
20933
  }
22740
20934
  if (copy) {
22741
20935
  try {
22742
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20936
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22743
20937
  cwd: sourcePath
22744
20938
  });
22745
20939
  } catch (error) {
@@ -22751,7 +20945,7 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22751
20945
  }
22752
20946
  }
22753
20947
  try {
22754
- const { stdout: stdout3 } = await execFileAsync8(
20948
+ const { stdout: stdout3 } = await execFileAsync7(
22755
20949
  "git",
22756
20950
  ["stash", "list", "--format=%H %gd"],
22757
20951
  { cwd: sourcePath }
@@ -22759,14 +20953,14 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
22759
20953
  const line = stdout3.split("\n").find((candidate) => candidate.startsWith(stashSha));
22760
20954
  const ref = line?.trim().split(/\s+/).at(1);
22761
20955
  if (ref)
22762
- await execFileAsync8("git", ["stash", "drop", ref], { cwd: sourcePath });
20956
+ await execFileAsync7("git", ["stash", "drop", ref], { cwd: sourcePath });
22763
20957
  } catch {
22764
20958
  }
22765
20959
  return true;
22766
20960
  }
22767
20961
  async function restoreTakeStash(sourcePath, stashSha, stderr) {
22768
20962
  try {
22769
- await execFileAsync8("git", ["stash", "apply", stashSha], {
20963
+ await execFileAsync7("git", ["stash", "apply", stashSha], {
22770
20964
  cwd: sourcePath
22771
20965
  });
22772
20966
  } catch {
@@ -23045,7 +21239,7 @@ async function navigateToExistingWorktree(options, path9, worktree) {
23045
21239
  {
23046
21240
  branch: worktree?.branch ?? void 0,
23047
21241
  path: path9,
23048
- repo: basename10(repository.repoRoot),
21242
+ repo: basename9(repository.repoRoot),
23049
21243
  slot: await getWorktreeSlot(path9)
23050
21244
  },
23051
21245
  options.stderr
@@ -23183,7 +21377,7 @@ async function runHistoryCommand(options) {
23183
21377
  }
23184
21378
 
23185
21379
  // src/hub.ts
23186
- import { basename as basename11 } from "node:path";
21380
+ import { basename as basename10 } from "node:path";
23187
21381
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
23188
21382
  var MAX_HUB_REPOSITORY_CONCURRENCY = 4;
23189
21383
  var defaultDependencies = {
@@ -23206,7 +21400,7 @@ async function buildHubData(cwd, dependencies = {}) {
23206
21400
  existing.sources.push(source);
23207
21401
  } else {
23208
21402
  groups.set(repoRoot, {
23209
- name: source.repoName || basename11(repoRoot),
21403
+ name: source.repoName || basename10(repoRoot),
23210
21404
  sources: [source]
23211
21405
  });
23212
21406
  }
@@ -23468,10 +21662,10 @@ async function mapWithConcurrency4(items, limit, mapper) {
23468
21662
  }
23469
21663
 
23470
21664
  // src/init.ts
23471
- import { constants as constants9 } from "node:fs";
23472
- 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";
23473
21667
  import { homedir as homedir8 } from "node:os";
23474
- import { dirname as dirname13 } from "node:path";
21668
+ import { dirname as dirname12 } from "node:path";
23475
21669
  var ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
23476
21670
  var SHELL_WRAPPED_COMMANDS = [
23477
21671
  {
@@ -23595,8 +21789,8 @@ async function runDefaultOnboarding(context) {
23595
21789
  const installCompletion = await promptForCompletion(shell, completionPath);
23596
21790
  if (installCompletion === null) return abortDefaultOnboarding();
23597
21791
  if (installCompletion) {
23598
- await mkdir10(dirname13(completionPath), { recursive: true });
23599
- await writeFile9(completionPath, renderShellCompletion(shell), "utf8");
21792
+ await mkdir9(dirname12(completionPath), { recursive: true });
21793
+ await writeFile8(completionPath, renderShellCompletion(shell), "utf8");
23600
21794
  }
23601
21795
  if (shell === "zsh" && (completionAlreadyInstalled || installCompletion)) {
23602
21796
  await ensureZshCompletionPath(rcPath);
@@ -23688,8 +21882,8 @@ async function applyOnboardingResult(result, home) {
23688
21882
  const completionAlreadyInstalled = await fileExists(completionPath);
23689
21883
  await applyShellIntegration(result.shell, result.shellIntegration, home);
23690
21884
  if (result.installCompletion) {
23691
- await mkdir10(dirname13(completionPath), { recursive: true });
23692
- await writeFile9(
21885
+ await mkdir9(dirname12(completionPath), { recursive: true });
21886
+ await writeFile8(
23693
21887
  completionPath,
23694
21888
  renderShellCompletion(result.shell),
23695
21889
  "utf8"
@@ -23704,17 +21898,17 @@ async function applyOnboardingResult(result, home) {
23704
21898
  }
23705
21899
  async function installOnboardingShellIntegration(shell, home) {
23706
21900
  const rcPath = resolveShellConfigPath(shell, home);
23707
- await mkdir10(dirname13(rcPath), { recursive: true });
21901
+ await mkdir9(dirname12(rcPath), { recursive: true });
23708
21902
  const current = await readExistingConfig(rcPath);
23709
21903
  if (shell === "fish") {
23710
- await writeFile9(
21904
+ await writeFile8(
23711
21905
  rcPath,
23712
21906
  upsertShellIntegration(current, renderShellIntegration(shell)),
23713
21907
  "utf8"
23714
21908
  );
23715
21909
  return;
23716
21910
  }
23717
- await writeFile9(
21911
+ await writeFile8(
23718
21912
  rcPath,
23719
21913
  upsertOnboardingShellIntegration(current, shell),
23720
21914
  "utf8"
@@ -23748,7 +21942,7 @@ async function ensureZshCompletionPath(rcPath) {
23748
21942
  const current = await readExistingConfig(rcPath);
23749
21943
  const next = upsertZshCompletionPath(current);
23750
21944
  if (next !== current) {
23751
- await writeFile9(rcPath, next, "utf8");
21945
+ await writeFile8(rcPath, next, "utf8");
23752
21946
  }
23753
21947
  }
23754
21948
  function upsertZshCompletionPath(existingConfig) {
@@ -23771,7 +21965,7 @@ ${suffix}`
23771
21965
  }
23772
21966
  async function fileExists(path9) {
23773
21967
  try {
23774
- await access10(path9, constants9.F_OK);
21968
+ await access9(path9, constants8.F_OK);
23775
21969
  return true;
23776
21970
  } catch {
23777
21971
  return false;
@@ -23792,10 +21986,10 @@ async function runLegacyInitCommand(options) {
23792
21986
  return 0;
23793
21987
  }
23794
21988
  const rcPath = resolveShellConfigPath(shell, home);
23795
- await mkdir10(dirname13(rcPath), { recursive: true });
21989
+ await mkdir9(dirname12(rcPath), { recursive: true });
23796
21990
  const current = await readExistingConfig(rcPath);
23797
21991
  const next = upsertShellIntegration(current, script);
23798
- await writeFile9(rcPath, next, "utf8");
21992
+ await writeFile8(rcPath, next, "utf8");
23799
21993
  options.stdout(`${rcPath}
23800
21994
  `);
23801
21995
  const { config: globalConfig } = await loadGlobalConfig(home);
@@ -23886,7 +22080,7 @@ async function saveWizardConfig(result, cwd, home) {
23886
22080
  }
23887
22081
  async function readExistingConfig(path9) {
23888
22082
  try {
23889
- return await readFile10(path9, "utf8");
22083
+ return await readFile9(path9, "utf8");
23890
22084
  } catch (error) {
23891
22085
  if (isMissingFileError3(error)) {
23892
22086
  return "";
@@ -24135,11 +22329,11 @@ function sortWorktrees(worktrees) {
24135
22329
  }
24136
22330
 
24137
22331
  // src/open.ts
24138
- import { execFile as execFile9 } from "node:child_process";
24139
- import { access as access11, writeFile as writeFile10 } from "node:fs/promises";
24140
- import { join as join16 } from "node:path";
24141
- import { promisify as promisify10 } from "node:util";
24142
- 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);
24143
22337
  function createOpenCommand(dependencies = {}) {
24144
22338
  const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
24145
22339
  const promptForEditor2 = dependencies.promptForEditor ?? defaultPromptForEditor;
@@ -24286,7 +22480,7 @@ async function detectInstalledEditors() {
24286
22480
  }
24287
22481
  async function isCommandAvailable(command) {
24288
22482
  try {
24289
- await execFileAsync9("which", [command]);
22483
+ await execFileAsync8("which", [command]);
24290
22484
  return true;
24291
22485
  } catch {
24292
22486
  return false;
@@ -24304,14 +22498,14 @@ async function defaultPromptForEditor(editors) {
24304
22498
  return choice;
24305
22499
  }
24306
22500
  async function ensureWorkspaceFile(worktreePath, repoName) {
24307
- const workspacePath = join16(worktreePath, `${repoName}.code-workspace`);
22501
+ const workspacePath = join14(worktreePath, `${repoName}.code-workspace`);
24308
22502
  try {
24309
- await access11(workspacePath);
22503
+ await access10(workspacePath);
24310
22504
  return workspacePath;
24311
22505
  } catch {
24312
22506
  }
24313
22507
  const workspace = { folders: [{ path: "." }], settings: {} };
24314
- await writeFile10(
22508
+ await writeFile9(
24315
22509
  workspacePath,
24316
22510
  `${JSON.stringify(workspace, null, 2)}
24317
22511
  `,
@@ -24321,10 +22515,10 @@ async function ensureWorkspaceFile(worktreePath, repoName) {
24321
22515
  }
24322
22516
 
24323
22517
  // src/browser.ts
24324
- import { execFile as execFile10 } from "node:child_process";
22518
+ import { execFile as execFile9 } from "node:child_process";
24325
22519
  import { platform as platform2 } from "node:process";
24326
- import { promisify as promisify11 } from "node:util";
24327
- var execFileAsync10 = promisify11(execFile10);
22520
+ import { promisify as promisify10 } from "node:util";
22521
+ var execFileAsync9 = promisify10(execFile9);
24328
22522
  function createBrowserOpener(dependencies = {}) {
24329
22523
  const currentPlatform = dependencies.platform ?? platform2;
24330
22524
  const runCommand2 = dependencies.runCommand ?? defaultRunCommand2;
@@ -24354,7 +22548,7 @@ function validateBrowserUrl(url) {
24354
22548
  }
24355
22549
  }
24356
22550
  async function defaultRunCommand2(command, args) {
24357
- await execFileAsync10(command, args, { timeout: 1e4 });
22551
+ await execFileAsync9(command, args, { timeout: 1e4 });
24358
22552
  }
24359
22553
 
24360
22554
  // src/pr-open.ts
@@ -24719,7 +22913,7 @@ function formatError(error) {
24719
22913
  }
24720
22914
 
24721
22915
  // src/remove.ts
24722
- import { basename as basename12 } from "node:path";
22916
+ import { basename as basename11 } from "node:path";
24723
22917
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
24724
22918
  function createRemoveCommand(dependencies = {}) {
24725
22919
  const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree3;
@@ -24825,7 +23019,7 @@ function createRemoveCommand(dependencies = {}) {
24825
23019
  {
24826
23020
  branch: worktree.branch ?? void 0,
24827
23021
  path: worktree.path,
24828
- repo: basename12(repository.repoRoot)
23022
+ repo: basename11(repository.repoRoot)
24829
23023
  },
24830
23024
  options.stderr
24831
23025
  );
@@ -25224,7 +23418,7 @@ function resolveConfiguredString2(value) {
25224
23418
 
25225
23419
  // src/sync-files-command.ts
25226
23420
  import { homedir as homedir9 } from "node:os";
25227
- import { join as join17 } from "node:path";
23421
+ import { join as join15 } from "node:path";
25228
23422
  async function runSyncFilesCommand(options) {
25229
23423
  const repository = await detectRepository(options.cwd);
25230
23424
  const home = options.home ?? homedir9();
@@ -25271,9 +23465,9 @@ async function runSyncFilesCommand(options) {
25271
23465
  }
25272
23466
  function findRepoConfigEntry(config, repoRoot, home) {
25273
23467
  const repos = config.repos;
25274
- if (!isPlainObject3(repos)) return null;
23468
+ if (!isPlainObject2(repos)) return null;
25275
23469
  for (const [key, value] of Object.entries(repos)) {
25276
- if (expandTilde3(key, home) === repoRoot && isPlainObject3(value)) {
23470
+ if (expandTilde3(key, home) === repoRoot && isPlainObject2(value)) {
25277
23471
  return { config: value, key };
25278
23472
  }
25279
23473
  }
@@ -25330,8 +23524,8 @@ function removeSyncFiles(existing, removals) {
25330
23524
  return existing.filter((path9) => !removalSet.has(path9));
25331
23525
  }
25332
23526
  async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
25333
- const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
25334
- const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
23527
+ const repos = isPlainObject2(config.repos) ? { ...config.repos } : {};
23528
+ const repoConfig = isPlainObject2(repos[repoKey]) ? repos[repoKey] : {};
25335
23529
  const nextRepoConfig = { ...repoConfig };
25336
23530
  if (syncFiles2.length > 0) {
25337
23531
  nextRepoConfig.syncFiles = syncFiles2;
@@ -25345,7 +23539,7 @@ async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
25345
23539
  }
25346
23540
  await saveGlobalConfig({ ...config, repos }, home);
25347
23541
  }
25348
- function isPlainObject3(value) {
23542
+ function isPlainObject2(value) {
25349
23543
  return typeof value === "object" && value !== null && !Array.isArray(value);
25350
23544
  }
25351
23545
  function writeError(options, message) {
@@ -25359,7 +23553,7 @@ function writeError(options, message) {
25359
23553
  }
25360
23554
  function expandTilde3(value, home) {
25361
23555
  if (value === "~") return home;
25362
- if (value.startsWith("~/")) return join17(home, value.slice(2));
23556
+ if (value.startsWith("~/")) return join15(home, value.slice(2));
25363
23557
  return value;
25364
23558
  }
25365
23559
 
@@ -25624,9 +23818,7 @@ async function maybeRegisterCurrentRepo(cwd) {
25624
23818
  }
25625
23819
  }
25626
23820
  function registerCommands(program2) {
25627
- program2.command("new [branch]").description(
25628
- "create a new branch or detached linked worktree and CoW-bootstrap configured directories"
25629
- ).option(
23821
+ program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
25630
23822
  "-f, --force",
25631
23823
  "remove and recreate the worktree if the target path already exists"
25632
23824
  ).option("--detached", "create a detached worktree without a branch").option(
@@ -25644,6 +23836,9 @@ function registerCommands(program2) {
25644
23836
  ).option(
25645
23837
  "--dry-run",
25646
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"
25647
23842
  ).option(
25648
23843
  "--json",
25649
23844
  "emit JSON on success or error instead of human-readable output"
@@ -25666,6 +23861,9 @@ function registerCommands(program2) {
25666
23861
  ).option(
25667
23862
  "--dry-run",
25668
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"
25669
23867
  ).option(
25670
23868
  "--json",
25671
23869
  "emit JSON on success or error instead of human-readable output"
@@ -25743,6 +23941,7 @@ function attachCommandActions(program2, options) {
25743
23941
  force: commandOptions2.force,
25744
23942
  noFetch: commandOptions2.fetch === false,
25745
23943
  json: commandOptions2.json,
23944
+ noInstall: commandOptions2.install === false,
25746
23945
  open: commandOptions2.open,
25747
23946
  take: commandOptions2.take,
25748
23947
  task: commandOptions2.task
@@ -25825,6 +24024,7 @@ function attachCommandActions(program2, options) {
25825
24024
  cwd: options.cwd,
25826
24025
  dryRun: commandOptions2.dryRun,
25827
24026
  json: commandOptions2.json,
24027
+ noInstall: commandOptions2.install === false,
25828
24028
  number,
25829
24029
  stderr: options.stderr,
25830
24030
  stdout: options.stdout