@solaqua/gji 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +26 -1
  2. package/dist/back.d.ts +1 -1
  3. package/dist/back.js +23 -17
  4. package/dist/clean.d.ts +1 -1
  5. package/dist/clean.js +44 -35
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +264 -164
  8. package/dist/completion.js +3 -3
  9. package/dist/config-command.js +5 -5
  10. package/dist/config.js +41 -35
  11. package/dist/conflict.d.ts +1 -1
  12. package/dist/conflict.js +14 -6
  13. package/dist/editor.js +29 -9
  14. package/dist/file-sync.d.ts +1 -0
  15. package/dist/file-sync.js +15 -11
  16. package/dist/git.d.ts +1 -1
  17. package/dist/git.js +21 -19
  18. package/dist/gji-bundle.mjs +1709 -850
  19. package/dist/go.d.ts +2 -2
  20. package/dist/go.js +39 -26
  21. package/dist/headless.js +1 -1
  22. package/dist/history-command.js +3 -3
  23. package/dist/history.js +12 -12
  24. package/dist/hooks.js +16 -16
  25. package/dist/index.js +13 -9
  26. package/dist/init.d.ts +2 -2
  27. package/dist/init.js +106 -94
  28. package/dist/install-prompt.d.ts +3 -3
  29. package/dist/install-prompt.js +46 -28
  30. package/dist/ls.d.ts +2 -2
  31. package/dist/ls.js +29 -29
  32. package/dist/new.d.ts +2 -2
  33. package/dist/new.js +96 -81
  34. package/dist/open.d.ts +2 -2
  35. package/dist/open.js +24 -21
  36. package/dist/package-manager.js +96 -45
  37. package/dist/pr.d.ts +2 -2
  38. package/dist/pr.js +47 -34
  39. package/dist/remove.d.ts +1 -1
  40. package/dist/remove.js +39 -27
  41. package/dist/repo-registry.js +45 -19
  42. package/dist/repo.js +29 -28
  43. package/dist/root.js +3 -3
  44. package/dist/shell-completion.d.ts +1 -1
  45. package/dist/shell-completion.js +65 -37
  46. package/dist/shell-handoff.js +2 -2
  47. package/dist/shell.d.ts +1 -1
  48. package/dist/shell.js +4 -4
  49. package/dist/status.d.ts +5 -5
  50. package/dist/status.js +23 -23
  51. package/dist/sync-files-command.d.ts +10 -0
  52. package/dist/sync-files-command.js +137 -0
  53. package/dist/sync.js +23 -15
  54. package/dist/trigger-hook.js +9 -5
  55. package/dist/warp.js +66 -34
  56. package/dist/worktree-info.d.ts +9 -9
  57. package/dist/worktree-info.js +31 -29
  58. package/dist/worktree-management.d.ts +1 -1
  59. package/dist/worktree-management.js +26 -11
  60. package/dist/worktree-prompts.js +5 -5
  61. package/man/man1/gji-back.1 +1 -1
  62. package/man/man1/gji-clean.1 +1 -1
  63. package/man/man1/gji-completion.1 +1 -1
  64. package/man/man1/gji-config.1 +1 -1
  65. package/man/man1/gji-go.1 +1 -1
  66. package/man/man1/gji-history.1 +1 -1
  67. package/man/man1/gji-init.1 +1 -1
  68. package/man/man1/gji-ls.1 +1 -1
  69. package/man/man1/gji-new.1 +1 -1
  70. package/man/man1/gji-open.1 +1 -1
  71. package/man/man1/gji-pr.1 +1 -1
  72. package/man/man1/gji-remove.1 +1 -1
  73. package/man/man1/gji-root.1 +1 -1
  74. package/man/man1/gji-status.1 +1 -1
  75. package/man/man1/gji-sync-files.1 +23 -0
  76. package/man/man1/gji-sync.1 +1 -1
  77. package/man/man1/gji-trigger-hook.1 +1 -1
  78. package/man/man1/gji-warp.1 +1 -1
  79. package/man/man1/gji.1 +5 -1
  80. package/package.json +8 -2
@@ -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 join9 = __require("path").join;
5389
+ var join10 = __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
- join9(etc, name, "config"),
5415
- join9(etc, name + "rc")
5414
+ join10(etc, name, "config"),
5415
+ join10(etc, name + "rc")
5416
5416
  ].forEach(addConfigFile);
5417
5417
  if (home)
5418
5418
  [
5419
- join9(home, ".config", name, "config"),
5420
- join9(home, ".config", name),
5421
- join9(home, "." + name, "config"),
5422
- join9(home, "." + name + "rc")
5419
+ join10(home, ".config", name, "config"),
5420
+ join10(home, ".config", name),
5421
+ join10(home, "." + name, "config"),
5422
+ join10(home, "." + name + "rc")
5423
5423
  ].forEach(addConfigFile);
5424
5424
  addConfigFile(cc.find("." + name + "rc"));
5425
5425
  if (env4.config) addConfigFile(env4.config);
@@ -9422,225 +9422,7 @@ var require_picocolors = __commonJS({
9422
9422
  });
9423
9423
 
9424
9424
  // src/index.ts
9425
- import { homedir as homedir6 } from "node:os";
9426
-
9427
- // src/config.ts
9428
- import { mkdir, readFile, writeFile } from "node:fs/promises";
9429
- import { homedir } from "node:os";
9430
- import { dirname, join, resolve } from "node:path";
9431
- var CONFIG_FILE_NAME = ".gji.json";
9432
- var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
9433
- var GLOBAL_CONFIG_NAME = "config.json";
9434
- var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
9435
- "branchPrefix",
9436
- "editor",
9437
- "hooks",
9438
- "installSaveTarget",
9439
- "shellIntegration",
9440
- "skipInstallPrompt",
9441
- "syncDefaultBranch",
9442
- "syncFiles",
9443
- "syncRemote",
9444
- "worktreePath"
9445
- ]);
9446
- var KNOWN_GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set([
9447
- ...KNOWN_CONFIG_KEYS,
9448
- "repos"
9449
- ]);
9450
- var DEFAULT_CONFIG = Object.freeze({});
9451
- async function loadConfig(root) {
9452
- const path9 = join(root, CONFIG_FILE_NAME);
9453
- return loadConfigFile(path9);
9454
- }
9455
- async function loadEffectiveConfig(root, home = homedir(), onWarning) {
9456
- const [globalConfig, localConfig] = await Promise.all([
9457
- loadGlobalConfig(home),
9458
- loadConfig(root)
9459
- ]);
9460
- const repos = globalConfig.config.repos;
9461
- const perRepoConfig = isPlainObject(repos) ? findPerRepoConfig(repos, root, home) : {};
9462
- const globalBase = { ...globalConfig.config };
9463
- delete globalBase.repos;
9464
- if (onWarning) {
9465
- if (globalConfig.exists) {
9466
- warnUnknownKeys(globalBase, globalConfig.path, KNOWN_GLOBAL_CONFIG_KEYS, onWarning);
9467
- if (Object.keys(perRepoConfig).length > 0) {
9468
- warnUnknownKeys(perRepoConfig, globalConfig.path, KNOWN_CONFIG_KEYS, onWarning);
9469
- }
9470
- }
9471
- if (localConfig.exists) {
9472
- warnUnknownKeys(localConfig.config, localConfig.path, KNOWN_CONFIG_KEYS, onWarning);
9473
- }
9474
- }
9475
- const merged = mergeConfig(globalBase, perRepoConfig, localConfig.config);
9476
- const worktreePathValue = merged.worktreePath;
9477
- if (onWarning && typeof worktreePathValue === "string" && worktreePathValue.length > 0 && !worktreePathValue.startsWith("/") && !worktreePathValue.startsWith("~")) {
9478
- onWarning(
9479
- `gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" \u2014 using default
9480
- `
9481
- );
9482
- }
9483
- const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
9484
- const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
9485
- const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
9486
- if (Object.keys(globalHooks).length > 0 || Object.keys(perRepoHooks).length > 0 || Object.keys(localHooks).length > 0) {
9487
- merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
9488
- }
9489
- return merged;
9490
- }
9491
- async function loadGlobalConfig(home = homedir()) {
9492
- return loadConfigFile(GLOBAL_CONFIG_FILE_PATH(home));
9493
- }
9494
- async function saveLocalConfig(root, config) {
9495
- const path9 = join(root, CONFIG_FILE_NAME);
9496
- await writeFile(path9, `${JSON.stringify(config, null, 2)}
9497
- `, "utf8");
9498
- return path9;
9499
- }
9500
- async function updateLocalConfigKey(root, key, value) {
9501
- const loaded = await loadConfig(root);
9502
- const nextConfig = {
9503
- ...loaded.config,
9504
- [key]: value
9505
- };
9506
- await saveLocalConfig(root, nextConfig);
9507
- return nextConfig;
9508
- }
9509
- async function saveGlobalConfig(config, home = homedir()) {
9510
- const path9 = GLOBAL_CONFIG_FILE_PATH(home);
9511
- await mkdir(dirname(path9), { recursive: true });
9512
- await writeFile(path9, `${JSON.stringify(config, null, 2)}
9513
- `, "utf8");
9514
- return path9;
9515
- }
9516
- async function unsetGlobalConfigKey(key, home = homedir()) {
9517
- const loaded = await loadGlobalConfig(home);
9518
- const nextConfig = { ...loaded.config };
9519
- delete nextConfig[key];
9520
- await saveGlobalConfig(nextConfig, home);
9521
- return nextConfig;
9522
- }
9523
- async function updateGlobalConfigKey(key, value, home = homedir()) {
9524
- const loaded = await loadGlobalConfig(home);
9525
- const nextConfig = {
9526
- ...loaded.config,
9527
- [key]: value
9528
- };
9529
- await saveGlobalConfig(nextConfig, home);
9530
- return nextConfig;
9531
- }
9532
- async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
9533
- const loaded = await loadGlobalConfig(home);
9534
- const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
9535
- const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
9536
- repos[repoRoot] = { ...existing, [key]: value };
9537
- const nextConfig = { ...loaded.config, repos };
9538
- await saveGlobalConfig(nextConfig, home);
9539
- return nextConfig;
9540
- }
9541
- function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
9542
- const configDir = process.env.GJI_CONFIG_DIR;
9543
- if (configDir) {
9544
- return join(resolve(configDir), GLOBAL_CONFIG_NAME);
9545
- }
9546
- return join(home, GLOBAL_CONFIG_DIRECTORY, GLOBAL_CONFIG_NAME);
9547
- }
9548
- function parseConfigValue(value) {
9549
- try {
9550
- return JSON.parse(value);
9551
- } catch {
9552
- return value;
9553
- }
9554
- }
9555
- function resolveConfigString(config, key) {
9556
- const value = config[key];
9557
- return typeof value === "string" && value.length > 0 ? value : void 0;
9558
- }
9559
- async function loadConfigFile(path9) {
9560
- try {
9561
- const rawConfig = await readFile(path9, "utf8");
9562
- const parsedConfig = JSON.parse(rawConfig);
9563
- return {
9564
- config: mergeConfig(parsedConfig),
9565
- exists: true,
9566
- path: path9
9567
- };
9568
- } catch (error) {
9569
- if (isMissingFileError(error)) {
9570
- return {
9571
- config: DEFAULT_CONFIG,
9572
- exists: false,
9573
- path: path9
9574
- };
9575
- }
9576
- throw error;
9577
- }
9578
- }
9579
- function mergeConfig(...values) {
9580
- return values.reduce(
9581
- (config, value) => ({
9582
- ...config,
9583
- ...value
9584
- }),
9585
- { ...DEFAULT_CONFIG }
9586
- );
9587
- }
9588
- function findPerRepoConfig(repos, repoRoot, home) {
9589
- for (const [key, value] of Object.entries(repos)) {
9590
- const expandedKey = expandTilde(key, home);
9591
- if (expandedKey === repoRoot && isPlainObject(value)) {
9592
- return value;
9593
- }
9594
- }
9595
- return {};
9596
- }
9597
- function expandTilde(value, home) {
9598
- if (value === "~") return home;
9599
- if (value.startsWith("~/")) return join(home, value.slice(2));
9600
- return value;
9601
- }
9602
- function isPlainObject(value) {
9603
- return typeof value === "object" && value !== null && !Array.isArray(value);
9604
- }
9605
- function isMissingFileError(error) {
9606
- return error instanceof Error && "code" in error && error.code === "ENOENT";
9607
- }
9608
- function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
9609
- for (const key of Object.keys(config)) {
9610
- if (!knownKeys.has(key)) {
9611
- const suggestion = closestKey(key, knownKeys);
9612
- const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
9613
- onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}
9614
- `);
9615
- }
9616
- }
9617
- }
9618
- function closestKey(unknown, knownKeys) {
9619
- let best = null;
9620
- let bestDist = Infinity;
9621
- for (const key of knownKeys) {
9622
- const dist = levenshtein(unknown, key);
9623
- if (dist < bestDist) {
9624
- bestDist = dist;
9625
- best = key;
9626
- }
9627
- }
9628
- return bestDist <= Math.max(2, Math.floor(unknown.length / 2)) ? best : null;
9629
- }
9630
- function levenshtein(a, b3) {
9631
- const m2 = a.length;
9632
- const n = b3.length;
9633
- const dp = Array.from(
9634
- { length: m2 + 1 },
9635
- (_3, i) => Array.from({ length: n + 1 }, (_4, j2) => i === 0 ? j2 : j2 === 0 ? i : 0)
9636
- );
9637
- for (let i = 1; i <= m2; i++) {
9638
- for (let j2 = 1; j2 <= n; j2++) {
9639
- dp[i][j2] = a[i - 1] === b3[j2 - 1] ? dp[i - 1][j2 - 1] : 1 + Math.min(dp[i - 1][j2], dp[i][j2 - 1], dp[i - 1][j2 - 1]);
9640
- }
9641
- }
9642
- return dp[m2][n];
9643
- }
9425
+ import { homedir as homedir7 } from "node:os";
9644
9426
 
9645
9427
  // src/cli.ts
9646
9428
  import { createRequire } from "node:module";
@@ -9740,7 +9522,7 @@ var retryifyAsync = (fn, options) => {
9740
9522
  throw error;
9741
9523
  const delay2 = Math.round(interval * Math.random());
9742
9524
  if (delay2 > 0) {
9743
- const delayPromise = new Promise((resolve5) => setTimeout(resolve5, delay2));
9525
+ const delayPromise = new Promise((resolve6) => setTimeout(resolve6, delay2));
9744
9526
  return delayPromise.then(() => attempt.apply(void 0, args));
9745
9527
  } else {
9746
9528
  return attempt.apply(void 0, args);
@@ -9999,14 +9781,14 @@ var Temp = {
9999
9781
  }
10000
9782
  },
10001
9783
  truncate: (filePath) => {
10002
- const basename8 = path2.basename(filePath);
10003
- if (basename8.length <= LIMIT_BASENAME_LENGTH)
9784
+ const basename9 = path2.basename(filePath);
9785
+ if (basename9.length <= LIMIT_BASENAME_LENGTH)
10004
9786
  return filePath;
10005
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename8);
9787
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename9);
10006
9788
  if (!truncable)
10007
9789
  return filePath;
10008
- const truncationLength = basename8.length - LIMIT_BASENAME_LENGTH;
10009
- return `${filePath.slice(0, -basename8.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
9790
+ const truncationLength = basename9.length - LIMIT_BASENAME_LENGTH;
9791
+ return `${filePath.slice(0, -basename9.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
10010
9792
  }
10011
9793
  };
10012
9794
  node_default(Temp.purgeSyncAll);
@@ -11306,14 +11088,14 @@ var TimeoutError = class extends Error {
11306
11088
 
11307
11089
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
11308
11090
  async function timeout(request, init, abortController, options) {
11309
- return new Promise((resolve5, reject) => {
11091
+ return new Promise((resolve6, reject) => {
11310
11092
  const timeoutId = setTimeout(() => {
11311
11093
  if (abortController) {
11312
11094
  abortController.abort();
11313
11095
  }
11314
11096
  reject(new TimeoutError(request));
11315
11097
  }, options.timeout);
11316
- void options.fetch(request, init).then(resolve5).catch(reject).then(() => {
11098
+ void options.fetch(request, init).then(resolve6).catch(reject).then(() => {
11317
11099
  clearTimeout(timeoutId);
11318
11100
  });
11319
11101
  });
@@ -11321,7 +11103,7 @@ async function timeout(request, init, abortController, options) {
11321
11103
 
11322
11104
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
11323
11105
  async function delay(ms, { signal }) {
11324
- return new Promise((resolve5, reject) => {
11106
+ return new Promise((resolve6, reject) => {
11325
11107
  if (signal) {
11326
11108
  signal.throwIfAborted();
11327
11109
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -11332,7 +11114,7 @@ async function delay(ms, { signal }) {
11332
11114
  }
11333
11115
  const timeoutId = setTimeout(() => {
11334
11116
  signal?.removeEventListener("abort", abortHandler);
11335
- resolve5();
11117
+ resolve6();
11336
11118
  }, ms);
11337
11119
  });
11338
11120
  }
@@ -13032,23 +12814,296 @@ function updateNotifier(options) {
13032
12814
  import { access } from "node:fs/promises";
13033
12815
  import { basename as basename2 } from "node:path";
13034
12816
 
13035
- // src/hooks.ts
13036
- import { spawn as spawn2 } from "node:child_process";
13037
- async function runHook(hookCmd, cwd, context, stderr) {
13038
- if (!hookCmd) return;
13039
- if (Array.isArray(hookCmd)) {
13040
- await runArgvHook(hookCmd, cwd, context, stderr);
13041
- return;
13042
- }
13043
- await runShellHook(hookCmd, cwd, context, stderr);
12817
+ // src/config.ts
12818
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
12819
+ import { homedir } from "node:os";
12820
+ import { dirname, join, resolve } from "node:path";
12821
+ var CONFIG_FILE_NAME = ".gji.json";
12822
+ var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
12823
+ var GLOBAL_CONFIG_NAME = "config.json";
12824
+ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
12825
+ "branchPrefix",
12826
+ "editor",
12827
+ "hooks",
12828
+ "installSaveTarget",
12829
+ "shellIntegration",
12830
+ "skipInstallPrompt",
12831
+ "syncDefaultBranch",
12832
+ "syncFiles",
12833
+ "syncRemote",
12834
+ "worktreePath"
12835
+ ]);
12836
+ var KNOWN_GLOBAL_CONFIG_KEYS = /* @__PURE__ */ new Set([
12837
+ ...KNOWN_CONFIG_KEYS,
12838
+ "repos"
12839
+ ]);
12840
+ var DEFAULT_CONFIG = Object.freeze({});
12841
+ async function loadConfig(root) {
12842
+ const path9 = join(root, CONFIG_FILE_NAME);
12843
+ return loadConfigFile(path9);
13044
12844
  }
13045
- async function runArgvHook(hookCmd, cwd, context, stderr) {
13046
- const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
13047
- if (!command) {
13048
- stderr("gji: hook argv command must include a non-empty command\n");
12845
+ async function loadEffectiveConfig(root, home = homedir(), onWarning) {
12846
+ const [globalConfig, localConfig] = await Promise.all([
12847
+ loadGlobalConfig(home),
12848
+ loadConfig(root)
12849
+ ]);
12850
+ const repos = globalConfig.config.repos;
12851
+ const perRepoConfig = isPlainObject(repos) ? findPerRepoConfig(repos, root, home) : {};
12852
+ const globalBase = { ...globalConfig.config };
12853
+ delete globalBase.repos;
12854
+ if (onWarning) {
12855
+ if (globalConfig.exists) {
12856
+ warnUnknownKeys(
12857
+ globalBase,
12858
+ globalConfig.path,
12859
+ KNOWN_GLOBAL_CONFIG_KEYS,
12860
+ onWarning
12861
+ );
12862
+ if (Object.keys(perRepoConfig).length > 0) {
12863
+ warnUnknownKeys(
12864
+ perRepoConfig,
12865
+ globalConfig.path,
12866
+ KNOWN_CONFIG_KEYS,
12867
+ onWarning
12868
+ );
12869
+ }
12870
+ }
12871
+ if (localConfig.exists) {
12872
+ warnUnknownKeys(
12873
+ localConfig.config,
12874
+ localConfig.path,
12875
+ KNOWN_CONFIG_KEYS,
12876
+ onWarning
12877
+ );
12878
+ }
12879
+ }
12880
+ const merged = mergeConfig(globalBase, perRepoConfig, localConfig.config);
12881
+ const worktreePathValue = merged.worktreePath;
12882
+ if (onWarning && typeof worktreePathValue === "string" && worktreePathValue.length > 0 && !worktreePathValue.startsWith("/") && !worktreePathValue.startsWith("~")) {
12883
+ onWarning(
12884
+ `gji: "worktreePath" must be an absolute path or start with ~, got "${worktreePathValue}" \u2014 using default
12885
+ `
12886
+ );
12887
+ }
12888
+ const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
12889
+ const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
12890
+ const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
12891
+ if (Object.keys(globalHooks).length > 0 || Object.keys(perRepoHooks).length > 0 || Object.keys(localHooks).length > 0) {
12892
+ merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
12893
+ }
12894
+ return merged;
12895
+ }
12896
+ async function loadGlobalConfig(home = homedir()) {
12897
+ return loadConfigFile(GLOBAL_CONFIG_FILE_PATH(home));
12898
+ }
12899
+ async function saveLocalConfig(root, config) {
12900
+ const path9 = join(root, CONFIG_FILE_NAME);
12901
+ await writeFile(path9, `${JSON.stringify(config, null, 2)}
12902
+ `, "utf8");
12903
+ return path9;
12904
+ }
12905
+ async function updateLocalConfigKey(root, key, value) {
12906
+ const loaded = await loadConfig(root);
12907
+ const nextConfig = {
12908
+ ...loaded.config,
12909
+ [key]: value
12910
+ };
12911
+ await saveLocalConfig(root, nextConfig);
12912
+ return nextConfig;
12913
+ }
12914
+ async function saveGlobalConfig(config, home = homedir()) {
12915
+ const path9 = GLOBAL_CONFIG_FILE_PATH(home);
12916
+ await mkdir(dirname(path9), { recursive: true });
12917
+ await writeFile(path9, `${JSON.stringify(config, null, 2)}
12918
+ `, "utf8");
12919
+ return path9;
12920
+ }
12921
+ async function unsetGlobalConfigKey(key, home = homedir()) {
12922
+ const loaded = await loadGlobalConfig(home);
12923
+ const nextConfig = { ...loaded.config };
12924
+ delete nextConfig[key];
12925
+ await saveGlobalConfig(nextConfig, home);
12926
+ return nextConfig;
12927
+ }
12928
+ async function updateGlobalConfigKey(key, value, home = homedir()) {
12929
+ const loaded = await loadGlobalConfig(home);
12930
+ const nextConfig = {
12931
+ ...loaded.config,
12932
+ [key]: value
12933
+ };
12934
+ await saveGlobalConfig(nextConfig, home);
12935
+ return nextConfig;
12936
+ }
12937
+ async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
12938
+ const loaded = await loadGlobalConfig(home);
12939
+ const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
12940
+ const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
12941
+ repos[repoRoot] = { ...existing, [key]: value };
12942
+ const nextConfig = { ...loaded.config, repos };
12943
+ await saveGlobalConfig(nextConfig, home);
12944
+ return nextConfig;
12945
+ }
12946
+ function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
12947
+ const configDir = process.env.GJI_CONFIG_DIR;
12948
+ if (configDir) {
12949
+ return join(resolve(configDir), GLOBAL_CONFIG_NAME);
12950
+ }
12951
+ return join(home, GLOBAL_CONFIG_DIRECTORY, GLOBAL_CONFIG_NAME);
12952
+ }
12953
+ function parseConfigValue(value) {
12954
+ try {
12955
+ return JSON.parse(value);
12956
+ } catch {
12957
+ return value;
12958
+ }
12959
+ }
12960
+ function resolveConfigString(config, key) {
12961
+ const value = config[key];
12962
+ return typeof value === "string" && value.length > 0 ? value : void 0;
12963
+ }
12964
+ async function loadConfigFile(path9) {
12965
+ try {
12966
+ const rawConfig = await readFile(path9, "utf8");
12967
+ const parsedConfig = JSON.parse(rawConfig);
12968
+ return {
12969
+ config: mergeConfig(parsedConfig),
12970
+ exists: true,
12971
+ path: path9
12972
+ };
12973
+ } catch (error) {
12974
+ if (isMissingFileError(error)) {
12975
+ return {
12976
+ config: DEFAULT_CONFIG,
12977
+ exists: false,
12978
+ path: path9
12979
+ };
12980
+ }
12981
+ throw error;
12982
+ }
12983
+ }
12984
+ function mergeConfig(...values) {
12985
+ return values.reduce(
12986
+ (config, value) => ({
12987
+ ...config,
12988
+ ...value
12989
+ }),
12990
+ { ...DEFAULT_CONFIG }
12991
+ );
12992
+ }
12993
+ function findPerRepoConfig(repos, repoRoot, home) {
12994
+ for (const [key, value] of Object.entries(repos)) {
12995
+ const expandedKey = expandTilde(key, home);
12996
+ if (expandedKey === repoRoot && isPlainObject(value)) {
12997
+ return value;
12998
+ }
12999
+ }
13000
+ return {};
13001
+ }
13002
+ function expandTilde(value, home) {
13003
+ if (value === "~") return home;
13004
+ if (value.startsWith("~/")) return join(home, value.slice(2));
13005
+ return value;
13006
+ }
13007
+ function isPlainObject(value) {
13008
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13009
+ }
13010
+ function isMissingFileError(error) {
13011
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
13012
+ }
13013
+ function warnUnknownKeys(config, filePath, knownKeys, onWarning) {
13014
+ for (const key of Object.keys(config)) {
13015
+ if (!knownKeys.has(key)) {
13016
+ const suggestion = closestKey(key, knownKeys);
13017
+ const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
13018
+ onWarning(`gji: unknown config key "${key}" in ${filePath}${hint}
13019
+ `);
13020
+ }
13021
+ }
13022
+ }
13023
+ function closestKey(unknown, knownKeys) {
13024
+ let best = null;
13025
+ let bestDist = Infinity;
13026
+ for (const key of knownKeys) {
13027
+ const dist = levenshtein(unknown, key);
13028
+ if (dist < bestDist) {
13029
+ bestDist = dist;
13030
+ best = key;
13031
+ }
13032
+ }
13033
+ return bestDist <= Math.max(2, Math.floor(unknown.length / 2)) ? best : null;
13034
+ }
13035
+ function levenshtein(a, b3) {
13036
+ const m2 = a.length;
13037
+ const n = b3.length;
13038
+ const dp = Array.from(
13039
+ { length: m2 + 1 },
13040
+ (_3, i) => Array.from({ length: n + 1 }, (_4, j2) => i === 0 ? j2 : j2 === 0 ? i : 0)
13041
+ );
13042
+ for (let i = 1; i <= m2; i++) {
13043
+ for (let j2 = 1; j2 <= n; j2++) {
13044
+ dp[i][j2] = a[i - 1] === b3[j2 - 1] ? dp[i - 1][j2 - 1] : 1 + Math.min(dp[i - 1][j2], dp[i][j2 - 1], dp[i - 1][j2 - 1]);
13045
+ }
13046
+ }
13047
+ return dp[m2][n];
13048
+ }
13049
+
13050
+ // src/history.ts
13051
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
13052
+ import { homedir as homedir2 } from "node:os";
13053
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
13054
+ var HISTORY_FILE_NAME = "history.json";
13055
+ var MAX_HISTORY_ENTRIES = 50;
13056
+ function HISTORY_FILE_PATH(home = homedir2()) {
13057
+ const configDir = process.env.GJI_CONFIG_DIR;
13058
+ if (configDir) {
13059
+ return join2(resolve2(configDir), HISTORY_FILE_NAME);
13060
+ }
13061
+ return join2(home, GLOBAL_CONFIG_DIRECTORY, HISTORY_FILE_NAME);
13062
+ }
13063
+ async function loadHistory(home = homedir2()) {
13064
+ const path9 = HISTORY_FILE_PATH(home);
13065
+ try {
13066
+ const raw = await readFile2(path9, "utf8");
13067
+ const parsed = JSON.parse(raw);
13068
+ if (!Array.isArray(parsed)) return [];
13069
+ return parsed.filter(isHistoryEntry);
13070
+ } catch {
13071
+ return [];
13072
+ }
13073
+ }
13074
+ async function appendHistory(path9, branch, home = homedir2()) {
13075
+ const historyPath = HISTORY_FILE_PATH(home);
13076
+ const existing = await loadHistory(home);
13077
+ if (existing.length > 0 && existing[0].path === path9) {
13078
+ return;
13079
+ }
13080
+ const entry = { branch, path: path9, timestamp: Date.now() };
13081
+ const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
13082
+ await mkdir2(dirname2(historyPath), { recursive: true });
13083
+ await writeFile2(historyPath, `${JSON.stringify(next, null, 2)}
13084
+ `, "utf8");
13085
+ }
13086
+ function isHistoryEntry(value) {
13087
+ return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "timestamp" in value && typeof value.timestamp === "number";
13088
+ }
13089
+
13090
+ // src/hooks.ts
13091
+ import { spawn as spawn2 } from "node:child_process";
13092
+ async function runHook(hookCmd, cwd, context, stderr) {
13093
+ if (!hookCmd) return;
13094
+ if (Array.isArray(hookCmd)) {
13095
+ await runArgvHook(hookCmd, cwd, context, stderr);
13049
13096
  return;
13050
13097
  }
13051
- await new Promise((resolve5) => {
13098
+ await runShellHook(hookCmd, cwd, context, stderr);
13099
+ }
13100
+ async function runArgvHook(hookCmd, cwd, context, stderr) {
13101
+ const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
13102
+ if (!command) {
13103
+ stderr("gji: hook argv command must include a non-empty command\n");
13104
+ return;
13105
+ }
13106
+ await new Promise((resolve6) => {
13052
13107
  const child = spawn2(command, args, {
13053
13108
  cwd,
13054
13109
  shell: false,
@@ -13060,21 +13115,23 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
13060
13115
  });
13061
13116
  child.on("close", (code) => {
13062
13117
  if (code !== 0) {
13063
- stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
13064
- `);
13118
+ stderr(
13119
+ `gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
13120
+ `
13121
+ );
13065
13122
  }
13066
- resolve5();
13123
+ resolve6();
13067
13124
  });
13068
13125
  child.on("error", (err) => {
13069
13126
  stderr(`gji: hook failed to start: ${err.message}
13070
13127
  `);
13071
- resolve5();
13128
+ resolve6();
13072
13129
  });
13073
13130
  });
13074
13131
  }
13075
13132
  async function runShellHook(hookCmd, cwd, context, stderr) {
13076
13133
  const interpolated = interpolate(hookCmd, context);
13077
- await new Promise((resolve5) => {
13134
+ await new Promise((resolve6) => {
13078
13135
  const child = spawn2(interpolated, {
13079
13136
  cwd,
13080
13137
  shell: true,
@@ -13089,12 +13146,12 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
13089
13146
  stderr(`gji: hook exited with code ${code}: ${interpolated}
13090
13147
  `);
13091
13148
  }
13092
- resolve5();
13149
+ resolve6();
13093
13150
  });
13094
13151
  child.on("error", (err) => {
13095
13152
  stderr(`gji: hook failed to start: ${err.message}
13096
13153
  `);
13097
- resolve5();
13154
+ resolve6();
13098
13155
  });
13099
13156
  });
13100
13157
  }
@@ -13132,49 +13189,9 @@ function parseHookCommand(value) {
13132
13189
  return void 0;
13133
13190
  }
13134
13191
 
13135
- // src/history.ts
13136
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
13137
- import { homedir as homedir2 } from "node:os";
13138
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
13139
- var HISTORY_FILE_NAME = "history.json";
13140
- var MAX_HISTORY_ENTRIES = 50;
13141
- function HISTORY_FILE_PATH(home = homedir2()) {
13142
- const configDir = process.env.GJI_CONFIG_DIR;
13143
- if (configDir) {
13144
- return join2(resolve2(configDir), HISTORY_FILE_NAME);
13145
- }
13146
- return join2(home, GLOBAL_CONFIG_DIRECTORY, HISTORY_FILE_NAME);
13147
- }
13148
- async function loadHistory(home = homedir2()) {
13149
- const path9 = HISTORY_FILE_PATH(home);
13150
- try {
13151
- const raw = await readFile2(path9, "utf8");
13152
- const parsed = JSON.parse(raw);
13153
- if (!Array.isArray(parsed)) return [];
13154
- return parsed.filter(isHistoryEntry);
13155
- } catch {
13156
- return [];
13157
- }
13158
- }
13159
- async function appendHistory(path9, branch, home = homedir2()) {
13160
- const historyPath = HISTORY_FILE_PATH(home);
13161
- const existing = await loadHistory(home);
13162
- if (existing.length > 0 && existing[0].path === path9) {
13163
- return;
13164
- }
13165
- const entry = { branch, path: path9, timestamp: Date.now() };
13166
- const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
13167
- await mkdir2(dirname2(historyPath), { recursive: true });
13168
- await writeFile2(historyPath, `${JSON.stringify(next, null, 2)}
13169
- `, "utf8");
13170
- }
13171
- function isHistoryEntry(value) {
13172
- return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "timestamp" in value && typeof value.timestamp === "number";
13173
- }
13174
-
13175
13192
  // src/repo.ts
13176
- import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13177
13193
  import { homedir as homedir3 } from "node:os";
13194
+ import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13178
13195
 
13179
13196
  // src/git.ts
13180
13197
  import { execFile } from "node:child_process";
@@ -13190,7 +13207,11 @@ async function runGit(cwd, args) {
13190
13207
  }
13191
13208
  }
13192
13209
  async function readWorktreeHealth(cwd) {
13193
- const { stdout } = await execFileAsync("git", ["status", "--porcelain=v2", "--branch"], { cwd });
13210
+ const { stdout } = await execFileAsync(
13211
+ "git",
13212
+ ["status", "--porcelain=v2", "--branch"],
13213
+ { cwd }
13214
+ );
13194
13215
  return parseWorktreeHealth(stdout);
13195
13216
  }
13196
13217
  async function isDirtyWorktree(cwd) {
@@ -13199,7 +13220,9 @@ async function isDirtyWorktree(cwd) {
13199
13220
  }
13200
13221
  async function isBranchMergedInto(cwd, branch, base = "HEAD") {
13201
13222
  try {
13202
- await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], { cwd });
13223
+ await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
13224
+ cwd
13225
+ });
13203
13226
  return true;
13204
13227
  } catch (error) {
13205
13228
  if (hasExitCode(error, 1)) {
@@ -13209,7 +13232,11 @@ async function isBranchMergedInto(cwd, branch, base = "HEAD") {
13209
13232
  }
13210
13233
  }
13211
13234
  async function resolveRemoteDefaultBranch(cwd, remote) {
13212
- const { stdout } = await execFileAsync("git", ["ls-remote", "--symref", remote, "HEAD"], { cwd });
13235
+ const { stdout } = await execFileAsync(
13236
+ "git",
13237
+ ["ls-remote", "--symref", remote, "HEAD"],
13238
+ { cwd }
13239
+ );
13213
13240
  const refLine = stdout.split("\n").find((line) => line.startsWith("ref: refs/heads/"));
13214
13241
  if (!refLine) {
13215
13242
  return null;
@@ -13219,7 +13246,11 @@ async function resolveRemoteDefaultBranch(cwd, remote) {
13219
13246
  }
13220
13247
  async function readBranchLastCommitTimestamp(cwd, branch) {
13221
13248
  try {
13222
- const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%ct", branch], { cwd });
13249
+ const { stdout } = await execFileAsync(
13250
+ "git",
13251
+ ["log", "-1", "--format=%ct", branch],
13252
+ { cwd }
13253
+ );
13223
13254
  const timestamp = Number(stdout.trim());
13224
13255
  return Number.isFinite(timestamp) ? timestamp : null;
13225
13256
  } catch {
@@ -13283,7 +13314,9 @@ function resolveWorktreePath(repoRoot, branch, basePath) {
13283
13314
  throw new Error("Branch name must not be empty.");
13284
13315
  }
13285
13316
  if (segments.some((segment) => segment === "." || segment === "..")) {
13286
- throw new Error(`Branch name '${branch}' contains an invalid path segment.`);
13317
+ throw new Error(
13318
+ `Branch name '${branch}' contains an invalid path segment.`
13319
+ );
13287
13320
  }
13288
13321
  const base = basePath ? expandTildeInPath(basePath) : join3(dirname3(repoRoot), "worktrees", basename(repoRoot));
13289
13322
  return join3(base, ...segments);
@@ -13403,17 +13436,27 @@ async function runBackCommand(options) {
13403
13436
  }
13404
13437
  if (!target) {
13405
13438
  options.stderr("gji back: no previous worktree in history\n");
13406
- options.stderr("Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n");
13439
+ options.stderr(
13440
+ "Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n"
13441
+ );
13407
13442
  return 1;
13408
13443
  }
13409
13444
  try {
13410
13445
  const repository = await detectRepository(target.path);
13411
- const config = await loadEffectiveConfig(repository.repoRoot, options.home, options.stderr);
13446
+ const config = await loadEffectiveConfig(
13447
+ repository.repoRoot,
13448
+ options.home,
13449
+ options.stderr
13450
+ );
13412
13451
  const hooks = extractHooks(config);
13413
13452
  await runHook(
13414
13453
  hooks.afterEnter,
13415
13454
  target.path,
13416
- { branch: target.branch ?? void 0, path: target.path, repo: basename2(repository.repoRoot) },
13455
+ {
13456
+ branch: target.branch ?? void 0,
13457
+ path: target.path,
13458
+ repo: basename2(repository.repoRoot)
13459
+ },
13417
13460
  options.stderr
13418
13461
  );
13419
13462
  } catch {
@@ -13427,7 +13470,9 @@ function formatHistoryList(history, cwd) {
13427
13470
  "BRANCH".length,
13428
13471
  ...history.map((e2) => (e2.branch ?? "(detached)").length)
13429
13472
  );
13430
- const lines = [" " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"];
13473
+ const lines = [
13474
+ " " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"
13475
+ ];
13431
13476
  for (const entry of history) {
13432
13477
  const isCurrent = entry.path === cwd;
13433
13478
  const branch = (entry.branch ?? "(detached)").padEnd(branchWidth);
@@ -14121,7 +14166,10 @@ function formatLastCommit(timestampSeconds) {
14121
14166
  return timestampSeconds === null ? "n/a" : formatRelativeAge(timestampSeconds);
14122
14167
  }
14123
14168
  function formatRelativeAge(timestampSeconds) {
14124
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1e3) - timestampSeconds);
14169
+ const ageSeconds = Math.max(
14170
+ 0,
14171
+ Math.floor(Date.now() / 1e3) - timestampSeconds
14172
+ );
14125
14173
  const units = [
14126
14174
  { label: "y", seconds: 365 * 24 * 60 * 60 },
14127
14175
  { label: "mo", seconds: 30 * 24 * 60 * 60 },
@@ -14154,16 +14202,28 @@ async function loadLinkedWorktrees(cwd) {
14154
14202
  };
14155
14203
  }
14156
14204
  async function removeWorktree(repoRoot, worktreePath) {
14157
- await execFileAsync2("git", ["worktree", "remove", worktreePath], { cwd: repoRoot, env: GIT_ENV });
14205
+ await execFileAsync2("git", ["worktree", "remove", worktreePath], {
14206
+ cwd: repoRoot,
14207
+ env: GIT_ENV
14208
+ });
14158
14209
  }
14159
14210
  async function forceRemoveWorktree(repoRoot, worktreePath) {
14160
- await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, env: GIT_ENV });
14211
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
14212
+ cwd: repoRoot,
14213
+ env: GIT_ENV
14214
+ });
14161
14215
  }
14162
14216
  async function deleteBranch(repoRoot, branch) {
14163
- await execFileAsync2("git", ["branch", "-d", branch], { cwd: repoRoot, env: GIT_ENV });
14217
+ await execFileAsync2("git", ["branch", "-d", branch], {
14218
+ cwd: repoRoot,
14219
+ env: GIT_ENV
14220
+ });
14164
14221
  }
14165
14222
  async function forceDeleteBranch(repoRoot, branch) {
14166
- await execFileAsync2("git", ["branch", "-D", branch], { cwd: repoRoot, env: GIT_ENV });
14223
+ await execFileAsync2("git", ["branch", "-D", branch], {
14224
+ cwd: repoRoot,
14225
+ env: GIT_ENV
14226
+ });
14167
14227
  }
14168
14228
  function isWorktreeDirtyError(error) {
14169
14229
  return hasStderr(error) && error.stderr.includes("contains modified or untracked files");
@@ -14202,12 +14262,18 @@ function createCleanCommand(dependencies = {}) {
14202
14262
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
14203
14263
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
14204
14264
  return async function runCleanCommand2(options) {
14205
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
14265
+ const { linkedWorktrees, repository } = await loadLinkedWorktrees(
14266
+ options.cwd
14267
+ );
14206
14268
  const linkedCleanupCandidates = linkedWorktrees.filter(
14207
14269
  (worktree) => worktree.path !== repository.currentRoot
14208
14270
  );
14209
14271
  const staleBaseRef = options.stale ? await resolveStaleBaseRef(repository.repoRoot, options.stderr) : null;
14210
- const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(repository.repoRoot, linkedCleanupCandidates, staleBaseRef) : linkedCleanupCandidates;
14272
+ const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
14273
+ repository.repoRoot,
14274
+ linkedCleanupCandidates,
14275
+ staleBaseRef
14276
+ ) : linkedCleanupCandidates;
14211
14277
  if (cleanupCandidates.length === 0) {
14212
14278
  if (options.stale) {
14213
14279
  emitNoStaleCandidates(options);
@@ -14221,8 +14287,10 @@ function createCleanCommand(dependencies = {}) {
14221
14287
  if (options.json) {
14222
14288
  emitError(options, message);
14223
14289
  } else {
14224
- options.stderr(`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
14225
- `);
14290
+ options.stderr(
14291
+ `gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
14292
+ `
14293
+ );
14226
14294
  }
14227
14295
  return 1;
14228
14296
  }
@@ -14232,7 +14300,10 @@ function createCleanCommand(dependencies = {}) {
14232
14300
  options.stderr("Aborted\n");
14233
14301
  return 1;
14234
14302
  }
14235
- const selectedWorktrees = resolveSelectedWorktrees(cleanupCandidates, selections);
14303
+ const selectedWorktrees = resolveSelectedWorktrees(
14304
+ cleanupCandidates,
14305
+ selections
14306
+ );
14236
14307
  if (selectedWorktrees.length !== selections.length) {
14237
14308
  options.stderr("Selected worktree no longer exists\n");
14238
14309
  return 1;
@@ -14247,13 +14318,19 @@ function createCleanCommand(dependencies = {}) {
14247
14318
  }
14248
14319
  if (options.dryRun) {
14249
14320
  if (options.json) {
14250
- const removed = selectedWorktreeInfos.map((info) => serializeWorktreeInfo(info));
14251
- options.stdout(`${JSON.stringify({ removed, dryRun: true }, null, 2)}
14252
- `);
14321
+ const removed = selectedWorktreeInfos.map(
14322
+ (info) => serializeWorktreeInfo(info)
14323
+ );
14324
+ options.stdout(
14325
+ `${JSON.stringify({ removed, dryRun: true }, null, 2)}
14326
+ `
14327
+ );
14253
14328
  } else {
14254
14329
  for (const info of selectedWorktreeInfos) {
14255
- options.stdout(`Would remove worktree at ${info.path} (${formatCleanInfo(info)})
14256
- `);
14330
+ options.stdout(
14331
+ `Would remove worktree at ${info.path} (${formatCleanInfo(info)})
14332
+ `
14333
+ );
14257
14334
  }
14258
14335
  }
14259
14336
  return 0;
@@ -14261,9 +14338,15 @@ function createCleanCommand(dependencies = {}) {
14261
14338
  const removedPaths = [];
14262
14339
  const removedWorktrees = [];
14263
14340
  for (const worktree of selectedWorktrees) {
14264
- if (options.stale && !await isStaleCleanupCandidate(repository.repoRoot, worktree, staleBaseRef)) {
14265
- options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14266
- `);
14341
+ if (options.stale && !await isStaleCleanupCandidate(
14342
+ repository.repoRoot,
14343
+ worktree,
14344
+ staleBaseRef
14345
+ )) {
14346
+ options.stderr(
14347
+ `Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14348
+ `
14349
+ );
14267
14350
  continue;
14268
14351
  }
14269
14352
  try {
@@ -14273,8 +14356,10 @@ function createCleanCommand(dependencies = {}) {
14273
14356
  throw error;
14274
14357
  }
14275
14358
  if (options.stale) {
14276
- options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14277
- `);
14359
+ options.stderr(
14360
+ `Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14361
+ `
14362
+ );
14278
14363
  continue;
14279
14364
  }
14280
14365
  if (!options.force && !await confirmForceRemoveWorktree(worktree.path)) {
@@ -14288,7 +14373,10 @@ function createCleanCommand(dependencies = {}) {
14288
14373
  if (!options.json) {
14289
14374
  reportRemovedPaths(removedPaths, options.stderr);
14290
14375
  }
14291
- emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
14376
+ emitError(
14377
+ options,
14378
+ `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`
14379
+ );
14292
14380
  return 1;
14293
14381
  }
14294
14382
  }
@@ -14305,12 +14393,16 @@ function createCleanCommand(dependencies = {}) {
14305
14393
  try {
14306
14394
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
14307
14395
  } catch (forceError) {
14308
- options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
14309
- `);
14396
+ options.stderr(
14397
+ `Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
14398
+ `
14399
+ );
14310
14400
  }
14311
14401
  } else {
14312
- options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)
14313
- `);
14402
+ options.stderr(
14403
+ `Branch ${worktree.branch} was not deleted (has unmerged commits)
14404
+ `
14405
+ );
14314
14406
  }
14315
14407
  }
14316
14408
  }
@@ -14335,19 +14427,30 @@ async function filterStaleCleanupCandidates(repoRoot, worktrees, baseBranch) {
14335
14427
  return [];
14336
14428
  }
14337
14429
  const results = await Promise.all(
14338
- worktrees.map((worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch))
14430
+ worktrees.map(
14431
+ (worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch)
14432
+ )
14339
14433
  );
14340
14434
  return worktrees.filter((_3, index) => results[index]);
14341
14435
  }
14342
14436
  async function resolveStaleBaseRef(repoRoot, stderr) {
14343
14437
  const config = await loadEffectiveConfig(repoRoot, void 0, stderr);
14344
14438
  const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
14345
- const configuredDefaultBranch = resolveConfiguredString(config.syncDefaultBranch);
14439
+ const configuredDefaultBranch = resolveConfiguredString(
14440
+ config.syncDefaultBranch
14441
+ );
14346
14442
  if (configuredDefaultBranch) {
14347
- return await resolveFetchedRemoteRef(repoRoot, remote, configuredDefaultBranch);
14443
+ return await resolveFetchedRemoteRef(
14444
+ repoRoot,
14445
+ remote,
14446
+ configuredDefaultBranch
14447
+ );
14348
14448
  }
14349
14449
  try {
14350
- const remoteDefaultBranch = await resolveRemoteDefaultBranch(repoRoot, remote);
14450
+ const remoteDefaultBranch = await resolveRemoteDefaultBranch(
14451
+ repoRoot,
14452
+ remote
14453
+ );
14351
14454
  return remoteDefaultBranch === null ? null : await resolveFetchedRemoteRef(repoRoot, remote, remoteDefaultBranch);
14352
14455
  } catch {
14353
14456
  return null;
@@ -14442,14 +14545,22 @@ async function defaultPromptForWorktrees(worktrees) {
14442
14545
  return pD(choice) ? null : choice;
14443
14546
  }
14444
14547
  async function defaultConfirmRemoval(worktrees) {
14445
- const branchCount = worktrees.filter((worktree) => worktree.branch !== null).length;
14548
+ const branchCount = worktrees.filter(
14549
+ (worktree) => worktree.branch !== null
14550
+ ).length;
14446
14551
  const detachedCount = worktrees.length - branchCount;
14447
- const messageParts = [`Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`];
14552
+ const messageParts = [
14553
+ `Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`
14554
+ ];
14448
14555
  if (branchCount > 0) {
14449
- messageParts.push(`delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`);
14556
+ messageParts.push(
14557
+ `delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`
14558
+ );
14450
14559
  }
14451
14560
  if (detachedCount > 0) {
14452
- messageParts.push(`remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`);
14561
+ messageParts.push(
14562
+ `remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`
14563
+ );
14453
14564
  }
14454
14565
  const choice = await ye({
14455
14566
  active: "Yes",
@@ -14460,25 +14571,35 @@ async function defaultConfirmRemoval(worktrees) {
14460
14571
  return !pD(choice) && choice;
14461
14572
  }
14462
14573
 
14463
- // src/history-command.ts
14464
- async function runHistoryCommand(options) {
14465
- const history = await loadHistory(options.home);
14466
- if (options.json) {
14467
- options.stdout(`${JSON.stringify(history, null, 2)}
14468
- `);
14469
- return 0;
14574
+ // src/shell.ts
14575
+ function resolveSupportedShell(requestedShell, detectedShell) {
14576
+ const requested = normalizeShell(requestedShell);
14577
+ if (requested) {
14578
+ return requested;
14470
14579
  }
14471
- if (history.length === 0) {
14472
- options.stdout("No navigation history.\n");
14473
- return 0;
14580
+ return normalizeShell(detectedShell);
14581
+ }
14582
+ function normalizeShell(value) {
14583
+ if (!value) {
14584
+ return null;
14585
+ }
14586
+ const candidate = value.split("/").at(-1)?.toLowerCase();
14587
+ switch (candidate) {
14588
+ case "bash":
14589
+ case "fish":
14590
+ case "zsh":
14591
+ return candidate;
14592
+ default:
14593
+ return null;
14474
14594
  }
14475
- options.stdout(formatHistoryList(history, options.cwd));
14476
- return 0;
14477
14595
  }
14478
14596
 
14479
14597
  // src/shell-completion.ts
14480
14598
  var TOP_LEVEL_COMMANDS = [
14481
- { name: "new", description: "create a new branch or detached linked worktree" },
14599
+ {
14600
+ name: "new",
14601
+ description: "create a new branch or detached linked worktree"
14602
+ },
14482
14603
  { name: "init", description: "print or install shell integration" },
14483
14604
  { name: "completion", description: "print shell completion definitions" },
14484
14605
  { name: "pr", description: "fetch a pull request into a linked worktree" },
@@ -14490,11 +14611,21 @@ var TOP_LEVEL_COMMANDS = [
14490
14611
  { name: "root", description: "print the main repository root path" },
14491
14612
  { name: "status", description: "summarize repository and worktree health" },
14492
14613
  { name: "sync", description: "fetch and update one or all worktrees" },
14614
+ {
14615
+ name: "sync-files",
14616
+ description: "manage local files copied into new worktrees"
14617
+ },
14493
14618
  { name: "ls", description: "list active worktrees" },
14494
14619
  { name: "clean", description: "interactively prune linked worktrees" },
14495
- { name: "remove", description: "remove a linked worktree and delete its branch when present" },
14620
+ {
14621
+ name: "remove",
14622
+ description: "remove a linked worktree and delete its branch when present"
14623
+ },
14496
14624
  { name: "rm", description: "alias of remove" },
14497
- { name: "trigger-hook", description: "run a named hook in the current worktree" },
14625
+ {
14626
+ name: "trigger-hook",
14627
+ description: "run a named hook in the current worktree"
14628
+ },
14498
14629
  { name: "warp", description: "jump to any worktree across all known repos" },
14499
14630
  { name: "config", description: "manage global config defaults" }
14500
14631
  ];
@@ -14512,7 +14643,9 @@ function renderShellCompletion(shell) {
14512
14643
  }
14513
14644
  }
14514
14645
  function renderBashCompletion() {
14515
- const topLevelCommands = TOP_LEVEL_COMMANDS.map((command) => command.name).join(" ");
14646
+ const topLevelCommands = TOP_LEVEL_COMMANDS.map(
14647
+ (command) => command.name
14648
+ ).join(" ");
14516
14649
  const shells = SHELL_NAMES.join(" ");
14517
14650
  const hooks = HOOK_NAMES.join(" ");
14518
14651
  const configKeys = CONFIG_KEYS.join(" ");
@@ -14566,6 +14699,12 @@ _gji_completion() {
14566
14699
  sync)
14567
14700
  COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
14568
14701
  ;;
14702
+ sync-files)
14703
+ if [ "$COMP_CWORD" -eq 2 ]; then
14704
+ COMPREPLY=( $(compgen -W "list add remove rm --json --help" -- "$cur") )
14705
+ return 0
14706
+ fi
14707
+ ;;
14569
14708
  ls)
14570
14709
  COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
14571
14710
  ;;
@@ -14679,6 +14818,12 @@ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print reposi
14679
14818
  complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
14680
14819
  complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
14681
14820
 
14821
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -a 'list add remove rm' -d 'sync-files action'
14822
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
14823
+ complete -c gji -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
14824
+ complete -c gji -n '__fish_seen_subcommand_from add; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
14825
+ complete -c gji -n '__fish_seen_subcommand_from remove rm; and __fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
14826
+
14682
14827
  complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
14683
14828
  complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
14684
14829
 
@@ -14760,6 +14905,9 @@ case "\${words[2]}" in
14760
14905
  sync)
14761
14906
  _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14762
14907
  ;;
14908
+ sync-files)
14909
+ _arguments '--json[emit JSON instead of human-readable output]' '2:action:(list add remove rm)' '*:path: '
14910
+ ;;
14763
14911
  ls)
14764
14912
  _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14765
14913
  ;;
@@ -14806,29 +14954,6 @@ function escapeSingleQuotes(value) {
14806
14954
  return value.replace(/'/g, `'\\''`);
14807
14955
  }
14808
14956
 
14809
- // src/shell.ts
14810
- function resolveSupportedShell(requestedShell, detectedShell) {
14811
- const requested = normalizeShell(requestedShell);
14812
- if (requested) {
14813
- return requested;
14814
- }
14815
- return normalizeShell(detectedShell);
14816
- }
14817
- function normalizeShell(value) {
14818
- if (!value) {
14819
- return null;
14820
- }
14821
- const candidate = value.split("/").at(-1)?.toLowerCase();
14822
- switch (candidate) {
14823
- case "bash":
14824
- case "fish":
14825
- case "zsh":
14826
- return candidate;
14827
- default:
14828
- return null;
14829
- }
14830
- }
14831
-
14832
14957
  // src/completion.ts
14833
14958
  async function runCompletionCommand(options) {
14834
14959
  const shell = resolveSupportedShell(options.shell, process.env.SHELL);
@@ -14860,7 +14985,10 @@ async function runConfigCommand(options) {
14860
14985
  }
14861
14986
  case "set":
14862
14987
  if (options.key && options.value !== void 0) {
14863
- await updateGlobalConfigKey(options.key, parseConfigValue(options.value));
14988
+ await updateGlobalConfigKey(
14989
+ options.key,
14990
+ parseConfigValue(options.value)
14991
+ );
14864
14992
  return 0;
14865
14993
  }
14866
14994
  break;
@@ -14881,28 +15009,85 @@ function writeJson(stdout, value) {
14881
15009
  }
14882
15010
 
14883
15011
  // src/go.ts
14884
- import { basename as basename5 } from "node:path";
15012
+ import { basename as basename6 } from "node:path";
15013
+
15014
+ // src/warp.ts
15015
+ import { realpath as realpath2 } from "node:fs/promises";
15016
+ import { basename as basename5, resolve as resolve5 } from "node:path";
14885
15017
 
14886
15018
  // src/new.ts
15019
+ import { execFile as execFile3 } from "node:child_process";
14887
15020
  import { mkdir as mkdir4 } from "node:fs/promises";
14888
15021
  import { basename as basename3, dirname as dirname5 } from "node:path";
14889
- import { execFile as execFile3 } from "node:child_process";
14890
15022
  import { promisify as promisify4 } from "node:util";
14891
15023
 
15024
+ // src/conflict.ts
15025
+ import { constants } from "node:fs";
15026
+ import { access as access2 } from "node:fs/promises";
15027
+ async function pathExists(path9) {
15028
+ try {
15029
+ await access2(path9, constants.F_OK);
15030
+ return true;
15031
+ } catch {
15032
+ return false;
15033
+ }
15034
+ }
15035
+ async function promptForPathConflict(path9) {
15036
+ const choice = await ve({
15037
+ message: `Target path already exists: ${path9}`,
15038
+ options: [
15039
+ {
15040
+ value: "abort",
15041
+ label: "Abort",
15042
+ hint: "Keep the existing directory untouched"
15043
+ },
15044
+ {
15045
+ value: "reuse",
15046
+ label: "Reuse path",
15047
+ hint: "Print the existing path and stop"
15048
+ }
15049
+ ]
15050
+ });
15051
+ if (pD(choice)) {
15052
+ return "abort";
15053
+ }
15054
+ return choice;
15055
+ }
15056
+
14892
15057
  // src/editor.ts
14893
15058
  import { spawn as spawn3 } from "node:child_process";
14894
15059
  var EDITORS = [
14895
- { cli: "cursor", name: "Cursor", newWindowFlag: "--new-window", supportsWorkspace: true },
14896
- { cli: "code", name: "VS Code", newWindowFlag: "--new-window", supportsWorkspace: true },
14897
- { cli: "windsurf", name: "Windsurf", newWindowFlag: "--new-window", supportsWorkspace: true },
15060
+ {
15061
+ cli: "cursor",
15062
+ name: "Cursor",
15063
+ newWindowFlag: "--new-window",
15064
+ supportsWorkspace: true
15065
+ },
15066
+ {
15067
+ cli: "code",
15068
+ name: "VS Code",
15069
+ newWindowFlag: "--new-window",
15070
+ supportsWorkspace: true
15071
+ },
15072
+ {
15073
+ cli: "windsurf",
15074
+ name: "Windsurf",
15075
+ newWindowFlag: "--new-window",
15076
+ supportsWorkspace: true
15077
+ },
14898
15078
  { cli: "zed", name: "Zed", supportsWorkspace: false },
14899
- { cli: "subl", name: "Sublime Text", newWindowFlag: "--new-window", supportsWorkspace: false }
15079
+ {
15080
+ cli: "subl",
15081
+ name: "Sublime Text",
15082
+ newWindowFlag: "--new-window",
15083
+ supportsWorkspace: false
15084
+ }
14900
15085
  ];
14901
15086
  async function defaultSpawnEditor(cli, args) {
14902
15087
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
14903
- await new Promise((resolve5, reject) => {
15088
+ await new Promise((resolve6, reject) => {
14904
15089
  child.once("error", reject);
14905
- child.once("spawn", resolve5);
15090
+ child.once("spawn", resolve6);
14906
15091
  });
14907
15092
  child.unref();
14908
15093
  }
@@ -14912,13 +15097,7 @@ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
14912
15097
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
14913
15098
  async function syncFiles(mainRoot, targetPath, patterns) {
14914
15099
  for (const pattern of patterns) {
14915
- if (isAbsolute2(pattern)) {
14916
- throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
14917
- }
14918
- const normalized = normalize(pattern);
14919
- if (normalized.startsWith("..")) {
14920
- throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
14921
- }
15100
+ const normalized = validateSyncFilePattern(pattern);
14922
15101
  const sourcePath = join4(mainRoot, normalized);
14923
15102
  const destPath = join4(targetPath, normalized);
14924
15103
  const sourceExists = await fileExists(sourcePath);
@@ -14933,6 +15112,20 @@ async function syncFiles(mainRoot, targetPath, patterns) {
14933
15112
  await copyFile(sourcePath, destPath);
14934
15113
  }
14935
15114
  }
15115
+ function validateSyncFilePattern(pattern) {
15116
+ if (isAbsolute2(pattern)) {
15117
+ throw new Error(
15118
+ `syncFiles: pattern must be a relative path, got: ${pattern}`
15119
+ );
15120
+ }
15121
+ const normalized = normalize(pattern);
15122
+ if (normalized.startsWith("..")) {
15123
+ throw new Error(
15124
+ `syncFiles: pattern must not contain '..' segments, got: ${pattern}`
15125
+ );
15126
+ }
15127
+ return normalized;
15128
+ }
14936
15129
  async function fileExists(path9) {
14937
15130
  try {
14938
15131
  await stat(path9);
@@ -14952,7 +15145,7 @@ function isNotFoundError(error) {
14952
15145
  import { spawn as spawn4 } from "node:child_process";
14953
15146
 
14954
15147
  // src/package-manager.ts
14955
- import { access as access2, readdir } from "node:fs/promises";
15148
+ import { access as access3, readdir } from "node:fs/promises";
14956
15149
  import { join as join5 } from "node:path";
14957
15150
  var ENTRIES = [
14958
15151
  // JavaScript / TypeScript
@@ -14966,10 +15159,22 @@ var ENTRIES = [
14966
15159
  { name: "uv", signals: ["uv.lock"], command: "uv sync" },
14967
15160
  { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14968
15161
  { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
14969
- { name: "conda-lock", signals: ["conda-lock.yml"], command: "conda-lock install" },
14970
- { name: "conda", signals: ["environment.yml"], command: "conda env update --file environment.yml" },
15162
+ {
15163
+ name: "conda-lock",
15164
+ signals: ["conda-lock.yml"],
15165
+ command: "conda-lock install"
15166
+ },
15167
+ {
15168
+ name: "conda",
15169
+ signals: ["environment.yml"],
15170
+ command: "conda env update --file environment.yml"
15171
+ },
14971
15172
  // R
14972
- { name: "renv", signals: ["renv.lock"], command: "Rscript -e 'renv::restore()'" },
15173
+ {
15174
+ name: "renv",
15175
+ signals: ["renv.lock"],
15176
+ command: "Rscript -e 'renv::restore()'"
15177
+ },
14973
15178
  // Rust
14974
15179
  { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
14975
15180
  // Go
@@ -14986,25 +15191,56 @@ var ENTRIES = [
14986
15191
  // Java / Kotlin / Scala
14987
15192
  { name: "maven", signals: ["pom.xml"], command: "mvn install" },
14988
15193
  { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
14989
- { name: "gradle", signals: ["build.gradle", "build.gradle.kts"], command: "gradle build" },
15194
+ {
15195
+ name: "gradle",
15196
+ signals: ["build.gradle", "build.gradle.kts"],
15197
+ command: "gradle build"
15198
+ },
14990
15199
  { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
14991
15200
  // .NET (C# / F# / VB)
14992
- { name: "dotnet", signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"], command: "dotnet restore", glob: true },
15201
+ {
15202
+ name: "dotnet",
15203
+ signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
15204
+ command: "dotnet restore",
15205
+ glob: true
15206
+ },
14993
15207
  // Swift
14994
- { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
15208
+ {
15209
+ name: "swift",
15210
+ signals: ["Package.swift"],
15211
+ command: "swift package resolve"
15212
+ },
14995
15213
  // Haskell
14996
15214
  { name: "stack", signals: ["stack.yaml"], command: "stack build" },
14997
- { name: "cabal", signals: ["cabal.project"], command: "cabal install --only-dependencies" },
14998
- { name: "cabal", signals: ["*.cabal"], command: "cabal install --only-dependencies", glob: true },
15215
+ {
15216
+ name: "cabal",
15217
+ signals: ["cabal.project"],
15218
+ command: "cabal install --only-dependencies"
15219
+ },
15220
+ {
15221
+ name: "cabal",
15222
+ signals: ["*.cabal"],
15223
+ command: "cabal install --only-dependencies",
15224
+ glob: true
15225
+ },
14999
15226
  // Clojure
15000
15227
  { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
15001
15228
  { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
15002
15229
  // OCaml
15003
15230
  { name: "dune", signals: ["dune-project"], command: "dune build" },
15004
15231
  // Julia
15005
- { name: "julia", signals: ["Manifest.toml"], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
15232
+ {
15233
+ name: "julia",
15234
+ signals: ["Manifest.toml"],
15235
+ command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
15236
+ },
15006
15237
  // Nim
15007
- { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
15238
+ {
15239
+ name: "nimble",
15240
+ signals: ["*.nimble"],
15241
+ command: "nimble install",
15242
+ glob: true
15243
+ },
15008
15244
  // Crystal
15009
15245
  { name: "shards", signals: ["shard.yml"], command: "shards install" },
15010
15246
  // Perl
@@ -15013,12 +15249,20 @@ var ENTRIES = [
15013
15249
  { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
15014
15250
  // C / C++
15015
15251
  { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
15016
- { name: "conan", signals: ["conanfile.py", "conanfile.txt"], command: "conan install ." },
15252
+ {
15253
+ name: "conan",
15254
+ signals: ["conanfile.py", "conanfile.txt"],
15255
+ command: "conan install ."
15256
+ },
15017
15257
  // Nix
15018
15258
  { name: "nix", signals: ["flake.nix"], command: "nix develop" },
15019
15259
  { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
15020
15260
  // Terraform / OpenTofu
15021
- { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
15261
+ {
15262
+ name: "terraform",
15263
+ signals: ["terraform.lock.hcl"],
15264
+ command: "terraform init"
15265
+ }
15022
15266
  ];
15023
15267
  async function detectPackageManager(repoRoot) {
15024
15268
  for (const entry of ENTRIES) {
@@ -15032,7 +15276,7 @@ async function detectPackageManager(repoRoot) {
15032
15276
  async function matchesExact(repoRoot, signals) {
15033
15277
  for (const signal of signals) {
15034
15278
  try {
15035
- await access2(join5(repoRoot, signal));
15279
+ await access3(join5(repoRoot, signal));
15036
15280
  return true;
15037
15281
  } catch {
15038
15282
  }
@@ -15081,8 +15325,10 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15081
15325
  try {
15082
15326
  await runner(pm.installCommand, worktreePath, stderr);
15083
15327
  } catch (error) {
15084
- stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15085
- `);
15328
+ stderr(
15329
+ `gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15330
+ `
15331
+ );
15086
15332
  }
15087
15333
  }
15088
15334
  const saveGlobal = config.installSaveTarget === "global";
@@ -15092,15 +15338,23 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15092
15338
  try {
15093
15339
  if (saveGlobal) {
15094
15340
  const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15095
- await writeGlobalKey(repoRoot, "hooks", { ...existingHooks, afterCreate: pm.installCommand });
15341
+ await writeGlobalKey(repoRoot, "hooks", {
15342
+ ...existingHooks,
15343
+ afterCreate: pm.installCommand
15344
+ });
15096
15345
  } else {
15097
15346
  const { config: localConfig } = await loadConfig(repoRoot);
15098
15347
  const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15099
- await writeKey(repoRoot, "hooks", { ...existingLocalHooks, afterCreate: pm.installCommand });
15348
+ await writeKey(repoRoot, "hooks", {
15349
+ ...existingLocalHooks,
15350
+ afterCreate: pm.installCommand
15351
+ });
15100
15352
  }
15101
15353
  } catch (error) {
15102
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15103
- `);
15354
+ stderr(
15355
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15356
+ `
15357
+ );
15104
15358
  }
15105
15359
  }
15106
15360
  if (choice === "never") {
@@ -15111,14 +15365,20 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15111
15365
  await writeKey(repoRoot, "skipInstallPrompt", true);
15112
15366
  }
15113
15367
  } catch (error) {
15114
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15115
- `);
15368
+ stderr(
15369
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15370
+ `
15371
+ );
15116
15372
  }
15117
15373
  }
15118
15374
  }
15119
15375
  async function defaultRunInstallCommand(command, cwd, stderr) {
15120
- await new Promise((resolve5, reject) => {
15121
- const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15376
+ await new Promise((resolve6, reject) => {
15377
+ const child = spawn4(command, {
15378
+ cwd,
15379
+ shell: true,
15380
+ stdio: ["ignore", "inherit", "pipe"]
15381
+ });
15122
15382
  child.stderr.on("data", (chunk) => {
15123
15383
  stderr(chunk.toString());
15124
15384
  });
@@ -15126,7 +15386,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
15126
15386
  if (code !== 0) {
15127
15387
  reject(new Error(`exited with code ${code}`));
15128
15388
  } else {
15129
- resolve5();
15389
+ resolve6();
15130
15390
  }
15131
15391
  });
15132
15392
  child.on("error", (err) => {
@@ -15153,7 +15413,11 @@ async function defaultPromptForInstallChoice(pm) {
15153
15413
  { value: "yes", label: "Yes", hint: "run once" },
15154
15414
  { value: "no", label: "No", hint: "skip this time" },
15155
15415
  { value: "always", label: "Always", hint: "save as afterCreate hook" },
15156
- { value: "never", label: "Never", hint: "disable this prompt for this repo" }
15416
+ {
15417
+ value: "never",
15418
+ label: "Never",
15419
+ hint: "disable this prompt for this repo"
15420
+ }
15157
15421
  ]
15158
15422
  });
15159
15423
  if (pD(choice)) {
@@ -15169,31 +15433,6 @@ function isConfiguredHookCommand(value) {
15169
15433
  return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15170
15434
  }
15171
15435
 
15172
- // src/conflict.ts
15173
- import { access as access3 } from "node:fs/promises";
15174
- import { constants } from "node:fs";
15175
- async function pathExists(path9) {
15176
- try {
15177
- await access3(path9, constants.F_OK);
15178
- return true;
15179
- } catch {
15180
- return false;
15181
- }
15182
- }
15183
- async function promptForPathConflict(path9) {
15184
- const choice = await ve({
15185
- message: `Target path already exists: ${path9}`,
15186
- options: [
15187
- { value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
15188
- { value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
15189
- ]
15190
- });
15191
- if (pD(choice)) {
15192
- return "abort";
15193
- }
15194
- return choice;
15195
- }
15196
-
15197
15436
  // src/new.ts
15198
15437
  var execFileAsync3 = promisify4(execFile3);
15199
15438
  var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
@@ -15204,7 +15443,11 @@ function createNewCommand(dependencies = {}) {
15204
15443
  const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15205
15444
  return async function runNewCommand2(options) {
15206
15445
  const repository = await detectRepository(options.cwd);
15207
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15446
+ const config = await loadEffectiveConfig(
15447
+ repository.repoRoot,
15448
+ void 0,
15449
+ options.stderr
15450
+ );
15208
15451
  const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15209
15452
  if (options.editor && !options.open) {
15210
15453
  options.stderr("gji new: --editor has no effect without --open\n");
@@ -15215,8 +15458,10 @@ function createNewCommand(dependencies = {}) {
15215
15458
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15216
15459
  `);
15217
15460
  } else {
15218
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15219
- `);
15461
+ options.stderr(
15462
+ `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15463
+ `
15464
+ );
15220
15465
  }
15221
15466
  return 1;
15222
15467
  }
@@ -15234,8 +15479,10 @@ function createNewCommand(dependencies = {}) {
15234
15479
  const branchError = validateBranchName(rawBranch);
15235
15480
  if (branchError) {
15236
15481
  if (options.json) {
15237
- options.stderr(`${JSON.stringify({ error: branchError }, null, 2)}
15238
- `);
15482
+ options.stderr(
15483
+ `${JSON.stringify({ error: branchError }, null, 2)}
15484
+ `
15485
+ );
15239
15486
  } else {
15240
15487
  options.stderr(`gji new: ${branchError}
15241
15488
  `);
@@ -15246,18 +15493,32 @@ function createNewCommand(dependencies = {}) {
15246
15493
  const rawBasePath = resolveConfigString(config, "worktreePath");
15247
15494
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
15248
15495
  const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
15249
- const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath) : resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
15496
+ const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
15497
+ repository.repoRoot,
15498
+ worktreeName,
15499
+ configuredBasePath
15500
+ ) : resolveWorktreePath(
15501
+ repository.repoRoot,
15502
+ worktreeName,
15503
+ configuredBasePath
15504
+ );
15250
15505
  if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
15251
15506
  if (options.force) {
15252
15507
  if (!options.dryRun) {
15253
15508
  try {
15254
- await execFileAsync3("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
15509
+ await execFileAsync3(
15510
+ "git",
15511
+ ["worktree", "remove", "--force", worktreePath],
15512
+ { cwd: repository.repoRoot }
15513
+ );
15255
15514
  } catch (err) {
15256
15515
  if (!isNotRegisteredWorktreeError(err)) {
15257
15516
  const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
15258
15517
  if (options.json) {
15259
- options.stderr(`${JSON.stringify({ warning: msg }, null, 2)}
15260
- `);
15518
+ options.stderr(
15519
+ `${JSON.stringify({ warning: msg }, null, 2)}
15520
+ `
15521
+ );
15261
15522
  } else {
15262
15523
  options.stderr(`Warning: ${msg}
15263
15524
  `);
@@ -15266,7 +15527,9 @@ function createNewCommand(dependencies = {}) {
15266
15527
  }
15267
15528
  if (!options.detached) {
15268
15529
  try {
15269
- await execFileAsync3("git", ["branch", "-D", worktreeName], { cwd: repository.repoRoot });
15530
+ await execFileAsync3("git", ["branch", "-D", worktreeName], {
15531
+ cwd: repository.repoRoot
15532
+ });
15270
15533
  } catch {
15271
15534
  }
15272
15535
  }
@@ -15277,12 +15540,18 @@ function createNewCommand(dependencies = {}) {
15277
15540
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15278
15541
  `);
15279
15542
  } else {
15280
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15281
- `);
15282
- options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15283
- `);
15284
- options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15285
- `);
15543
+ options.stderr(
15544
+ `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15545
+ `
15546
+ );
15547
+ options.stderr(
15548
+ `Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15549
+ `
15550
+ );
15551
+ options.stderr(
15552
+ `Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15553
+ `
15554
+ );
15286
15555
  }
15287
15556
  return 1;
15288
15557
  } else {
@@ -15292,53 +15561,81 @@ function createNewCommand(dependencies = {}) {
15292
15561
  await writeOutput(worktreePath, options.stdout);
15293
15562
  return 0;
15294
15563
  }
15295
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
15296
- `);
15564
+ options.stderr(
15565
+ `Aborted because target worktree path already exists: ${worktreePath}
15566
+ `
15567
+ );
15297
15568
  return 1;
15298
15569
  }
15299
15570
  }
15300
15571
  if (options.dryRun) {
15301
15572
  if (options.json) {
15302
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15303
- `);
15573
+ options.stdout(
15574
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15575
+ `
15576
+ );
15304
15577
  } else {
15305
15578
  const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
15306
15579
  const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
15307
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15308
- `);
15580
+ options.stdout(
15581
+ `Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15582
+ `
15583
+ );
15309
15584
  }
15310
15585
  return 0;
15311
15586
  }
15312
15587
  await mkdir4(dirname5(worktreePath), { recursive: true });
15313
15588
  const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15314
15589
  await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15315
- const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15590
+ const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
15591
+ (p2) => typeof p2 === "string"
15592
+ ) : [];
15316
15593
  for (const pattern of syncPatterns) {
15317
15594
  try {
15318
15595
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
15319
15596
  } catch (error) {
15320
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15321
- `);
15597
+ options.stderr(
15598
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15599
+ `
15600
+ );
15322
15601
  }
15323
15602
  }
15324
- await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
15603
+ await maybeRunInstallPrompt(
15604
+ worktreePath,
15605
+ repository.repoRoot,
15606
+ config,
15607
+ options.stderr,
15608
+ dependencies,
15609
+ !!options.json
15610
+ );
15325
15611
  const hooks = extractHooks(config);
15326
15612
  await runHook(
15327
15613
  hooks.afterCreate,
15328
15614
  worktreePath,
15329
- { branch: worktreeName, path: worktreePath, repo: basename3(repository.repoRoot) },
15615
+ {
15616
+ branch: worktreeName,
15617
+ path: worktreePath,
15618
+ repo: basename3(repository.repoRoot)
15619
+ },
15330
15620
  options.stderr
15331
15621
  );
15332
15622
  if (options.json) {
15333
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15334
- `);
15623
+ options.stdout(
15624
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15625
+ `
15626
+ );
15335
15627
  } else {
15336
15628
  await appendHistory(worktreePath, worktreeName);
15337
15629
  await writeOutput(worktreePath, options.stdout);
15338
15630
  }
15339
15631
  if (options.open) {
15340
15632
  const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
15341
- await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
15633
+ await openWorktree(
15634
+ worktreePath,
15635
+ resolvedEditor,
15636
+ spawnEditor,
15637
+ options.stderr
15638
+ );
15342
15639
  }
15343
15640
  return 0;
15344
15641
  };
@@ -15399,7 +15696,11 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15399
15696
  let attempt = 1;
15400
15697
  while (true) {
15401
15698
  const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15402
- const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15699
+ const candidatePath = resolveWorktreePath(
15700
+ repoRoot,
15701
+ candidateName,
15702
+ basePath
15703
+ );
15403
15704
  if (!await pathExists(candidatePath)) {
15404
15705
  return candidatePath;
15405
15706
  }
@@ -15427,7 +15728,11 @@ function pickRandom(values, random) {
15427
15728
  }
15428
15729
  async function localBranchExists(repoRoot, branchName) {
15429
15730
  try {
15430
- await execFileAsync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
15731
+ await execFileAsync3(
15732
+ "git",
15733
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
15734
+ { cwd: repoRoot }
15735
+ );
15431
15736
  return true;
15432
15737
  } catch {
15433
15738
  return false;
@@ -15448,7 +15753,9 @@ function toExecMessage(error) {
15448
15753
  }
15449
15754
  async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15450
15755
  if (!editorCli) {
15451
- stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
15756
+ stderr(
15757
+ "gji new: --open requires --editor <cli> or a saved editor in config\n"
15758
+ );
15452
15759
  return;
15453
15760
  }
15454
15761
  const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
@@ -15467,7 +15774,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15467
15774
  }
15468
15775
 
15469
15776
  // src/repo-registry.ts
15470
- import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
15777
+ import { mkdir as mkdir5, readFile as readFile3, realpath, writeFile as writeFile4 } from "node:fs/promises";
15471
15778
  import { homedir as homedir4 } from "node:os";
15472
15779
  import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
15473
15780
  var REGISTRY_FILE_NAME = "repos.json";
@@ -15490,21 +15797,46 @@ async function loadRegistry(home = homedir4()) {
15490
15797
  return [];
15491
15798
  }
15492
15799
  }
15800
+ async function canonicalizeRepoPath(repoPath) {
15801
+ try {
15802
+ return await realpath(repoPath);
15803
+ } catch {
15804
+ return resolve4(repoPath);
15805
+ }
15806
+ }
15493
15807
  async function registerRepo(repoPath, home = homedir4()) {
15494
15808
  const registryPath = REGISTRY_FILE_PATH(home);
15495
- const existing = await loadRegistry(home);
15496
- if (existing.length > 0 && existing[0].path === repoPath) return;
15809
+ const existing = await normalizeRegistryForWrite(await loadRegistry(home));
15810
+ const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
15811
+ if (existing.length > 0 && existing[0].path === canonicalRepoPath) return;
15497
15812
  const entry = {
15498
15813
  lastUsed: Date.now(),
15499
- name: basename4(repoPath),
15500
- path: repoPath
15814
+ name: basename4(canonicalRepoPath),
15815
+ path: canonicalRepoPath
15501
15816
  };
15502
- const filtered = existing.filter((e2) => e2.path !== repoPath);
15817
+ const filtered = existing.filter((e2) => e2.path !== canonicalRepoPath);
15503
15818
  const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
15504
15819
  await mkdir5(dirname6(registryPath), { recursive: true });
15505
15820
  await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
15506
15821
  `, "utf8");
15507
15822
  }
15823
+ async function normalizeRegistryForWrite(entries) {
15824
+ const normalized = [];
15825
+ const seenPaths = /* @__PURE__ */ new Set();
15826
+ for (const entry of entries) {
15827
+ const canonicalPath = await canonicalizeRepoPath(entry.path);
15828
+ if (seenPaths.has(canonicalPath)) {
15829
+ continue;
15830
+ }
15831
+ seenPaths.add(canonicalPath);
15832
+ normalized.push({
15833
+ ...entry,
15834
+ name: basename4(canonicalPath),
15835
+ path: canonicalPath
15836
+ });
15837
+ }
15838
+ return normalized;
15839
+ }
15508
15840
  function isRegistryEntry(value) {
15509
15841
  return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "name" in value && typeof value.name === "string" && "lastUsed" in value && typeof value.lastUsed === "number";
15510
15842
  }
@@ -15537,11 +15869,17 @@ async function runWarpNavigate(options) {
15537
15869
  }
15538
15870
  return 1;
15539
15871
  }
15540
- const target = await resolveWarpTarget({ ...options, commandName: "gji warp", json: options.json });
15872
+ const target = await resolveWarpTarget({
15873
+ ...options,
15874
+ commandName: "gji warp",
15875
+ json: options.json
15876
+ });
15541
15877
  if (!target) return 1;
15542
15878
  if (options.json) {
15543
- options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15544
- `);
15879
+ options.stdout(
15880
+ `${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15881
+ `
15882
+ );
15545
15883
  return 0;
15546
15884
  }
15547
15885
  appendHistory(target.path, target.branch).catch(() => void 0);
@@ -15549,9 +15887,10 @@ async function runWarpNavigate(options) {
15549
15887
  return 0;
15550
15888
  }
15551
15889
  async function runWarpNew(options, registry) {
15890
+ const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
15552
15891
  let targetRepoRoot;
15553
- if (registry.length === 1) {
15554
- targetRepoRoot = registry[0].path;
15892
+ if (deduplicatedRegistry.length === 1) {
15893
+ targetRepoRoot = deduplicatedRegistry[0].path;
15555
15894
  } else {
15556
15895
  if (isHeadless()) {
15557
15896
  options.stderr(
@@ -15561,7 +15900,7 @@ async function runWarpNew(options, registry) {
15561
15900
  }
15562
15901
  const choice = await ve({
15563
15902
  message: "Create worktree in which repo?",
15564
- options: registry.map((entry) => ({
15903
+ options: deduplicatedRegistry.map((entry) => ({
15565
15904
  value: entry.path,
15566
15905
  label: entry.name,
15567
15906
  hint: entry.path
@@ -15602,6 +15941,30 @@ async function runWarpNew(options, registry) {
15602
15941
  await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
15603
15942
  return 0;
15604
15943
  }
15944
+ async function deduplicateRegistryForNew(registry) {
15945
+ const deduplicated = [];
15946
+ const seenPaths = /* @__PURE__ */ new Set();
15947
+ for (const entry of registry) {
15948
+ const canonicalPath = await canonicalizeRepoPath2(entry.path);
15949
+ if (seenPaths.has(canonicalPath)) {
15950
+ continue;
15951
+ }
15952
+ seenPaths.add(canonicalPath);
15953
+ deduplicated.push({
15954
+ ...entry,
15955
+ name: basename5(canonicalPath),
15956
+ path: canonicalPath
15957
+ });
15958
+ }
15959
+ return deduplicated;
15960
+ }
15961
+ async function canonicalizeRepoPath2(repoPath) {
15962
+ try {
15963
+ return await realpath2(repoPath);
15964
+ } catch {
15965
+ return resolve5(repoPath);
15966
+ }
15967
+ }
15605
15968
  function findByQuery(items, query) {
15606
15969
  const slashIdx = query.indexOf("/");
15607
15970
  if (slashIdx !== -1) {
@@ -15717,14 +16080,19 @@ function createGoCommand(dependencies = {}) {
15717
16080
  );
15718
16081
  return 1;
15719
16082
  }
15720
- const target = await resolveWarpTarget({ ...options, commandName: "gji go" });
16083
+ const target = await resolveWarpTarget({
16084
+ ...options,
16085
+ commandName: "gji go"
16086
+ });
15721
16087
  if (!target) return 1;
15722
16088
  appendHistory(target.path, target.branch).catch(() => void 0);
15723
16089
  await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
15724
16090
  return 0;
15725
16091
  }
15726
16092
  if (!options.branch && isHeadless()) {
15727
- options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
16093
+ options.stderr(
16094
+ "gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
16095
+ );
15728
16096
  return 1;
15729
16097
  }
15730
16098
  const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
@@ -15740,204 +16108,83 @@ function createGoCommand(dependencies = {}) {
15740
16108
  }
15741
16109
  return 1;
15742
16110
  }
15743
- const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
15744
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15745
- const hooks = extractHooks(config);
15746
- await runHook(
15747
- hooks.afterEnter,
15748
- resolvedPath,
15749
- { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename5(repository.repoRoot) },
15750
- options.stderr
15751
- );
15752
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
15753
- await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15754
- return 0;
15755
- };
15756
- }
15757
- var runGoCommand = createGoCommand();
15758
- async function promptForWorktree(worktrees) {
15759
- const healthResults = await Promise.allSettled(
15760
- worktrees.map((w2) => readWorktreeHealth(w2.path))
15761
- );
15762
- const choice = await ve({
15763
- message: "Choose a worktree",
15764
- options: worktrees.map((worktree, i) => {
15765
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15766
- const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
15767
- const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
15768
- return {
15769
- value: worktree.path,
15770
- label: worktree.branch ?? "(detached)",
15771
- hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
15772
- };
15773
- })
15774
- });
15775
- if (pD(choice)) {
15776
- return null;
15777
- }
15778
- return choice;
15779
- }
15780
- function formatUpstreamHint(branch, health) {
15781
- if (branch === null) return null;
15782
- if (!health.hasUpstream) return "no upstream";
15783
- if (health.upstreamGone) return "upstream gone";
15784
- if (health.ahead === 0 && health.behind === 0) return "up to date";
15785
- if (health.ahead === 0) return `behind ${health.behind}`;
15786
- if (health.behind === 0) return `ahead ${health.ahead}`;
15787
- return `ahead ${health.ahead}, behind ${health.behind}`;
15788
- }
15789
-
15790
- // src/open.ts
15791
- import { execFile as execFile4 } from "node:child_process";
15792
- import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
15793
- import { join as join7 } from "node:path";
15794
- import { promisify as promisify5 } from "node:util";
15795
- var execFileAsync4 = promisify5(execFile4);
15796
- function createOpenCommand(dependencies = {}) {
15797
- const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
15798
- const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
15799
- const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
15800
- const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15801
- return async function runOpenCommand2(options) {
15802
- const [worktrees, repository] = await Promise.all([
15803
- listWorktrees(options.cwd),
15804
- detectRepository(options.cwd)
15805
- ]);
15806
- let targetPath;
15807
- if (options.branch) {
15808
- const entry = worktrees.find((w2) => w2.branch === options.branch);
15809
- if (!entry) {
15810
- options.stderr(`gji open: no worktree found for branch: ${options.branch}
15811
- `);
15812
- options.stderr(`Hint: Use 'gji ls' to see available worktrees
15813
- `);
15814
- return 1;
15815
- }
15816
- targetPath = entry.path;
15817
- } else if (isHeadless()) {
15818
- targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
15819
- } else {
15820
- const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
15821
- if (!chosen) {
15822
- options.stderr("Aborted\n");
15823
- return 1;
15824
- }
15825
- targetPath = chosen;
15826
- }
15827
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15828
- const savedEditor = resolveConfigString(config, "editor");
15829
- let editorCli;
15830
- if (options.editor) {
15831
- editorCli = options.editor;
15832
- } else if (savedEditor) {
15833
- editorCli = savedEditor;
15834
- } else {
15835
- const installed = await detectEditors();
15836
- if (installed.length === 0) {
15837
- options.stderr(
15838
- "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
15839
- );
15840
- return 1;
15841
- }
15842
- if (installed.length === 1 || isHeadless()) {
15843
- editorCli = installed[0].cli;
15844
- } else {
15845
- const chosen = await promptForEditor(installed);
15846
- if (!chosen) {
15847
- options.stderr("Aborted\n");
15848
- return 1;
15849
- }
15850
- editorCli = chosen;
15851
- }
15852
- }
15853
- if (options.save && editorCli !== savedEditor) {
15854
- await updateGlobalConfigKey("editor", editorCli);
15855
- const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
15856
- options.stdout(`Saved editor "${displayName2}" to global config
15857
- `);
15858
- }
15859
- const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15860
- let openTarget = targetPath;
15861
- if (options.workspace) {
15862
- if (editorDef?.supportsWorkspace) {
15863
- openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
15864
- } else {
15865
- const displayName2 = editorDef?.name ?? editorCli;
15866
- options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
15867
- `);
15868
- }
15869
- }
15870
- const args = [];
15871
- if (editorDef?.newWindowFlag) {
15872
- args.push(editorDef.newWindowFlag);
15873
- }
15874
- args.push(openTarget);
15875
- try {
15876
- await spawnEditor(editorCli, args);
15877
- } catch (error) {
15878
- const message = error instanceof Error ? error.message : String(error);
15879
- options.stderr(`gji open: failed to launch editor: ${message}
15880
- `);
15881
- return 1;
15882
- }
15883
- const displayName = editorDef?.name ?? editorCli;
15884
- options.stdout(`Opened ${targetPath} in ${displayName}
15885
- `);
16111
+ const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
16112
+ const config = await loadEffectiveConfig(
16113
+ repository.repoRoot,
16114
+ void 0,
16115
+ options.stderr
16116
+ );
16117
+ const hooks = extractHooks(config);
16118
+ await runHook(
16119
+ hooks.afterEnter,
16120
+ resolvedPath,
16121
+ {
16122
+ branch: chosenWorktree?.branch ?? void 0,
16123
+ path: resolvedPath,
16124
+ repo: basename6(repository.repoRoot)
16125
+ },
16126
+ options.stderr
16127
+ );
16128
+ appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
16129
+ () => void 0
16130
+ );
16131
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15886
16132
  return 0;
15887
16133
  };
15888
16134
  }
15889
- var runOpenCommand = createOpenCommand();
15890
- async function detectInstalledEditors() {
15891
- const results = await Promise.all(
15892
- EDITORS.map(async (editor) => ({ editor, available: await isCommandAvailable(editor.cli) }))
16135
+ var runGoCommand = createGoCommand();
16136
+ async function promptForWorktree(worktrees) {
16137
+ const healthResults = await Promise.allSettled(
16138
+ worktrees.map((w2) => readWorktreeHealth(w2.path))
15893
16139
  );
15894
- return results.filter((r2) => r2.available).map((r2) => r2.editor);
15895
- }
15896
- async function isCommandAvailable(command) {
15897
- try {
15898
- await execFileAsync4("which", [command]);
15899
- return true;
15900
- } catch {
15901
- return false;
15902
- }
15903
- }
15904
- async function defaultPromptForWorktree(worktrees) {
15905
16140
  const choice = await ve({
15906
- message: "Choose a worktree to open",
15907
- options: worktrees.map((w2) => ({
15908
- value: w2.path,
15909
- label: w2.branch ?? "(detached)",
15910
- hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
15911
- }))
16141
+ message: "Choose a worktree",
16142
+ options: worktrees.map((worktree, i) => {
16143
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
16144
+ const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
16145
+ const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
16146
+ return {
16147
+ value: worktree.path,
16148
+ label: worktree.branch ?? "(detached)",
16149
+ hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
16150
+ };
16151
+ })
15912
16152
  });
15913
- if (pD(choice)) return null;
16153
+ if (pD(choice)) {
16154
+ return null;
16155
+ }
15914
16156
  return choice;
15915
16157
  }
15916
- async function defaultPromptForEditor(editors) {
15917
- const choice = await ve({
15918
- message: "Choose an editor",
15919
- options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
15920
- });
15921
- if (pD(choice)) return null;
15922
- return choice;
16158
+ function formatUpstreamHint(branch, health) {
16159
+ if (branch === null) return null;
16160
+ if (!health.hasUpstream) return "no upstream";
16161
+ if (health.upstreamGone) return "upstream gone";
16162
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
16163
+ if (health.ahead === 0) return `behind ${health.behind}`;
16164
+ if (health.behind === 0) return `ahead ${health.ahead}`;
16165
+ return `ahead ${health.ahead}, behind ${health.behind}`;
15923
16166
  }
15924
- async function ensureWorkspaceFile(worktreePath, repoName) {
15925
- const workspacePath = join7(worktreePath, `${repoName}.code-workspace`);
15926
- try {
15927
- await access4(workspacePath);
15928
- return workspacePath;
15929
- } catch {
16167
+
16168
+ // src/history-command.ts
16169
+ async function runHistoryCommand(options) {
16170
+ const history = await loadHistory(options.home);
16171
+ if (options.json) {
16172
+ options.stdout(`${JSON.stringify(history, null, 2)}
16173
+ `);
16174
+ return 0;
15930
16175
  }
15931
- const workspace = { folders: [{ path: "." }], settings: {} };
15932
- await writeFile5(workspacePath, `${JSON.stringify(workspace, null, 2)}
15933
- `, "utf8");
15934
- return workspacePath;
16176
+ if (history.length === 0) {
16177
+ options.stdout("No navigation history.\n");
16178
+ return 0;
16179
+ }
16180
+ options.stdout(formatHistoryList(history, options.cwd));
16181
+ return 0;
15935
16182
  }
15936
16183
 
15937
16184
  // src/init.ts
15938
- import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile6 } from "node:fs/promises";
16185
+ import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
15939
16186
  import { homedir as homedir5 } from "node:os";
15940
- import { dirname as dirname7, join as join8 } from "node:path";
16187
+ import { dirname as dirname7, join as join7 } from "node:path";
15941
16188
  var START_MARKER = "# >>> gji init >>>";
15942
16189
  var END_MARKER = "# <<< gji init <<<";
15943
16190
  var SHELL_WRAPPED_COMMANDS = [
@@ -16009,7 +16256,7 @@ async function runInitCommand(options) {
16009
16256
  await mkdir6(dirname7(rcPath), { recursive: true });
16010
16257
  const current = await readExistingConfig(rcPath);
16011
16258
  const next = upsertShellIntegration(current, script);
16012
- await writeFile6(rcPath, next, "utf8");
16259
+ await writeFile5(rcPath, next, "utf8");
16013
16260
  options.stdout(`${rcPath}
16014
16261
  `);
16015
16262
  const { config: globalConfig } = await loadGlobalConfig(home);
@@ -16020,7 +16267,11 @@ async function runInitCommand(options) {
16020
16267
  const prompt = options.promptForSetup ?? defaultPromptForSetup;
16021
16268
  const result = await prompt();
16022
16269
  if (result) {
16023
- await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
16270
+ await updateGlobalConfigKey(
16271
+ "installSaveTarget",
16272
+ result.installSaveTarget,
16273
+ home
16274
+ );
16024
16275
  await saveWizardConfig(result, options.cwd, home);
16025
16276
  }
16026
16277
  }
@@ -16080,7 +16331,8 @@ async function saveWizardConfig(result, cwd, home) {
16080
16331
  const hooks = {};
16081
16332
  if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16082
16333
  if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16083
- if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
16334
+ if (result.hooks?.beforeRemove)
16335
+ hooks.beforeRemove = result.hooks.beforeRemove;
16084
16336
  if (Object.keys(hooks).length > 0) values.hooks = hooks;
16085
16337
  if (Object.keys(values).length === 0) return;
16086
16338
  if (result.installSaveTarget === "local") {
@@ -16094,11 +16346,11 @@ async function saveWizardConfig(result, cwd, home) {
16094
16346
  function resolveShellConfigPath(shell, home) {
16095
16347
  switch (shell) {
16096
16348
  case "bash":
16097
- return join8(home, ".bashrc");
16349
+ return join7(home, ".bashrc");
16098
16350
  case "fish":
16099
- return join8(home, ".config", "fish", "config.fish");
16351
+ return join7(home, ".config", "fish", "config.fish");
16100
16352
  case "zsh":
16101
- return join8(home, ".zshrc");
16353
+ return join7(home, ".zshrc");
16102
16354
  }
16103
16355
  }
16104
16356
  async function readExistingConfig(path9) {
@@ -16124,7 +16376,9 @@ function isMissingFileError2(error) {
16124
16376
  function renderFishWrapper(command) {
16125
16377
  const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
16126
16378
  const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
16127
- const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
16379
+ const bypassTests = command.bypassOptions.map(
16380
+ (opt) => `test $argv[1] = ${opt}`
16381
+ );
16128
16382
  const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
16129
16383
  return `if test (count $argv) -gt 0; and ${nameCondition}
16130
16384
  set -e argv[1]
@@ -16176,8 +16430,16 @@ async function defaultPromptForSetup() {
16176
16430
  const installSaveTarget = await ve({
16177
16431
  message: "Where should preferences be saved?",
16178
16432
  options: [
16179
- { value: "global", label: "~/.config/gji/config.json", hint: "personal \u2014 never committed" },
16180
- { value: "local", label: ".gji.json", hint: "repo \u2014 committed with the project" }
16433
+ {
16434
+ value: "global",
16435
+ label: "~/.config/gji/config.json",
16436
+ hint: "personal \u2014 never committed"
16437
+ },
16438
+ {
16439
+ value: "local",
16440
+ label: ".gji.json",
16441
+ hint: "repo \u2014 committed with the project"
16442
+ }
16181
16443
  ]
16182
16444
  });
16183
16445
  if (pD(installSaveTarget)) {
@@ -16280,10 +16542,22 @@ function formatDetailedWorktreeTable(worktrees) {
16280
16542
  status: worktree.status,
16281
16543
  upstream: formatUpstreamState(worktree.upstream)
16282
16544
  }));
16283
- const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
16284
- const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
16285
- const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
16286
- const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
16545
+ const branchWidth = Math.max(
16546
+ "BRANCH".length,
16547
+ ...rows.map((row) => row.branch.length)
16548
+ );
16549
+ const statusWidth = Math.max(
16550
+ "STATUS".length,
16551
+ ...rows.map((row) => row.status.length)
16552
+ );
16553
+ const upstreamWidth = Math.max(
16554
+ "UPSTREAM".length,
16555
+ ...rows.map((row) => row.upstream.length)
16556
+ );
16557
+ const lastCommitWidth = Math.max(
16558
+ "LAST".length,
16559
+ ...rows.map((row) => row.lastCommit.length)
16560
+ );
16287
16561
  const lines = [
16288
16562
  " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
16289
16563
  ];
@@ -16306,7 +16580,9 @@ function formatWorktreeTable(worktrees) {
16306
16580
  );
16307
16581
  const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
16308
16582
  for (const row of rows) {
16309
- lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
16583
+ lines.push(
16584
+ `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`
16585
+ );
16310
16586
  }
16311
16587
  return lines.join("\n");
16312
16588
  }
@@ -16318,10 +16594,172 @@ function sortWorktrees(worktrees) {
16318
16594
  });
16319
16595
  }
16320
16596
 
16597
+ // src/open.ts
16598
+ import { execFile as execFile4 } from "node:child_process";
16599
+ import { access as access4, writeFile as writeFile6 } from "node:fs/promises";
16600
+ import { join as join8 } from "node:path";
16601
+ import { promisify as promisify5 } from "node:util";
16602
+ var execFileAsync4 = promisify5(execFile4);
16603
+ function createOpenCommand(dependencies = {}) {
16604
+ const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
16605
+ const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
16606
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
16607
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
16608
+ return async function runOpenCommand2(options) {
16609
+ const [worktrees, repository] = await Promise.all([
16610
+ listWorktrees(options.cwd),
16611
+ detectRepository(options.cwd)
16612
+ ]);
16613
+ let targetPath;
16614
+ if (options.branch) {
16615
+ const entry = worktrees.find((w2) => w2.branch === options.branch);
16616
+ if (!entry) {
16617
+ options.stderr(
16618
+ `gji open: no worktree found for branch: ${options.branch}
16619
+ `
16620
+ );
16621
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
16622
+ `);
16623
+ return 1;
16624
+ }
16625
+ targetPath = entry.path;
16626
+ } else if (isHeadless()) {
16627
+ targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
16628
+ } else {
16629
+ const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
16630
+ if (!chosen) {
16631
+ options.stderr("Aborted\n");
16632
+ return 1;
16633
+ }
16634
+ targetPath = chosen;
16635
+ }
16636
+ const config = await loadEffectiveConfig(
16637
+ repository.repoRoot,
16638
+ void 0,
16639
+ options.stderr
16640
+ );
16641
+ const savedEditor = resolveConfigString(config, "editor");
16642
+ let editorCli;
16643
+ if (options.editor) {
16644
+ editorCli = options.editor;
16645
+ } else if (savedEditor) {
16646
+ editorCli = savedEditor;
16647
+ } else {
16648
+ const installed = await detectEditors();
16649
+ if (installed.length === 0) {
16650
+ options.stderr(
16651
+ "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
16652
+ );
16653
+ return 1;
16654
+ }
16655
+ if (installed.length === 1 || isHeadless()) {
16656
+ editorCli = installed[0].cli;
16657
+ } else {
16658
+ const chosen = await promptForEditor(installed);
16659
+ if (!chosen) {
16660
+ options.stderr("Aborted\n");
16661
+ return 1;
16662
+ }
16663
+ editorCli = chosen;
16664
+ }
16665
+ }
16666
+ if (options.save && editorCli !== savedEditor) {
16667
+ await updateGlobalConfigKey("editor", editorCli);
16668
+ const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
16669
+ options.stdout(`Saved editor "${displayName2}" to global config
16670
+ `);
16671
+ }
16672
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
16673
+ let openTarget = targetPath;
16674
+ if (options.workspace) {
16675
+ if (editorDef?.supportsWorkspace) {
16676
+ openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
16677
+ } else {
16678
+ const displayName2 = editorDef?.name ?? editorCli;
16679
+ options.stderr(
16680
+ `gji open: --workspace is not supported for ${displayName2}, ignoring
16681
+ `
16682
+ );
16683
+ }
16684
+ }
16685
+ const args = [];
16686
+ if (editorDef?.newWindowFlag) {
16687
+ args.push(editorDef.newWindowFlag);
16688
+ }
16689
+ args.push(openTarget);
16690
+ try {
16691
+ await spawnEditor(editorCli, args);
16692
+ } catch (error) {
16693
+ const message = error instanceof Error ? error.message : String(error);
16694
+ options.stderr(`gji open: failed to launch editor: ${message}
16695
+ `);
16696
+ return 1;
16697
+ }
16698
+ const displayName = editorDef?.name ?? editorCli;
16699
+ options.stdout(`Opened ${targetPath} in ${displayName}
16700
+ `);
16701
+ return 0;
16702
+ };
16703
+ }
16704
+ var runOpenCommand = createOpenCommand();
16705
+ async function detectInstalledEditors() {
16706
+ const results = await Promise.all(
16707
+ EDITORS.map(async (editor) => ({
16708
+ editor,
16709
+ available: await isCommandAvailable(editor.cli)
16710
+ }))
16711
+ );
16712
+ return results.filter((r2) => r2.available).map((r2) => r2.editor);
16713
+ }
16714
+ async function isCommandAvailable(command) {
16715
+ try {
16716
+ await execFileAsync4("which", [command]);
16717
+ return true;
16718
+ } catch {
16719
+ return false;
16720
+ }
16721
+ }
16722
+ async function defaultPromptForWorktree(worktrees) {
16723
+ const choice = await ve({
16724
+ message: "Choose a worktree to open",
16725
+ options: worktrees.map((w2) => ({
16726
+ value: w2.path,
16727
+ label: w2.branch ?? "(detached)",
16728
+ hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
16729
+ }))
16730
+ });
16731
+ if (pD(choice)) return null;
16732
+ return choice;
16733
+ }
16734
+ async function defaultPromptForEditor(editors) {
16735
+ const choice = await ve({
16736
+ message: "Choose an editor",
16737
+ options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
16738
+ });
16739
+ if (pD(choice)) return null;
16740
+ return choice;
16741
+ }
16742
+ async function ensureWorkspaceFile(worktreePath, repoName) {
16743
+ const workspacePath = join8(worktreePath, `${repoName}.code-workspace`);
16744
+ try {
16745
+ await access4(workspacePath);
16746
+ return workspacePath;
16747
+ } catch {
16748
+ }
16749
+ const workspace = { folders: [{ path: "." }], settings: {} };
16750
+ await writeFile6(
16751
+ workspacePath,
16752
+ `${JSON.stringify(workspace, null, 2)}
16753
+ `,
16754
+ "utf8"
16755
+ );
16756
+ return workspacePath;
16757
+ }
16758
+
16321
16759
  // src/pr.ts
16322
- import { mkdir as mkdir7 } from "node:fs/promises";
16323
- import { basename as basename6, dirname as dirname8 } from "node:path";
16324
16760
  import { execFile as execFile5 } from "node:child_process";
16761
+ import { mkdir as mkdir7 } from "node:fs/promises";
16762
+ import { basename as basename7, dirname as dirname8 } from "node:path";
16325
16763
  import { promisify as promisify6 } from "node:util";
16326
16764
  var execFileAsync5 = promisify6(execFile5);
16327
16765
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
@@ -16329,7 +16767,9 @@ function parsePrInput(input) {
16329
16767
  if (/^\d+$/.test(input)) return input;
16330
16768
  const hashMatch = input.match(/^#(\d+)$/);
16331
16769
  if (hashMatch) return hashMatch[1];
16332
- const urlMatch = input.match(/\/(?:pull|pull-requests|merge_requests)\/(\d+)/);
16770
+ const urlMatch = input.match(
16771
+ /\/(?:pull|pull-requests|merge_requests)\/(\d+)/
16772
+ );
16333
16773
  if (urlMatch) return urlMatch[1];
16334
16774
  return null;
16335
16775
  }
@@ -16349,12 +16789,20 @@ function createPrCommand(dependencies = {}) {
16349
16789
  return 1;
16350
16790
  }
16351
16791
  const repository = await detectRepository(options.cwd);
16352
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
16792
+ const config = await loadEffectiveConfig(
16793
+ repository.repoRoot,
16794
+ void 0,
16795
+ options.stderr
16796
+ );
16353
16797
  const branchName = `pr/${prNumber}`;
16354
16798
  const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
16355
16799
  const rawBasePath = resolveConfigString(config, "worktreePath");
16356
16800
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
16357
- const worktreePath = resolveWorktreePath(repository.repoRoot, branchName, configuredBasePath);
16801
+ const worktreePath = resolveWorktreePath(
16802
+ repository.repoRoot,
16803
+ branchName,
16804
+ configuredBasePath
16805
+ );
16358
16806
  if (await pathExists(worktreePath)) {
16359
16807
  if (options.json || isHeadless()) {
16360
16808
  const message = `target worktree path already exists: ${worktreePath}`;
@@ -16362,10 +16810,14 @@ function createPrCommand(dependencies = {}) {
16362
16810
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
16363
16811
  `);
16364
16812
  } else {
16365
- options.stderr(`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
16366
- `);
16367
- options.stderr(`Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
16368
- `);
16813
+ options.stderr(
16814
+ `gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
16815
+ `
16816
+ );
16817
+ options.stderr(
16818
+ `Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
16819
+ `
16820
+ );
16369
16821
  }
16370
16822
  return 1;
16371
16823
  }
@@ -16375,22 +16827,33 @@ function createPrCommand(dependencies = {}) {
16375
16827
  await writeOutput2(worktreePath, options.stdout);
16376
16828
  return 0;
16377
16829
  }
16378
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
16379
- `);
16830
+ options.stderr(
16831
+ `Aborted because target worktree path already exists: ${worktreePath}
16832
+ `
16833
+ );
16380
16834
  return 1;
16381
16835
  }
16382
16836
  if (options.dryRun) {
16383
16837
  if (options.json) {
16384
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16385
- `);
16838
+ options.stdout(
16839
+ `${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16840
+ `
16841
+ );
16386
16842
  } else {
16387
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})
16388
- `);
16843
+ options.stdout(
16844
+ `Would create worktree at ${worktreePath} (branch: ${branchName})
16845
+ `
16846
+ );
16389
16847
  }
16390
16848
  return 0;
16391
16849
  }
16392
16850
  try {
16393
- await fetchPullRequestRef(repository.repoRoot, options.number, prNumber, remoteRef);
16851
+ await fetchPullRequestRef(
16852
+ repository.repoRoot,
16853
+ options.number,
16854
+ prNumber,
16855
+ remoteRef
16856
+ );
16394
16857
  } catch {
16395
16858
  const message = `Failed to fetch PR #${prNumber} from origin`;
16396
16859
  if (options.json) {
@@ -16399,35 +16862,57 @@ function createPrCommand(dependencies = {}) {
16399
16862
  } else {
16400
16863
  options.stderr(`${message}
16401
16864
  `);
16402
- options.stderr(`Hint: Verify the remote is reachable: git fetch origin
16403
- `);
16865
+ options.stderr(
16866
+ `Hint: Verify the remote is reachable: git fetch origin
16867
+ `
16868
+ );
16404
16869
  }
16405
16870
  return 1;
16406
16871
  }
16407
16872
  await mkdir7(dirname8(worktreePath), { recursive: true });
16408
- const branchAlreadyExists = await localBranchExists2(repository.repoRoot, branchName);
16873
+ const branchAlreadyExists = await localBranchExists2(
16874
+ repository.repoRoot,
16875
+ branchName
16876
+ );
16409
16877
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
16410
16878
  await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
16411
- const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
16879
+ const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
16880
+ (p2) => typeof p2 === "string"
16881
+ ) : [];
16412
16882
  for (const pattern of syncPatterns) {
16413
16883
  try {
16414
16884
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
16415
16885
  } catch (error) {
16416
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16417
- `);
16886
+ options.stderr(
16887
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16888
+ `
16889
+ );
16418
16890
  }
16419
16891
  }
16420
- await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
16892
+ await maybeRunInstallPrompt(
16893
+ worktreePath,
16894
+ repository.repoRoot,
16895
+ config,
16896
+ options.stderr,
16897
+ dependencies,
16898
+ !!options.json
16899
+ );
16421
16900
  const hooks = extractHooks(config);
16422
16901
  await runHook(
16423
16902
  hooks.afterCreate,
16424
16903
  worktreePath,
16425
- { branch: branchName, path: worktreePath, repo: basename6(repository.repoRoot) },
16904
+ {
16905
+ branch: branchName,
16906
+ path: worktreePath,
16907
+ repo: basename7(repository.repoRoot)
16908
+ },
16426
16909
  options.stderr
16427
16910
  );
16428
16911
  if (options.json) {
16429
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16430
- `);
16912
+ options.stdout(
16913
+ `${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16914
+ `
16915
+ );
16431
16916
  } else {
16432
16917
  await appendHistory(worktreePath, branchName);
16433
16918
  await writeOutput2(worktreePath, options.stdout);
@@ -16463,9 +16948,16 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
16463
16948
  throw new Error(`No pull request ref found for #${prNumber}`);
16464
16949
  }
16465
16950
  function listPullRequestSourceRefs(input, prNumber) {
16466
- const allForges = ["github", "gitlab", "bitbucket"];
16951
+ const allForges = [
16952
+ "github",
16953
+ "gitlab",
16954
+ "bitbucket"
16955
+ ];
16467
16956
  const preferredForge = detectPullRequestForge(input);
16468
- const orderedForges = preferredForge === "unknown" ? allForges : [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
16957
+ const orderedForges = preferredForge === "unknown" ? allForges : [
16958
+ preferredForge,
16959
+ ...allForges.filter((forge) => forge !== preferredForge)
16960
+ ];
16469
16961
  return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
16470
16962
  }
16471
16963
  function detectPullRequestForge(input) {
@@ -16495,7 +16987,7 @@ async function writeOutput2(worktreePath, stdout) {
16495
16987
  }
16496
16988
 
16497
16989
  // src/remove.ts
16498
- import { basename as basename7 } from "node:path";
16990
+ import { basename as basename8 } from "node:path";
16499
16991
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
16500
16992
  function createRemoveCommand(dependencies = {}) {
16501
16993
  const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
@@ -16503,7 +16995,9 @@ function createRemoveCommand(dependencies = {}) {
16503
16995
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
16504
16996
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
16505
16997
  return async function runRemoveCommand2(options) {
16506
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
16998
+ const { linkedWorktrees, repository } = await loadLinkedWorktrees(
16999
+ options.cwd
17000
+ );
16507
17001
  if (linkedWorktrees.length === 0) {
16508
17002
  emitError2(options, "No linked worktrees to finish");
16509
17003
  return 1;
@@ -16513,8 +17007,10 @@ function createRemoveCommand(dependencies = {}) {
16513
17007
  if (options.json) {
16514
17008
  emitError2(options, message);
16515
17009
  } else {
16516
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16517
- `);
17010
+ options.stderr(
17011
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17012
+ `
17013
+ );
16518
17014
  }
16519
17015
  return 1;
16520
17016
  }
@@ -16535,8 +17031,10 @@ function createRemoveCommand(dependencies = {}) {
16535
17031
  if (options.json) {
16536
17032
  emitError2(options, message);
16537
17033
  } else {
16538
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16539
- `);
17034
+ options.stderr(
17035
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17036
+ `
17037
+ );
16540
17038
  }
16541
17039
  return 1;
16542
17040
  }
@@ -16546,8 +17044,10 @@ function createRemoveCommand(dependencies = {}) {
16546
17044
  }
16547
17045
  if (options.dryRun) {
16548
17046
  if (options.json) {
16549
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
16550
- `);
17047
+ options.stdout(
17048
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
17049
+ `
17050
+ );
16551
17051
  } else {
16552
17052
  const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
16553
17053
  options.stdout(`Would remove worktree at ${worktree.path} (${desc})
@@ -16555,12 +17055,20 @@ function createRemoveCommand(dependencies = {}) {
16555
17055
  }
16556
17056
  return 0;
16557
17057
  }
16558
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
17058
+ const config = await loadEffectiveConfig(
17059
+ repository.repoRoot,
17060
+ void 0,
17061
+ options.stderr
17062
+ );
16559
17063
  const hooks = extractHooks(config);
16560
17064
  await runHook(
16561
17065
  hooks.beforeRemove,
16562
17066
  worktree.path,
16563
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename7(repository.repoRoot) },
17067
+ {
17068
+ branch: worktree.branch ?? void 0,
17069
+ path: worktree.path,
17070
+ repo: basename8(repository.repoRoot)
17071
+ },
16564
17072
  options.stderr
16565
17073
  );
16566
17074
  try {
@@ -16576,7 +17084,10 @@ function createRemoveCommand(dependencies = {}) {
16576
17084
  try {
16577
17085
  await forceRemoveWorktree(repository.repoRoot, worktree.path);
16578
17086
  } catch (forceError) {
16579
- emitError2(options, `Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`);
17087
+ emitError2(
17088
+ options,
17089
+ `Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`
17090
+ );
16580
17091
  return 1;
16581
17092
  }
16582
17093
  }
@@ -16591,18 +17102,24 @@ function createRemoveCommand(dependencies = {}) {
16591
17102
  try {
16592
17103
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
16593
17104
  } catch (forceError) {
16594
- options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
16595
- `);
17105
+ options.stderr(
17106
+ `Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
17107
+ `
17108
+ );
16596
17109
  }
16597
17110
  } else {
16598
- options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)
16599
- `);
17111
+ options.stderr(
17112
+ `Branch ${worktree.branch} was not deleted (has unmerged commits)
17113
+ `
17114
+ );
16600
17115
  }
16601
17116
  }
16602
17117
  }
16603
17118
  if (options.json) {
16604
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
16605
- `);
17119
+ options.stdout(
17120
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
17121
+ `
17122
+ );
16606
17123
  } else {
16607
17124
  await writeOutput3(repository.repoRoot, options.stdout);
16608
17125
  }
@@ -16651,7 +17168,11 @@ var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
16651
17168
  async function runRootCommand(options) {
16652
17169
  const repository = await detectRepository(options.cwd);
16653
17170
  if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
16654
- await writeShellOutput(ROOT_OUTPUT_FILE_ENV, repository.repoRoot, options.stdout);
17171
+ await writeShellOutput(
17172
+ ROOT_OUTPUT_FILE_ENV,
17173
+ repository.repoRoot,
17174
+ options.stdout
17175
+ );
16655
17176
  return 0;
16656
17177
  }
16657
17178
  options.stdout(`${repository.repoRoot}
@@ -16667,18 +17188,31 @@ async function runStatusCommand(options) {
16667
17188
  worktrees.map(async (worktree) => buildStatusRow(worktree))
16668
17189
  );
16669
17190
  if (options.json) {
16670
- options.stdout(`${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
16671
- `);
17191
+ options.stdout(
17192
+ `${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
17193
+ `
17194
+ );
16672
17195
  return 0;
16673
17196
  }
16674
- options.stdout(`${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
16675
- `);
17197
+ options.stdout(
17198
+ `${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
17199
+ `
17200
+ );
16676
17201
  return 0;
16677
17202
  }
16678
17203
  function formatStatusOutput(repoRoot, currentRoot, rows) {
16679
- const currentWidth = Math.max("CURRENT".length, ...rows.map((row) => row.current ? 1 : 0));
16680
- const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => formatBranch(row.branch).length));
16681
- const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
17204
+ const currentWidth = Math.max(
17205
+ "CURRENT".length,
17206
+ ...rows.map((row) => row.current ? 1 : 0)
17207
+ );
17208
+ const branchWidth = Math.max(
17209
+ "BRANCH".length,
17210
+ ...rows.map((row) => formatBranch(row.branch).length)
17211
+ );
17212
+ const statusWidth = Math.max(
17213
+ "STATUS".length,
17214
+ ...rows.map((row) => row.status.length)
17215
+ );
16682
17216
  const upstreamWidth = Math.max(
16683
17217
  "UPSTREAM".length,
16684
17218
  ...rows.map((row) => formatUpstreamState2(row.upstream).length)
@@ -16714,7 +17248,9 @@ async function buildStatusRow(worktree) {
16714
17248
  };
16715
17249
  }
16716
17250
  function sortWorktreesByPath(worktrees) {
16717
- return [...worktrees].sort((left, right) => comparePaths(left.path, right.path));
17251
+ return [...worktrees].sort(
17252
+ (left, right) => comparePaths(left.path, right.path)
17253
+ );
16718
17254
  }
16719
17255
  function formatBranch(branch) {
16720
17256
  return branch ?? "(detached)";
@@ -16760,7 +17296,11 @@ function formatUpstreamState2(upstream) {
16760
17296
  // src/sync.ts
16761
17297
  async function runSyncCommand(options) {
16762
17298
  const repository = await detectRepository(options.cwd);
16763
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
17299
+ const config = await loadEffectiveConfig(
17300
+ repository.repoRoot,
17301
+ void 0,
17302
+ options.stderr
17303
+ );
16764
17304
  const worktrees = await listWorktrees(options.cwd);
16765
17305
  const remote = resolveConfiguredString2(config.syncRemote) ?? "origin";
16766
17306
  let defaultBranch;
@@ -16769,22 +17309,33 @@ async function runSyncCommand(options) {
16769
17309
  } catch {
16770
17310
  emitError3(options, `Unable to reach remote '${remote}'`);
16771
17311
  if (!options.json) {
16772
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16773
- `);
17312
+ options.stderr(
17313
+ `Hint: Add the remote with: git remote add ${remote} <url>
17314
+ `
17315
+ );
16774
17316
  }
16775
17317
  return 1;
16776
17318
  }
16777
17319
  if (!defaultBranch) {
16778
17320
  emitError3(options, "Unable to determine the default branch for sync.");
16779
17321
  if (!options.json) {
16780
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16781
- `);
17322
+ options.stderr(
17323
+ `Hint: Add the remote with: git remote add ${remote} <url>
17324
+ `
17325
+ );
16782
17326
  }
16783
17327
  return 1;
16784
17328
  }
16785
- const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
17329
+ const targetWorktrees = selectTargetWorktrees(
17330
+ worktrees,
17331
+ repository.currentRoot,
17332
+ options.all
17333
+ );
16786
17334
  if (targetWorktrees === "detached") {
16787
- emitError3(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
17335
+ emitError3(
17336
+ options,
17337
+ `Cannot sync detached worktree: ${repository.currentRoot}`
17338
+ );
16788
17339
  return 1;
16789
17340
  }
16790
17341
  for (const worktree of targetWorktrees) {
@@ -16798,15 +17349,21 @@ async function runSyncCommand(options) {
16798
17349
  } catch {
16799
17350
  emitError3(options, `Failed to fetch from remote '${remote}'`);
16800
17351
  if (!options.json) {
16801
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16802
- `);
17352
+ options.stderr(
17353
+ `Hint: Add the remote with: git remote add ${remote} <url>
17354
+ `
17355
+ );
16803
17356
  }
16804
17357
  return 1;
16805
17358
  }
16806
17359
  const updatedWorktrees = [];
16807
17360
  for (const worktree of targetWorktrees) {
16808
17361
  if (worktree.branch === defaultBranch) {
16809
- await runGit(worktree.path, ["merge", "--ff-only", `${remote}/${defaultBranch}`]);
17362
+ await runGit(worktree.path, [
17363
+ "merge",
17364
+ "--ff-only",
17365
+ `${remote}/${defaultBranch}`
17366
+ ]);
16810
17367
  } else {
16811
17368
  await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
16812
17369
  }
@@ -16817,7 +17374,10 @@ async function runSyncCommand(options) {
16817
17374
  }
16818
17375
  }
16819
17376
  if (options.json) {
16820
- const updated = updatedWorktrees.map((w2) => ({ branch: w2.branch, path: w2.path }));
17377
+ const updated = updatedWorktrees.map((w2) => ({
17378
+ branch: w2.branch,
17379
+ path: w2.path
17380
+ }));
16821
17381
  options.stdout(`${JSON.stringify({ updated }, null, 2)}
16822
17382
  `);
16823
17383
  }
@@ -16836,7 +17396,9 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
16836
17396
  if (all) {
16837
17397
  return worktrees.filter((worktree) => worktree.branch !== null).sort((left, right) => comparePaths(left.path, right.path));
16838
17398
  }
16839
- const currentWorktree = worktrees.find((worktree) => worktree.path === currentRoot);
17399
+ const currentWorktree = worktrees.find(
17400
+ (worktree) => worktree.path === currentRoot
17401
+ );
16840
17402
  if (!currentWorktree) {
16841
17403
  return [];
16842
17404
  }
@@ -16849,8 +17411,153 @@ function resolveConfiguredString2(value) {
16849
17411
  return typeof value === "string" && value.length > 0 ? value : null;
16850
17412
  }
16851
17413
 
17414
+ // src/sync-files-command.ts
17415
+ import { homedir as homedir6 } from "node:os";
17416
+ import { join as join9 } from "node:path";
17417
+ async function runSyncFilesCommand(options) {
17418
+ const repository = await detectRepository(options.cwd);
17419
+ const home = options.home ?? homedir6();
17420
+ const loaded = await loadGlobalConfig(home);
17421
+ const repoEntry = findRepoConfigEntry(
17422
+ loaded.config,
17423
+ repository.repoRoot,
17424
+ home
17425
+ );
17426
+ const repoConfig = repoEntry?.config ?? {};
17427
+ switch (options.action) {
17428
+ case void 0:
17429
+ case "list": {
17430
+ writeSyncFiles(options.stdout, readSyncFiles(repoConfig), !!options.json);
17431
+ return 0;
17432
+ }
17433
+ case "add": {
17434
+ const paths = validatePaths(options.paths ?? [], options);
17435
+ if (!paths) return 1;
17436
+ const nextFiles = mergeSyncFiles(readSyncFiles(repoConfig), paths);
17437
+ await saveRepoSyncFiles(
17438
+ loaded.config,
17439
+ repoEntry?.key ?? repository.repoRoot,
17440
+ nextFiles,
17441
+ home
17442
+ );
17443
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
17444
+ return 0;
17445
+ }
17446
+ case "remove": {
17447
+ const paths = validatePaths(options.paths ?? [], options);
17448
+ if (!paths) return 1;
17449
+ const existingFiles = readSyncFiles(repoConfig);
17450
+ const nextFiles = removeSyncFiles(existingFiles, paths);
17451
+ if (repoEntry && nextFiles.length !== existingFiles.length) {
17452
+ await saveRepoSyncFiles(loaded.config, repoEntry.key, nextFiles, home);
17453
+ }
17454
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
17455
+ return 0;
17456
+ }
17457
+ }
17458
+ writeError(options, `unknown action: ${options.action}`);
17459
+ return 1;
17460
+ }
17461
+ function findRepoConfigEntry(config, repoRoot, home) {
17462
+ const repos = config.repos;
17463
+ if (!isPlainObject3(repos)) return null;
17464
+ for (const [key, value] of Object.entries(repos)) {
17465
+ if (expandTilde2(key, home) === repoRoot && isPlainObject3(value)) {
17466
+ return { config: value, key };
17467
+ }
17468
+ }
17469
+ return null;
17470
+ }
17471
+ function readSyncFiles(config) {
17472
+ const syncFiles2 = config.syncFiles;
17473
+ if (!Array.isArray(syncFiles2)) return [];
17474
+ return syncFiles2.filter((item) => typeof item === "string");
17475
+ }
17476
+ function writeSyncFiles(stdout, files, json) {
17477
+ if (json) {
17478
+ stdout(`${JSON.stringify(files, null, 2)}
17479
+ `);
17480
+ return;
17481
+ }
17482
+ if (files.length === 0) {
17483
+ stdout("No sync files configured for this repo.\n");
17484
+ return;
17485
+ }
17486
+ stdout(`${files.join("\n")}
17487
+ `);
17488
+ }
17489
+ function validatePaths(paths, options) {
17490
+ if (paths.length === 0) {
17491
+ writeError(options, "at least one path is required");
17492
+ return null;
17493
+ }
17494
+ const validatedPaths = [];
17495
+ for (const path9 of paths) {
17496
+ try {
17497
+ validatedPaths.push(validateSyncFilePattern(path9));
17498
+ } catch (error) {
17499
+ writeError(
17500
+ options,
17501
+ error instanceof Error ? error.message : String(error)
17502
+ );
17503
+ return null;
17504
+ }
17505
+ }
17506
+ return validatedPaths;
17507
+ }
17508
+ function mergeSyncFiles(existing, additions) {
17509
+ const nextFiles = [...existing];
17510
+ for (const path9 of additions) {
17511
+ if (!nextFiles.includes(path9)) {
17512
+ nextFiles.push(path9);
17513
+ }
17514
+ }
17515
+ return nextFiles;
17516
+ }
17517
+ function removeSyncFiles(existing, removals) {
17518
+ const removalSet = new Set(removals);
17519
+ return existing.filter((path9) => !removalSet.has(path9));
17520
+ }
17521
+ async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
17522
+ const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
17523
+ const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
17524
+ const nextRepoConfig = { ...repoConfig };
17525
+ if (syncFiles2.length > 0) {
17526
+ nextRepoConfig.syncFiles = syncFiles2;
17527
+ } else {
17528
+ delete nextRepoConfig.syncFiles;
17529
+ }
17530
+ if (Object.keys(nextRepoConfig).length > 0) {
17531
+ repos[repoKey] = nextRepoConfig;
17532
+ } else {
17533
+ delete repos[repoKey];
17534
+ }
17535
+ await saveGlobalConfig({ ...config, repos }, home);
17536
+ }
17537
+ function isPlainObject3(value) {
17538
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17539
+ }
17540
+ function writeError(options, message) {
17541
+ if (options.json) {
17542
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
17543
+ `);
17544
+ return;
17545
+ }
17546
+ options.stderr(`gji sync-files: ${message}
17547
+ `);
17548
+ }
17549
+ function expandTilde2(value, home) {
17550
+ if (value === "~") return home;
17551
+ if (value.startsWith("~/")) return join9(home, value.slice(2));
17552
+ return value;
17553
+ }
17554
+
16852
17555
  // src/trigger-hook.ts
16853
- var VALID_HOOKS = ["afterCreate", "afterEnter", "beforeRemove"];
17556
+ var VALID_HOOKS = [
17557
+ "afterCreate",
17558
+ "afterEnter",
17559
+ "beforeRemove"
17560
+ ];
16854
17561
  function isValidHook(hook) {
16855
17562
  return VALID_HOOKS.includes(hook);
16856
17563
  }
@@ -16864,10 +17571,16 @@ async function runTriggerHookCommand(options) {
16864
17571
  }
16865
17572
  const hookName = options.hook;
16866
17573
  const repository = await detectRepository(options.cwd);
16867
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
17574
+ const config = await loadEffectiveConfig(
17575
+ repository.repoRoot,
17576
+ void 0,
17577
+ options.stderr
17578
+ );
16868
17579
  const hooks = extractHooks(config);
16869
17580
  const worktrees = await listWorktrees(options.cwd);
16870
- const currentWorktree = worktrees.find((w2) => w2.path === repository.currentRoot);
17581
+ const currentWorktree = worktrees.find(
17582
+ (w2) => w2.path === repository.currentRoot
17583
+ );
16871
17584
  await runHook(
16872
17585
  hooks[hookName],
16873
17586
  repository.currentRoot,
@@ -16947,46 +17660,117 @@ function maybeRegisterCurrentRepo(cwd) {
16947
17660
  detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
16948
17661
  }
16949
17662
  function registerCommands(program2) {
16950
- program2.command("new [branch]").description("create a new branch or detached linked worktree").option("-f, --force", "remove and recreate the worktree if the target path already exists").option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option("--editor <cli>", "editor CLI to use with --open (code, cursor, zed, \u2026)").option("--dry-run", "show what would be created without executing any git commands or writing files").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("new"));
17663
+ program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
17664
+ "-f, --force",
17665
+ "remove and recreate the worktree if the target path already exists"
17666
+ ).option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option(
17667
+ "--editor <cli>",
17668
+ "editor CLI to use with --open (code, cursor, zed, \u2026)"
17669
+ ).option(
17670
+ "--dry-run",
17671
+ "show what would be created without executing any git commands or writing files"
17672
+ ).option(
17673
+ "--json",
17674
+ "emit JSON on success or error instead of human-readable output"
17675
+ ).action(notImplemented("new"));
16951
17676
  program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
16952
17677
  program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
16953
- program2.command("pr <ref>").description("fetch a pull request by number, #number, or URL into a linked worktree").option("--dry-run", "show what would be created without executing any git commands or writing files").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("pr"));
16954
- program2.command("back [n]").description("navigate to the previously visited worktree, optionally N steps back").option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
17678
+ program2.command("pr <ref>").description(
17679
+ "fetch a pull request by number, #number, or URL into a linked worktree"
17680
+ ).option(
17681
+ "--dry-run",
17682
+ "show what would be created without executing any git commands or writing files"
17683
+ ).option(
17684
+ "--json",
17685
+ "emit JSON on success or error instead of human-readable output"
17686
+ ).action(notImplemented("pr"));
17687
+ program2.command("back [n]").description(
17688
+ "navigate to the previously visited worktree, optionally N steps back"
17689
+ ).option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
16955
17690
  program2.command("history").description("show navigation history").option("--json", "print history as JSON").action(notImplemented("history"));
16956
- program2.command("open [branch]").description("open the worktree in an editor").option("--editor <cli>", "editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)").option("--save", "save the chosen editor to global config").option("--workspace", "generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)").action(notImplemented("open"));
17691
+ program2.command("open [branch]").description("open the worktree in an editor").option(
17692
+ "--editor <cli>",
17693
+ "editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)"
17694
+ ).option("--save", "save the chosen editor to global config").option(
17695
+ "--workspace",
17696
+ "generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)"
17697
+ ).action(notImplemented("open"));
16957
17698
  program2.command("go [branch]").alias("jump").description("print or select a worktree path").option("--print", "print the resolved worktree path explicitly").action(notImplemented("go"));
16958
17699
  program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
16959
17700
  program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
16960
- program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("sync"));
17701
+ program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
17702
+ "--json",
17703
+ "emit JSON on success or error instead of human-readable output"
17704
+ ).action(notImplemented("sync"));
17705
+ const syncFilesCommand = program2.command("sync-files").description("manage local files copied into new worktrees").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files"));
17706
+ syncFilesCommand.command("list").description("list files synced into new worktrees for this repo").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files list"));
17707
+ syncFilesCommand.command("add <paths...>").description("add repo-local sync files to global config").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files add"));
17708
+ syncFilesCommand.command("remove <paths...>").alias("rm").description("remove repo-local sync files from global config").option("--json", "emit JSON instead of human-readable output").action(notImplemented("sync-files remove"));
16961
17709
  program2.command("ls").description("list active worktrees").option("--compact", "show only branch and path columns").option("--json", "print active worktrees as JSON").action(notImplemented("ls"));
16962
- program2.command("clean").description("interactively prune linked worktrees").option("-f, --force", "bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches").option("--stale", "only target clean worktrees whose upstream is gone and branch is merged into the default branch").option("--dry-run", "show what would be deleted without removing anything").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("clean"));
16963
- program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option("-f, --force", "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch").option("--dry-run", "show what would be deleted without removing anything").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("remove"));
16964
- program2.command("trigger-hook <hook>").description("run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree").action(notImplemented("trigger-hook"));
16965
- program2.command("warp [branch]").description("jump to any worktree across all known repos").option("-n, --new [branch]", "create a new worktree in a registered repo").option("--print", "print the resolved worktree path without changing directory").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("warp"));
17710
+ program2.command("clean").description("interactively prune linked worktrees").option(
17711
+ "-f, --force",
17712
+ "bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches"
17713
+ ).option(
17714
+ "--stale",
17715
+ "only target clean worktrees whose upstream is gone and branch is merged into the default branch"
17716
+ ).option("--dry-run", "show what would be deleted without removing anything").option(
17717
+ "--json",
17718
+ "emit JSON on success or error instead of human-readable output"
17719
+ ).action(notImplemented("clean"));
17720
+ program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option(
17721
+ "-f, --force",
17722
+ "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch"
17723
+ ).option("--dry-run", "show what would be deleted without removing anything").option(
17724
+ "--json",
17725
+ "emit JSON on success or error instead of human-readable output"
17726
+ ).action(notImplemented("remove"));
17727
+ program2.command("trigger-hook <hook>").description(
17728
+ "run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree"
17729
+ ).action(notImplemented("trigger-hook"));
17730
+ program2.command("warp [branch]").description("jump to any worktree across all known repos").option("-n, --new [branch]", "create a new worktree in a registered repo").option(
17731
+ "--print",
17732
+ "print the resolved worktree path without changing directory"
17733
+ ).option(
17734
+ "--json",
17735
+ "emit JSON on success or error instead of human-readable output"
17736
+ ).action(notImplemented("warp"));
16966
17737
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
16967
17738
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
16968
17739
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
16969
17740
  configCommand.command("unset <key>").description("remove a global config value").action(notImplemented("config unset"));
16970
17741
  }
16971
17742
  function attachCommandActions(program2, options) {
16972
- program2.commands.find((command) => command.name() === "new")?.action(async (branch, commandOptions) => {
16973
- const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, editor: commandOptions.editor, force: commandOptions.force, json: commandOptions.json, open: commandOptions.open });
16974
- if (exitCode !== 0) {
16975
- throw commanderExit(exitCode);
17743
+ program2.commands.find((command) => command.name() === "new")?.action(
17744
+ async (branch, commandOptions) => {
17745
+ const exitCode = await runNewCommand({
17746
+ ...options,
17747
+ branch,
17748
+ detached: commandOptions.detached,
17749
+ dryRun: commandOptions.dryRun,
17750
+ editor: commandOptions.editor,
17751
+ force: commandOptions.force,
17752
+ json: commandOptions.json,
17753
+ open: commandOptions.open
17754
+ });
17755
+ if (exitCode !== 0) {
17756
+ throw commanderExit(exitCode);
17757
+ }
16976
17758
  }
16977
- });
16978
- program2.commands.find((command) => command.name() === "init")?.action(async (shell, commandOptions) => {
16979
- const exitCode = await runInitCommand({
16980
- cwd: options.cwd,
16981
- shell,
16982
- stderr: options.stderr,
16983
- stdout: options.stdout,
16984
- write: commandOptions.write
16985
- });
16986
- if (exitCode !== 0) {
16987
- throw commanderExit(exitCode);
17759
+ );
17760
+ program2.commands.find((command) => command.name() === "init")?.action(
17761
+ async (shell, commandOptions) => {
17762
+ const exitCode = await runInitCommand({
17763
+ cwd: options.cwd,
17764
+ shell,
17765
+ stderr: options.stderr,
17766
+ stdout: options.stdout,
17767
+ write: commandOptions.write
17768
+ });
17769
+ if (exitCode !== 0) {
17770
+ throw commanderExit(exitCode);
17771
+ }
16988
17772
  }
16989
- });
17773
+ );
16990
17774
  program2.commands.find((command) => command.name() === "completion")?.action(async (shell) => {
16991
17775
  const exitCode = await runCompletionCommand({
16992
17776
  shell,
@@ -16997,32 +17781,93 @@ function attachCommandActions(program2, options) {
16997
17781
  throw commanderExit(exitCode);
16998
17782
  }
16999
17783
  });
17000
- program2.commands.find((command) => command.name() === "pr")?.action(async (number, commandOptions) => {
17001
- const exitCode = await runPrCommand({ cwd: options.cwd, dryRun: commandOptions.dryRun, json: commandOptions.json, number, stderr: options.stderr, stdout: options.stdout });
17784
+ program2.commands.find((command) => command.name() === "pr")?.action(
17785
+ async (number, commandOptions) => {
17786
+ const exitCode = await runPrCommand({
17787
+ cwd: options.cwd,
17788
+ dryRun: commandOptions.dryRun,
17789
+ json: commandOptions.json,
17790
+ number,
17791
+ stderr: options.stderr,
17792
+ stdout: options.stdout
17793
+ });
17794
+ if (exitCode !== 0) {
17795
+ throw commanderExit(exitCode);
17796
+ }
17797
+ }
17798
+ );
17799
+ program2.commands.find((command) => command.name() === "back")?.action(
17800
+ async (n, commandOptions) => {
17801
+ if (n !== void 0 && !/^\d+$/.test(n)) {
17802
+ options.stderr(`gji back: invalid step count: ${n}
17803
+ `);
17804
+ throw commanderExit(1);
17805
+ }
17806
+ const steps = n !== void 0 ? parseInt(n, 10) : void 0;
17807
+ const exitCode = await runBackCommand({
17808
+ cwd: options.cwd,
17809
+ n: steps,
17810
+ print: commandOptions.print,
17811
+ stderr: options.stderr,
17812
+ stdout: options.stdout
17813
+ });
17814
+ if (exitCode !== 0) {
17815
+ throw commanderExit(exitCode);
17816
+ }
17817
+ }
17818
+ );
17819
+ program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
17820
+ const exitCode = await runHistoryCommand({
17821
+ cwd: options.cwd,
17822
+ json: commandOptions.json,
17823
+ stdout: options.stdout
17824
+ });
17002
17825
  if (exitCode !== 0) {
17003
17826
  throw commanderExit(exitCode);
17004
17827
  }
17005
17828
  });
17006
- program2.commands.find((command) => command.name() === "back")?.action(async (n, commandOptions) => {
17007
- if (n !== void 0 && !/^\d+$/.test(n)) {
17008
- options.stderr(`gji back: invalid step count: ${n}
17009
- `);
17010
- throw commanderExit(1);
17829
+ program2.commands.find((command) => command.name() === "open")?.action(
17830
+ async (branch, commandOptions) => {
17831
+ const exitCode = await runOpenCommand({
17832
+ branch,
17833
+ cwd: options.cwd,
17834
+ editor: commandOptions.editor,
17835
+ save: commandOptions.save,
17836
+ stderr: options.stderr,
17837
+ stdout: options.stdout,
17838
+ workspace: commandOptions.workspace
17839
+ });
17840
+ if (exitCode !== 0) {
17841
+ throw commanderExit(exitCode);
17842
+ }
17843
+ }
17844
+ );
17845
+ program2.commands.find((command) => command.name() === "go")?.action(
17846
+ async (branch, commandOptions) => {
17847
+ const exitCode = await runGoCommand({
17848
+ branch,
17849
+ cwd: options.cwd,
17850
+ print: commandOptions.print,
17851
+ stderr: options.stderr,
17852
+ stdout: options.stdout
17853
+ });
17854
+ if (exitCode !== 0) {
17855
+ throw commanderExit(exitCode);
17856
+ }
17011
17857
  }
17012
- const steps = n !== void 0 ? parseInt(n, 10) : void 0;
17013
- const exitCode = await runBackCommand({
17858
+ );
17859
+ program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17860
+ const exitCode = await runRootCommand({
17014
17861
  cwd: options.cwd,
17015
- n: steps,
17016
17862
  print: commandOptions.print,
17017
- stderr: options.stderr,
17018
17863
  stdout: options.stdout
17019
17864
  });
17020
17865
  if (exitCode !== 0) {
17021
17866
  throw commanderExit(exitCode);
17022
17867
  }
17023
17868
  });
17024
- program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
17025
- const exitCode = await runHistoryCommand({
17869
+ program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17870
+ const exitCode = await runStatusCommand({
17026
17871
  cwd: options.cwd,
17027
17872
  json: commandOptions.json,
17028
17873
  stdout: options.stdout
@@ -17031,25 +17876,26 @@ function attachCommandActions(program2, options) {
17031
17876
  throw commanderExit(exitCode);
17032
17877
  }
17033
17878
  });
17034
- program2.commands.find((command) => command.name() === "open")?.action(async (branch, commandOptions) => {
17035
- const exitCode = await runOpenCommand({
17036
- branch,
17879
+ program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17880
+ const exitCode = await runSyncCommand({
17881
+ all: commandOptions.all,
17037
17882
  cwd: options.cwd,
17038
- editor: commandOptions.editor,
17039
- save: commandOptions.save,
17883
+ json: commandOptions.json,
17040
17884
  stderr: options.stderr,
17041
- stdout: options.stdout,
17042
- workspace: commandOptions.workspace
17885
+ stdout: options.stdout
17043
17886
  });
17044
17887
  if (exitCode !== 0) {
17045
17888
  throw commanderExit(exitCode);
17046
17889
  }
17047
17890
  });
17048
- program2.commands.find((command) => command.name() === "go")?.action(async (branch, commandOptions) => {
17049
- const exitCode = await runGoCommand({
17050
- branch,
17891
+ const syncFilesCommand = program2.commands.find(
17892
+ (command) => command.name() === "sync-files"
17893
+ );
17894
+ syncFilesCommand?.action(async (commandOptions) => {
17895
+ const exitCode = await runSyncFilesCommand({
17896
+ action: "list",
17051
17897
  cwd: options.cwd,
17052
- print: commandOptions.print,
17898
+ json: commandOptions.json,
17053
17899
  stderr: options.stderr,
17054
17900
  stdout: options.stdout
17055
17901
  });
@@ -17057,38 +17903,45 @@ function attachCommandActions(program2, options) {
17057
17903
  throw commanderExit(exitCode);
17058
17904
  }
17059
17905
  });
17060
- program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17061
- const exitCode = await runRootCommand({
17906
+ syncFilesCommand?.commands.find((command) => command.name() === "list")?.action(async (commandOptions) => {
17907
+ const exitCode = await runSyncFilesCommand({
17908
+ action: "list",
17062
17909
  cwd: options.cwd,
17063
- print: commandOptions.print,
17910
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17911
+ stderr: options.stderr,
17064
17912
  stdout: options.stdout
17065
17913
  });
17066
17914
  if (exitCode !== 0) {
17067
17915
  throw commanderExit(exitCode);
17068
17916
  }
17069
17917
  });
17070
- program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17071
- const exitCode = await runStatusCommand({
17918
+ syncFilesCommand?.commands.find((command) => command.name() === "add")?.action(async (paths, commandOptions) => {
17919
+ const exitCode = await runSyncFilesCommand({
17920
+ action: "add",
17072
17921
  cwd: options.cwd,
17073
- json: commandOptions.json,
17922
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17923
+ paths,
17924
+ stderr: options.stderr,
17074
17925
  stdout: options.stdout
17075
17926
  });
17076
17927
  if (exitCode !== 0) {
17077
17928
  throw commanderExit(exitCode);
17078
17929
  }
17079
17930
  });
17080
- program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17081
- const exitCode = await runSyncCommand({
17082
- all: commandOptions.all,
17931
+ const runSyncFilesRemoveCommand = async (paths, commandOptions) => {
17932
+ const exitCode = await runSyncFilesCommand({
17933
+ action: "remove",
17083
17934
  cwd: options.cwd,
17084
- json: commandOptions.json,
17935
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17936
+ paths,
17085
17937
  stderr: options.stderr,
17086
17938
  stdout: options.stdout
17087
17939
  });
17088
17940
  if (exitCode !== 0) {
17089
17941
  throw commanderExit(exitCode);
17090
17942
  }
17091
- });
17943
+ };
17944
+ syncFilesCommand?.commands.find((command) => command.name() === "remove")?.action(runSyncFilesRemoveCommand);
17092
17945
  program2.commands.find((command) => command.name() === "ls")?.action(async (commandOptions) => {
17093
17946
  const exitCode = await runLsCommand({
17094
17947
  compact: commandOptions.compact,
@@ -17100,20 +17953,22 @@ function attachCommandActions(program2, options) {
17100
17953
  throw commanderExit(exitCode);
17101
17954
  }
17102
17955
  });
17103
- program2.commands.find((command) => command.name() === "clean")?.action(async (commandOptions) => {
17104
- const exitCode = await runCleanCommand({
17105
- cwd: options.cwd,
17106
- dryRun: commandOptions.dryRun,
17107
- force: commandOptions.force,
17108
- json: commandOptions.json,
17109
- stale: commandOptions.stale,
17110
- stderr: options.stderr,
17111
- stdout: options.stdout
17112
- });
17113
- if (exitCode !== 0) {
17114
- throw commanderExit(exitCode);
17956
+ program2.commands.find((command) => command.name() === "clean")?.action(
17957
+ async (commandOptions) => {
17958
+ const exitCode = await runCleanCommand({
17959
+ cwd: options.cwd,
17960
+ dryRun: commandOptions.dryRun,
17961
+ force: commandOptions.force,
17962
+ json: commandOptions.json,
17963
+ stale: commandOptions.stale,
17964
+ stderr: options.stderr,
17965
+ stdout: options.stdout
17966
+ });
17967
+ if (exitCode !== 0) {
17968
+ throw commanderExit(exitCode);
17969
+ }
17115
17970
  }
17116
- });
17971
+ );
17117
17972
  const runRemovalCommand = async (branch, commandOptions = {}) => {
17118
17973
  const exitCode = await runRemoveCommand({
17119
17974
  branch,
@@ -17139,23 +17994,27 @@ function attachCommandActions(program2, options) {
17139
17994
  throw commanderExit(exitCode);
17140
17995
  }
17141
17996
  });
17142
- program2.commands.find((command) => command.name() === "warp")?.action(async (branch, commandOptions) => {
17143
- const newFlag = commandOptions.new;
17144
- const newWorktree = newFlag !== void 0 && newFlag !== false;
17145
- const newBranch = typeof newFlag === "string" ? newFlag : void 0;
17146
- const exitCode = await runWarpCommand({
17147
- branch: newWorktree ? newBranch ?? branch : branch,
17148
- cwd: options.cwd,
17149
- json: commandOptions.json,
17150
- newWorktree,
17151
- stderr: options.stderr,
17152
- stdout: options.stdout
17153
- });
17154
- if (exitCode !== 0) {
17155
- throw commanderExit(exitCode);
17997
+ program2.commands.find((command) => command.name() === "warp")?.action(
17998
+ async (branch, commandOptions) => {
17999
+ const newFlag = commandOptions.new;
18000
+ const newWorktree = newFlag !== void 0 && newFlag !== false;
18001
+ const newBranch = typeof newFlag === "string" ? newFlag : void 0;
18002
+ const exitCode = await runWarpCommand({
18003
+ branch: newWorktree ? newBranch ?? branch : branch,
18004
+ cwd: options.cwd,
18005
+ json: commandOptions.json,
18006
+ newWorktree,
18007
+ stderr: options.stderr,
18008
+ stdout: options.stdout
18009
+ });
18010
+ if (exitCode !== 0) {
18011
+ throw commanderExit(exitCode);
18012
+ }
17156
18013
  }
17157
- });
17158
- const configCommand = program2.commands.find((command) => command.name() === "config");
18014
+ );
18015
+ const configCommand = program2.commands.find(
18016
+ (command) => command.name() === "config"
18017
+ );
17159
18018
  configCommand?.action(async () => {
17160
18019
  const exitCode = await runConfigCommand({
17161
18020
  cwd: options.cwd,
@@ -17237,7 +18096,7 @@ async function main() {
17237
18096
  }
17238
18097
  async function warnIfMissingShellIntegration() {
17239
18098
  try {
17240
- const { config } = await loadGlobalConfig(homedir6());
18099
+ const { config } = await loadGlobalConfig(homedir7());
17241
18100
  if (!config.shellIntegration) {
17242
18101
  const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
17243
18102
  const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";