@solaqua/gji 0.6.2 → 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 +1624 -819
  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 +14 -14
  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 +37 -33
  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";
@@ -13032,20 +12814,293 @@ 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);
13096
+ return;
13097
+ }
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");
13049
13104
  return;
13050
13105
  }
13051
13106
  await new Promise((resolve6) => {
@@ -13060,8 +13115,10 @@ 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
13123
  resolve6();
13067
13124
  });
@@ -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;
@@ -14888,19 +15016,72 @@ import { realpath as realpath2 } from "node:fs/promises";
14888
15016
  import { basename as basename5, resolve as resolve5 } from "node:path";
14889
15017
 
14890
15018
  // src/new.ts
15019
+ import { execFile as execFile3 } from "node:child_process";
14891
15020
  import { mkdir as mkdir4 } from "node:fs/promises";
14892
15021
  import { basename as basename3, dirname as dirname5 } from "node:path";
14893
- import { execFile as execFile3 } from "node:child_process";
14894
15022
  import { promisify as promisify4 } from "node:util";
14895
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
+
14896
15057
  // src/editor.ts
14897
15058
  import { spawn as spawn3 } from "node:child_process";
14898
15059
  var EDITORS = [
14899
- { cli: "cursor", name: "Cursor", newWindowFlag: "--new-window", supportsWorkspace: true },
14900
- { cli: "code", name: "VS Code", newWindowFlag: "--new-window", supportsWorkspace: true },
14901
- { 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
+ },
14902
15078
  { cli: "zed", name: "Zed", supportsWorkspace: false },
14903
- { 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
+ }
14904
15085
  ];
14905
15086
  async function defaultSpawnEditor(cli, args) {
14906
15087
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
@@ -14915,14 +15096,8 @@ async function defaultSpawnEditor(cli, args) {
14915
15096
  import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
14916
15097
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
14917
15098
  async function syncFiles(mainRoot, targetPath, patterns) {
14918
- for (const pattern of patterns) {
14919
- if (isAbsolute2(pattern)) {
14920
- throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
14921
- }
14922
- const normalized = normalize(pattern);
14923
- if (normalized.startsWith("..")) {
14924
- throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
14925
- }
15099
+ for (const pattern of patterns) {
15100
+ const normalized = validateSyncFilePattern(pattern);
14926
15101
  const sourcePath = join4(mainRoot, normalized);
14927
15102
  const destPath = join4(targetPath, normalized);
14928
15103
  const sourceExists = await fileExists(sourcePath);
@@ -14937,6 +15112,20 @@ async function syncFiles(mainRoot, targetPath, patterns) {
14937
15112
  await copyFile(sourcePath, destPath);
14938
15113
  }
14939
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
+ }
14940
15129
  async function fileExists(path9) {
14941
15130
  try {
14942
15131
  await stat(path9);
@@ -14956,7 +15145,7 @@ function isNotFoundError(error) {
14956
15145
  import { spawn as spawn4 } from "node:child_process";
14957
15146
 
14958
15147
  // src/package-manager.ts
14959
- import { access as access2, readdir } from "node:fs/promises";
15148
+ import { access as access3, readdir } from "node:fs/promises";
14960
15149
  import { join as join5 } from "node:path";
14961
15150
  var ENTRIES = [
14962
15151
  // JavaScript / TypeScript
@@ -14970,10 +15159,22 @@ var ENTRIES = [
14970
15159
  { name: "uv", signals: ["uv.lock"], command: "uv sync" },
14971
15160
  { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14972
15161
  { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
14973
- { name: "conda-lock", signals: ["conda-lock.yml"], command: "conda-lock install" },
14974
- { 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
+ },
14975
15172
  // R
14976
- { 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
+ },
14977
15178
  // Rust
14978
15179
  { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
14979
15180
  // Go
@@ -14990,25 +15191,56 @@ var ENTRIES = [
14990
15191
  // Java / Kotlin / Scala
14991
15192
  { name: "maven", signals: ["pom.xml"], command: "mvn install" },
14992
15193
  { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
14993
- { 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
+ },
14994
15199
  { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
14995
15200
  // .NET (C# / F# / VB)
14996
- { 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
+ },
14997
15207
  // Swift
14998
- { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
15208
+ {
15209
+ name: "swift",
15210
+ signals: ["Package.swift"],
15211
+ command: "swift package resolve"
15212
+ },
14999
15213
  // Haskell
15000
15214
  { name: "stack", signals: ["stack.yaml"], command: "stack build" },
15001
- { name: "cabal", signals: ["cabal.project"], command: "cabal install --only-dependencies" },
15002
- { 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
+ },
15003
15226
  // Clojure
15004
15227
  { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
15005
15228
  { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
15006
15229
  // OCaml
15007
15230
  { name: "dune", signals: ["dune-project"], command: "dune build" },
15008
15231
  // Julia
15009
- { 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
+ },
15010
15237
  // Nim
15011
- { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
15238
+ {
15239
+ name: "nimble",
15240
+ signals: ["*.nimble"],
15241
+ command: "nimble install",
15242
+ glob: true
15243
+ },
15012
15244
  // Crystal
15013
15245
  { name: "shards", signals: ["shard.yml"], command: "shards install" },
15014
15246
  // Perl
@@ -15017,12 +15249,20 @@ var ENTRIES = [
15017
15249
  { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
15018
15250
  // C / C++
15019
15251
  { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
15020
- { 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
+ },
15021
15257
  // Nix
15022
15258
  { name: "nix", signals: ["flake.nix"], command: "nix develop" },
15023
15259
  { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
15024
15260
  // Terraform / OpenTofu
15025
- { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
15261
+ {
15262
+ name: "terraform",
15263
+ signals: ["terraform.lock.hcl"],
15264
+ command: "terraform init"
15265
+ }
15026
15266
  ];
15027
15267
  async function detectPackageManager(repoRoot) {
15028
15268
  for (const entry of ENTRIES) {
@@ -15036,7 +15276,7 @@ async function detectPackageManager(repoRoot) {
15036
15276
  async function matchesExact(repoRoot, signals) {
15037
15277
  for (const signal of signals) {
15038
15278
  try {
15039
- await access2(join5(repoRoot, signal));
15279
+ await access3(join5(repoRoot, signal));
15040
15280
  return true;
15041
15281
  } catch {
15042
15282
  }
@@ -15085,8 +15325,10 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15085
15325
  try {
15086
15326
  await runner(pm.installCommand, worktreePath, stderr);
15087
15327
  } catch (error) {
15088
- stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15089
- `);
15328
+ stderr(
15329
+ `gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15330
+ `
15331
+ );
15090
15332
  }
15091
15333
  }
15092
15334
  const saveGlobal = config.installSaveTarget === "global";
@@ -15096,15 +15338,23 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15096
15338
  try {
15097
15339
  if (saveGlobal) {
15098
15340
  const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15099
- await writeGlobalKey(repoRoot, "hooks", { ...existingHooks, afterCreate: pm.installCommand });
15341
+ await writeGlobalKey(repoRoot, "hooks", {
15342
+ ...existingHooks,
15343
+ afterCreate: pm.installCommand
15344
+ });
15100
15345
  } else {
15101
15346
  const { config: localConfig } = await loadConfig(repoRoot);
15102
15347
  const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15103
- await writeKey(repoRoot, "hooks", { ...existingLocalHooks, afterCreate: pm.installCommand });
15348
+ await writeKey(repoRoot, "hooks", {
15349
+ ...existingLocalHooks,
15350
+ afterCreate: pm.installCommand
15351
+ });
15104
15352
  }
15105
15353
  } catch (error) {
15106
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15107
- `);
15354
+ stderr(
15355
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15356
+ `
15357
+ );
15108
15358
  }
15109
15359
  }
15110
15360
  if (choice === "never") {
@@ -15115,14 +15365,20 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15115
15365
  await writeKey(repoRoot, "skipInstallPrompt", true);
15116
15366
  }
15117
15367
  } catch (error) {
15118
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15119
- `);
15368
+ stderr(
15369
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15370
+ `
15371
+ );
15120
15372
  }
15121
15373
  }
15122
15374
  }
15123
15375
  async function defaultRunInstallCommand(command, cwd, stderr) {
15124
15376
  await new Promise((resolve6, reject) => {
15125
- const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15377
+ const child = spawn4(command, {
15378
+ cwd,
15379
+ shell: true,
15380
+ stdio: ["ignore", "inherit", "pipe"]
15381
+ });
15126
15382
  child.stderr.on("data", (chunk) => {
15127
15383
  stderr(chunk.toString());
15128
15384
  });
@@ -15157,7 +15413,11 @@ async function defaultPromptForInstallChoice(pm) {
15157
15413
  { value: "yes", label: "Yes", hint: "run once" },
15158
15414
  { value: "no", label: "No", hint: "skip this time" },
15159
15415
  { value: "always", label: "Always", hint: "save as afterCreate hook" },
15160
- { 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
+ }
15161
15421
  ]
15162
15422
  });
15163
15423
  if (pD(choice)) {
@@ -15173,31 +15433,6 @@ function isConfiguredHookCommand(value) {
15173
15433
  return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15174
15434
  }
15175
15435
 
15176
- // src/conflict.ts
15177
- import { access as access3 } from "node:fs/promises";
15178
- import { constants } from "node:fs";
15179
- async function pathExists(path9) {
15180
- try {
15181
- await access3(path9, constants.F_OK);
15182
- return true;
15183
- } catch {
15184
- return false;
15185
- }
15186
- }
15187
- async function promptForPathConflict(path9) {
15188
- const choice = await ve({
15189
- message: `Target path already exists: ${path9}`,
15190
- options: [
15191
- { value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
15192
- { value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
15193
- ]
15194
- });
15195
- if (pD(choice)) {
15196
- return "abort";
15197
- }
15198
- return choice;
15199
- }
15200
-
15201
15436
  // src/new.ts
15202
15437
  var execFileAsync3 = promisify4(execFile3);
15203
15438
  var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
@@ -15208,7 +15443,11 @@ function createNewCommand(dependencies = {}) {
15208
15443
  const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15209
15444
  return async function runNewCommand2(options) {
15210
15445
  const repository = await detectRepository(options.cwd);
15211
- 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
+ );
15212
15451
  const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15213
15452
  if (options.editor && !options.open) {
15214
15453
  options.stderr("gji new: --editor has no effect without --open\n");
@@ -15219,8 +15458,10 @@ function createNewCommand(dependencies = {}) {
15219
15458
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15220
15459
  `);
15221
15460
  } else {
15222
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15223
- `);
15461
+ options.stderr(
15462
+ `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15463
+ `
15464
+ );
15224
15465
  }
15225
15466
  return 1;
15226
15467
  }
@@ -15238,8 +15479,10 @@ function createNewCommand(dependencies = {}) {
15238
15479
  const branchError = validateBranchName(rawBranch);
15239
15480
  if (branchError) {
15240
15481
  if (options.json) {
15241
- options.stderr(`${JSON.stringify({ error: branchError }, null, 2)}
15242
- `);
15482
+ options.stderr(
15483
+ `${JSON.stringify({ error: branchError }, null, 2)}
15484
+ `
15485
+ );
15243
15486
  } else {
15244
15487
  options.stderr(`gji new: ${branchError}
15245
15488
  `);
@@ -15250,18 +15493,32 @@ function createNewCommand(dependencies = {}) {
15250
15493
  const rawBasePath = resolveConfigString(config, "worktreePath");
15251
15494
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
15252
15495
  const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
15253
- 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
+ );
15254
15505
  if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
15255
15506
  if (options.force) {
15256
15507
  if (!options.dryRun) {
15257
15508
  try {
15258
- 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
+ );
15259
15514
  } catch (err) {
15260
15515
  if (!isNotRegisteredWorktreeError(err)) {
15261
15516
  const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
15262
15517
  if (options.json) {
15263
- options.stderr(`${JSON.stringify({ warning: msg }, null, 2)}
15264
- `);
15518
+ options.stderr(
15519
+ `${JSON.stringify({ warning: msg }, null, 2)}
15520
+ `
15521
+ );
15265
15522
  } else {
15266
15523
  options.stderr(`Warning: ${msg}
15267
15524
  `);
@@ -15270,7 +15527,9 @@ function createNewCommand(dependencies = {}) {
15270
15527
  }
15271
15528
  if (!options.detached) {
15272
15529
  try {
15273
- await execFileAsync3("git", ["branch", "-D", worktreeName], { cwd: repository.repoRoot });
15530
+ await execFileAsync3("git", ["branch", "-D", worktreeName], {
15531
+ cwd: repository.repoRoot
15532
+ });
15274
15533
  } catch {
15275
15534
  }
15276
15535
  }
@@ -15281,12 +15540,18 @@ function createNewCommand(dependencies = {}) {
15281
15540
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15282
15541
  `);
15283
15542
  } else {
15284
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15285
- `);
15286
- options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15287
- `);
15288
- options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15289
- `);
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
+ );
15290
15555
  }
15291
15556
  return 1;
15292
15557
  } else {
@@ -15296,53 +15561,81 @@ function createNewCommand(dependencies = {}) {
15296
15561
  await writeOutput(worktreePath, options.stdout);
15297
15562
  return 0;
15298
15563
  }
15299
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
15300
- `);
15564
+ options.stderr(
15565
+ `Aborted because target worktree path already exists: ${worktreePath}
15566
+ `
15567
+ );
15301
15568
  return 1;
15302
15569
  }
15303
15570
  }
15304
15571
  if (options.dryRun) {
15305
15572
  if (options.json) {
15306
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15307
- `);
15573
+ options.stdout(
15574
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15575
+ `
15576
+ );
15308
15577
  } else {
15309
15578
  const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
15310
15579
  const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
15311
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15312
- `);
15580
+ options.stdout(
15581
+ `Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15582
+ `
15583
+ );
15313
15584
  }
15314
15585
  return 0;
15315
15586
  }
15316
15587
  await mkdir4(dirname5(worktreePath), { recursive: true });
15317
15588
  const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15318
15589
  await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15319
- 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
+ ) : [];
15320
15593
  for (const pattern of syncPatterns) {
15321
15594
  try {
15322
15595
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
15323
15596
  } catch (error) {
15324
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15325
- `);
15597
+ options.stderr(
15598
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15599
+ `
15600
+ );
15326
15601
  }
15327
15602
  }
15328
- 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
+ );
15329
15611
  const hooks = extractHooks(config);
15330
15612
  await runHook(
15331
15613
  hooks.afterCreate,
15332
15614
  worktreePath,
15333
- { branch: worktreeName, path: worktreePath, repo: basename3(repository.repoRoot) },
15615
+ {
15616
+ branch: worktreeName,
15617
+ path: worktreePath,
15618
+ repo: basename3(repository.repoRoot)
15619
+ },
15334
15620
  options.stderr
15335
15621
  );
15336
15622
  if (options.json) {
15337
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15338
- `);
15623
+ options.stdout(
15624
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15625
+ `
15626
+ );
15339
15627
  } else {
15340
15628
  await appendHistory(worktreePath, worktreeName);
15341
15629
  await writeOutput(worktreePath, options.stdout);
15342
15630
  }
15343
15631
  if (options.open) {
15344
15632
  const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
15345
- await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
15633
+ await openWorktree(
15634
+ worktreePath,
15635
+ resolvedEditor,
15636
+ spawnEditor,
15637
+ options.stderr
15638
+ );
15346
15639
  }
15347
15640
  return 0;
15348
15641
  };
@@ -15403,7 +15696,11 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15403
15696
  let attempt = 1;
15404
15697
  while (true) {
15405
15698
  const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15406
- const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15699
+ const candidatePath = resolveWorktreePath(
15700
+ repoRoot,
15701
+ candidateName,
15702
+ basePath
15703
+ );
15407
15704
  if (!await pathExists(candidatePath)) {
15408
15705
  return candidatePath;
15409
15706
  }
@@ -15431,7 +15728,11 @@ function pickRandom(values, random) {
15431
15728
  }
15432
15729
  async function localBranchExists(repoRoot, branchName) {
15433
15730
  try {
15434
- 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
+ );
15435
15736
  return true;
15436
15737
  } catch {
15437
15738
  return false;
@@ -15452,7 +15753,9 @@ function toExecMessage(error) {
15452
15753
  }
15453
15754
  async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15454
15755
  if (!editorCli) {
15455
- 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
+ );
15456
15759
  return;
15457
15760
  }
15458
15761
  const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
@@ -15566,11 +15869,17 @@ async function runWarpNavigate(options) {
15566
15869
  }
15567
15870
  return 1;
15568
15871
  }
15569
- 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
+ });
15570
15877
  if (!target) return 1;
15571
15878
  if (options.json) {
15572
- options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15573
- `);
15879
+ options.stdout(
15880
+ `${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15881
+ `
15882
+ );
15574
15883
  return 0;
15575
15884
  }
15576
15885
  appendHistory(target.path, target.branch).catch(() => void 0);
@@ -15771,14 +16080,19 @@ function createGoCommand(dependencies = {}) {
15771
16080
  );
15772
16081
  return 1;
15773
16082
  }
15774
- const target = await resolveWarpTarget({ ...options, commandName: "gji go" });
16083
+ const target = await resolveWarpTarget({
16084
+ ...options,
16085
+ commandName: "gji go"
16086
+ });
15775
16087
  if (!target) return 1;
15776
16088
  appendHistory(target.path, target.branch).catch(() => void 0);
15777
16089
  await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
15778
16090
  return 0;
15779
16091
  }
15780
16092
  if (!options.branch && isHeadless()) {
15781
- 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
+ );
15782
16096
  return 1;
15783
16097
  }
15784
16098
  const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
@@ -15794,204 +16108,83 @@ function createGoCommand(dependencies = {}) {
15794
16108
  }
15795
16109
  return 1;
15796
16110
  }
15797
- const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
15798
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15799
- const hooks = extractHooks(config);
15800
- await runHook(
15801
- hooks.afterEnter,
15802
- resolvedPath,
15803
- { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename6(repository.repoRoot) },
15804
- options.stderr
15805
- );
15806
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
15807
- await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15808
- return 0;
15809
- };
15810
- }
15811
- var runGoCommand = createGoCommand();
15812
- async function promptForWorktree(worktrees) {
15813
- const healthResults = await Promise.allSettled(
15814
- worktrees.map((w2) => readWorktreeHealth(w2.path))
15815
- );
15816
- const choice = await ve({
15817
- message: "Choose a worktree",
15818
- options: worktrees.map((worktree, i) => {
15819
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15820
- const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
15821
- const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
15822
- return {
15823
- value: worktree.path,
15824
- label: worktree.branch ?? "(detached)",
15825
- hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
15826
- };
15827
- })
15828
- });
15829
- if (pD(choice)) {
15830
- return null;
15831
- }
15832
- return choice;
15833
- }
15834
- function formatUpstreamHint(branch, health) {
15835
- if (branch === null) return null;
15836
- if (!health.hasUpstream) return "no upstream";
15837
- if (health.upstreamGone) return "upstream gone";
15838
- if (health.ahead === 0 && health.behind === 0) return "up to date";
15839
- if (health.ahead === 0) return `behind ${health.behind}`;
15840
- if (health.behind === 0) return `ahead ${health.ahead}`;
15841
- return `ahead ${health.ahead}, behind ${health.behind}`;
15842
- }
15843
-
15844
- // src/open.ts
15845
- import { execFile as execFile4 } from "node:child_process";
15846
- import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
15847
- import { join as join7 } from "node:path";
15848
- import { promisify as promisify5 } from "node:util";
15849
- var execFileAsync4 = promisify5(execFile4);
15850
- function createOpenCommand(dependencies = {}) {
15851
- const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
15852
- const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
15853
- const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
15854
- const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15855
- return async function runOpenCommand2(options) {
15856
- const [worktrees, repository] = await Promise.all([
15857
- listWorktrees(options.cwd),
15858
- detectRepository(options.cwd)
15859
- ]);
15860
- let targetPath;
15861
- if (options.branch) {
15862
- const entry = worktrees.find((w2) => w2.branch === options.branch);
15863
- if (!entry) {
15864
- options.stderr(`gji open: no worktree found for branch: ${options.branch}
15865
- `);
15866
- options.stderr(`Hint: Use 'gji ls' to see available worktrees
15867
- `);
15868
- return 1;
15869
- }
15870
- targetPath = entry.path;
15871
- } else if (isHeadless()) {
15872
- targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
15873
- } else {
15874
- const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
15875
- if (!chosen) {
15876
- options.stderr("Aborted\n");
15877
- return 1;
15878
- }
15879
- targetPath = chosen;
15880
- }
15881
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15882
- const savedEditor = resolveConfigString(config, "editor");
15883
- let editorCli;
15884
- if (options.editor) {
15885
- editorCli = options.editor;
15886
- } else if (savedEditor) {
15887
- editorCli = savedEditor;
15888
- } else {
15889
- const installed = await detectEditors();
15890
- if (installed.length === 0) {
15891
- options.stderr(
15892
- "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
15893
- );
15894
- return 1;
15895
- }
15896
- if (installed.length === 1 || isHeadless()) {
15897
- editorCli = installed[0].cli;
15898
- } else {
15899
- const chosen = await promptForEditor(installed);
15900
- if (!chosen) {
15901
- options.stderr("Aborted\n");
15902
- return 1;
15903
- }
15904
- editorCli = chosen;
15905
- }
15906
- }
15907
- if (options.save && editorCli !== savedEditor) {
15908
- await updateGlobalConfigKey("editor", editorCli);
15909
- const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
15910
- options.stdout(`Saved editor "${displayName2}" to global config
15911
- `);
15912
- }
15913
- const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15914
- let openTarget = targetPath;
15915
- if (options.workspace) {
15916
- if (editorDef?.supportsWorkspace) {
15917
- openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
15918
- } else {
15919
- const displayName2 = editorDef?.name ?? editorCli;
15920
- options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
15921
- `);
15922
- }
15923
- }
15924
- const args = [];
15925
- if (editorDef?.newWindowFlag) {
15926
- args.push(editorDef.newWindowFlag);
15927
- }
15928
- args.push(openTarget);
15929
- try {
15930
- await spawnEditor(editorCli, args);
15931
- } catch (error) {
15932
- const message = error instanceof Error ? error.message : String(error);
15933
- options.stderr(`gji open: failed to launch editor: ${message}
15934
- `);
15935
- return 1;
15936
- }
15937
- const displayName = editorDef?.name ?? editorCli;
15938
- options.stdout(`Opened ${targetPath} in ${displayName}
15939
- `);
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);
15940
16132
  return 0;
15941
16133
  };
15942
16134
  }
15943
- var runOpenCommand = createOpenCommand();
15944
- async function detectInstalledEditors() {
15945
- const results = await Promise.all(
15946
- 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))
15947
16139
  );
15948
- return results.filter((r2) => r2.available).map((r2) => r2.editor);
15949
- }
15950
- async function isCommandAvailable(command) {
15951
- try {
15952
- await execFileAsync4("which", [command]);
15953
- return true;
15954
- } catch {
15955
- return false;
15956
- }
15957
- }
15958
- async function defaultPromptForWorktree(worktrees) {
15959
16140
  const choice = await ve({
15960
- message: "Choose a worktree to open",
15961
- options: worktrees.map((w2) => ({
15962
- value: w2.path,
15963
- label: w2.branch ?? "(detached)",
15964
- hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
15965
- }))
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
+ })
15966
16152
  });
15967
- if (pD(choice)) return null;
16153
+ if (pD(choice)) {
16154
+ return null;
16155
+ }
15968
16156
  return choice;
15969
16157
  }
15970
- async function defaultPromptForEditor(editors) {
15971
- const choice = await ve({
15972
- message: "Choose an editor",
15973
- options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
15974
- });
15975
- if (pD(choice)) return null;
15976
- 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}`;
15977
16166
  }
15978
- async function ensureWorkspaceFile(worktreePath, repoName) {
15979
- const workspacePath = join7(worktreePath, `${repoName}.code-workspace`);
15980
- try {
15981
- await access4(workspacePath);
15982
- return workspacePath;
15983
- } 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;
15984
16175
  }
15985
- const workspace = { folders: [{ path: "." }], settings: {} };
15986
- await writeFile5(workspacePath, `${JSON.stringify(workspace, null, 2)}
15987
- `, "utf8");
15988
- 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;
15989
16182
  }
15990
16183
 
15991
16184
  // src/init.ts
15992
- 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";
15993
16186
  import { homedir as homedir5 } from "node:os";
15994
- import { dirname as dirname7, join as join8 } from "node:path";
16187
+ import { dirname as dirname7, join as join7 } from "node:path";
15995
16188
  var START_MARKER = "# >>> gji init >>>";
15996
16189
  var END_MARKER = "# <<< gji init <<<";
15997
16190
  var SHELL_WRAPPED_COMMANDS = [
@@ -16063,7 +16256,7 @@ async function runInitCommand(options) {
16063
16256
  await mkdir6(dirname7(rcPath), { recursive: true });
16064
16257
  const current = await readExistingConfig(rcPath);
16065
16258
  const next = upsertShellIntegration(current, script);
16066
- await writeFile6(rcPath, next, "utf8");
16259
+ await writeFile5(rcPath, next, "utf8");
16067
16260
  options.stdout(`${rcPath}
16068
16261
  `);
16069
16262
  const { config: globalConfig } = await loadGlobalConfig(home);
@@ -16074,7 +16267,11 @@ async function runInitCommand(options) {
16074
16267
  const prompt = options.promptForSetup ?? defaultPromptForSetup;
16075
16268
  const result = await prompt();
16076
16269
  if (result) {
16077
- await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
16270
+ await updateGlobalConfigKey(
16271
+ "installSaveTarget",
16272
+ result.installSaveTarget,
16273
+ home
16274
+ );
16078
16275
  await saveWizardConfig(result, options.cwd, home);
16079
16276
  }
16080
16277
  }
@@ -16134,7 +16331,8 @@ async function saveWizardConfig(result, cwd, home) {
16134
16331
  const hooks = {};
16135
16332
  if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16136
16333
  if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16137
- if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
16334
+ if (result.hooks?.beforeRemove)
16335
+ hooks.beforeRemove = result.hooks.beforeRemove;
16138
16336
  if (Object.keys(hooks).length > 0) values.hooks = hooks;
16139
16337
  if (Object.keys(values).length === 0) return;
16140
16338
  if (result.installSaveTarget === "local") {
@@ -16148,11 +16346,11 @@ async function saveWizardConfig(result, cwd, home) {
16148
16346
  function resolveShellConfigPath(shell, home) {
16149
16347
  switch (shell) {
16150
16348
  case "bash":
16151
- return join8(home, ".bashrc");
16349
+ return join7(home, ".bashrc");
16152
16350
  case "fish":
16153
- return join8(home, ".config", "fish", "config.fish");
16351
+ return join7(home, ".config", "fish", "config.fish");
16154
16352
  case "zsh":
16155
- return join8(home, ".zshrc");
16353
+ return join7(home, ".zshrc");
16156
16354
  }
16157
16355
  }
16158
16356
  async function readExistingConfig(path9) {
@@ -16178,7 +16376,9 @@ function isMissingFileError2(error) {
16178
16376
  function renderFishWrapper(command) {
16179
16377
  const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
16180
16378
  const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
16181
- const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
16379
+ const bypassTests = command.bypassOptions.map(
16380
+ (opt) => `test $argv[1] = ${opt}`
16381
+ );
16182
16382
  const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
16183
16383
  return `if test (count $argv) -gt 0; and ${nameCondition}
16184
16384
  set -e argv[1]
@@ -16230,8 +16430,16 @@ async function defaultPromptForSetup() {
16230
16430
  const installSaveTarget = await ve({
16231
16431
  message: "Where should preferences be saved?",
16232
16432
  options: [
16233
- { value: "global", label: "~/.config/gji/config.json", hint: "personal \u2014 never committed" },
16234
- { 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
+ }
16235
16443
  ]
16236
16444
  });
16237
16445
  if (pD(installSaveTarget)) {
@@ -16334,10 +16542,22 @@ function formatDetailedWorktreeTable(worktrees) {
16334
16542
  status: worktree.status,
16335
16543
  upstream: formatUpstreamState(worktree.upstream)
16336
16544
  }));
16337
- const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
16338
- const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
16339
- const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
16340
- 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
+ );
16341
16561
  const lines = [
16342
16562
  " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
16343
16563
  ];
@@ -16360,7 +16580,9 @@ function formatWorktreeTable(worktrees) {
16360
16580
  );
16361
16581
  const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
16362
16582
  for (const row of rows) {
16363
- lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
16583
+ lines.push(
16584
+ `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`
16585
+ );
16364
16586
  }
16365
16587
  return lines.join("\n");
16366
16588
  }
@@ -16372,10 +16594,172 @@ function sortWorktrees(worktrees) {
16372
16594
  });
16373
16595
  }
16374
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
+
16375
16759
  // src/pr.ts
16760
+ import { execFile as execFile5 } from "node:child_process";
16376
16761
  import { mkdir as mkdir7 } from "node:fs/promises";
16377
16762
  import { basename as basename7, dirname as dirname8 } from "node:path";
16378
- import { execFile as execFile5 } from "node:child_process";
16379
16763
  import { promisify as promisify6 } from "node:util";
16380
16764
  var execFileAsync5 = promisify6(execFile5);
16381
16765
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
@@ -16383,7 +16767,9 @@ function parsePrInput(input) {
16383
16767
  if (/^\d+$/.test(input)) return input;
16384
16768
  const hashMatch = input.match(/^#(\d+)$/);
16385
16769
  if (hashMatch) return hashMatch[1];
16386
- const urlMatch = input.match(/\/(?:pull|pull-requests|merge_requests)\/(\d+)/);
16770
+ const urlMatch = input.match(
16771
+ /\/(?:pull|pull-requests|merge_requests)\/(\d+)/
16772
+ );
16387
16773
  if (urlMatch) return urlMatch[1];
16388
16774
  return null;
16389
16775
  }
@@ -16403,12 +16789,20 @@ function createPrCommand(dependencies = {}) {
16403
16789
  return 1;
16404
16790
  }
16405
16791
  const repository = await detectRepository(options.cwd);
16406
- 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
+ );
16407
16797
  const branchName = `pr/${prNumber}`;
16408
16798
  const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
16409
16799
  const rawBasePath = resolveConfigString(config, "worktreePath");
16410
16800
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
16411
- const worktreePath = resolveWorktreePath(repository.repoRoot, branchName, configuredBasePath);
16801
+ const worktreePath = resolveWorktreePath(
16802
+ repository.repoRoot,
16803
+ branchName,
16804
+ configuredBasePath
16805
+ );
16412
16806
  if (await pathExists(worktreePath)) {
16413
16807
  if (options.json || isHeadless()) {
16414
16808
  const message = `target worktree path already exists: ${worktreePath}`;
@@ -16416,10 +16810,14 @@ function createPrCommand(dependencies = {}) {
16416
16810
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
16417
16811
  `);
16418
16812
  } else {
16419
- options.stderr(`gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
16420
- `);
16421
- options.stderr(`Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
16422
- `);
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
+ );
16423
16821
  }
16424
16822
  return 1;
16425
16823
  }
@@ -16429,22 +16827,33 @@ function createPrCommand(dependencies = {}) {
16429
16827
  await writeOutput2(worktreePath, options.stdout);
16430
16828
  return 0;
16431
16829
  }
16432
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
16433
- `);
16830
+ options.stderr(
16831
+ `Aborted because target worktree path already exists: ${worktreePath}
16832
+ `
16833
+ );
16434
16834
  return 1;
16435
16835
  }
16436
16836
  if (options.dryRun) {
16437
16837
  if (options.json) {
16438
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16439
- `);
16838
+ options.stdout(
16839
+ `${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16840
+ `
16841
+ );
16440
16842
  } else {
16441
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})
16442
- `);
16843
+ options.stdout(
16844
+ `Would create worktree at ${worktreePath} (branch: ${branchName})
16845
+ `
16846
+ );
16443
16847
  }
16444
16848
  return 0;
16445
16849
  }
16446
16850
  try {
16447
- await fetchPullRequestRef(repository.repoRoot, options.number, prNumber, remoteRef);
16851
+ await fetchPullRequestRef(
16852
+ repository.repoRoot,
16853
+ options.number,
16854
+ prNumber,
16855
+ remoteRef
16856
+ );
16448
16857
  } catch {
16449
16858
  const message = `Failed to fetch PR #${prNumber} from origin`;
16450
16859
  if (options.json) {
@@ -16453,35 +16862,57 @@ function createPrCommand(dependencies = {}) {
16453
16862
  } else {
16454
16863
  options.stderr(`${message}
16455
16864
  `);
16456
- options.stderr(`Hint: Verify the remote is reachable: git fetch origin
16457
- `);
16865
+ options.stderr(
16866
+ `Hint: Verify the remote is reachable: git fetch origin
16867
+ `
16868
+ );
16458
16869
  }
16459
16870
  return 1;
16460
16871
  }
16461
16872
  await mkdir7(dirname8(worktreePath), { recursive: true });
16462
- const branchAlreadyExists = await localBranchExists2(repository.repoRoot, branchName);
16873
+ const branchAlreadyExists = await localBranchExists2(
16874
+ repository.repoRoot,
16875
+ branchName
16876
+ );
16463
16877
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
16464
16878
  await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
16465
- 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
+ ) : [];
16466
16882
  for (const pattern of syncPatterns) {
16467
16883
  try {
16468
16884
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
16469
16885
  } catch (error) {
16470
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16471
- `);
16886
+ options.stderr(
16887
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16888
+ `
16889
+ );
16472
16890
  }
16473
16891
  }
16474
- 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
+ );
16475
16900
  const hooks = extractHooks(config);
16476
16901
  await runHook(
16477
16902
  hooks.afterCreate,
16478
16903
  worktreePath,
16479
- { branch: branchName, path: worktreePath, repo: basename7(repository.repoRoot) },
16904
+ {
16905
+ branch: branchName,
16906
+ path: worktreePath,
16907
+ repo: basename7(repository.repoRoot)
16908
+ },
16480
16909
  options.stderr
16481
16910
  );
16482
16911
  if (options.json) {
16483
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16484
- `);
16912
+ options.stdout(
16913
+ `${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16914
+ `
16915
+ );
16485
16916
  } else {
16486
16917
  await appendHistory(worktreePath, branchName);
16487
16918
  await writeOutput2(worktreePath, options.stdout);
@@ -16517,9 +16948,16 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
16517
16948
  throw new Error(`No pull request ref found for #${prNumber}`);
16518
16949
  }
16519
16950
  function listPullRequestSourceRefs(input, prNumber) {
16520
- const allForges = ["github", "gitlab", "bitbucket"];
16951
+ const allForges = [
16952
+ "github",
16953
+ "gitlab",
16954
+ "bitbucket"
16955
+ ];
16521
16956
  const preferredForge = detectPullRequestForge(input);
16522
- 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
+ ];
16523
16961
  return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
16524
16962
  }
16525
16963
  function detectPullRequestForge(input) {
@@ -16557,7 +16995,9 @@ function createRemoveCommand(dependencies = {}) {
16557
16995
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
16558
16996
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
16559
16997
  return async function runRemoveCommand2(options) {
16560
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
16998
+ const { linkedWorktrees, repository } = await loadLinkedWorktrees(
16999
+ options.cwd
17000
+ );
16561
17001
  if (linkedWorktrees.length === 0) {
16562
17002
  emitError2(options, "No linked worktrees to finish");
16563
17003
  return 1;
@@ -16567,8 +17007,10 @@ function createRemoveCommand(dependencies = {}) {
16567
17007
  if (options.json) {
16568
17008
  emitError2(options, message);
16569
17009
  } else {
16570
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16571
- `);
17010
+ options.stderr(
17011
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17012
+ `
17013
+ );
16572
17014
  }
16573
17015
  return 1;
16574
17016
  }
@@ -16589,8 +17031,10 @@ function createRemoveCommand(dependencies = {}) {
16589
17031
  if (options.json) {
16590
17032
  emitError2(options, message);
16591
17033
  } else {
16592
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16593
- `);
17034
+ options.stderr(
17035
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17036
+ `
17037
+ );
16594
17038
  }
16595
17039
  return 1;
16596
17040
  }
@@ -16600,8 +17044,10 @@ function createRemoveCommand(dependencies = {}) {
16600
17044
  }
16601
17045
  if (options.dryRun) {
16602
17046
  if (options.json) {
16603
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
16604
- `);
17047
+ options.stdout(
17048
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
17049
+ `
17050
+ );
16605
17051
  } else {
16606
17052
  const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
16607
17053
  options.stdout(`Would remove worktree at ${worktree.path} (${desc})
@@ -16609,12 +17055,20 @@ function createRemoveCommand(dependencies = {}) {
16609
17055
  }
16610
17056
  return 0;
16611
17057
  }
16612
- 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
+ );
16613
17063
  const hooks = extractHooks(config);
16614
17064
  await runHook(
16615
17065
  hooks.beforeRemove,
16616
17066
  worktree.path,
16617
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename8(repository.repoRoot) },
17067
+ {
17068
+ branch: worktree.branch ?? void 0,
17069
+ path: worktree.path,
17070
+ repo: basename8(repository.repoRoot)
17071
+ },
16618
17072
  options.stderr
16619
17073
  );
16620
17074
  try {
@@ -16630,7 +17084,10 @@ function createRemoveCommand(dependencies = {}) {
16630
17084
  try {
16631
17085
  await forceRemoveWorktree(repository.repoRoot, worktree.path);
16632
17086
  } catch (forceError) {
16633
- 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
+ );
16634
17091
  return 1;
16635
17092
  }
16636
17093
  }
@@ -16645,18 +17102,24 @@ function createRemoveCommand(dependencies = {}) {
16645
17102
  try {
16646
17103
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
16647
17104
  } catch (forceError) {
16648
- options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
16649
- `);
17105
+ options.stderr(
17106
+ `Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
17107
+ `
17108
+ );
16650
17109
  }
16651
17110
  } else {
16652
- options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)
16653
- `);
17111
+ options.stderr(
17112
+ `Branch ${worktree.branch} was not deleted (has unmerged commits)
17113
+ `
17114
+ );
16654
17115
  }
16655
17116
  }
16656
17117
  }
16657
17118
  if (options.json) {
16658
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
16659
- `);
17119
+ options.stdout(
17120
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
17121
+ `
17122
+ );
16660
17123
  } else {
16661
17124
  await writeOutput3(repository.repoRoot, options.stdout);
16662
17125
  }
@@ -16705,7 +17168,11 @@ var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
16705
17168
  async function runRootCommand(options) {
16706
17169
  const repository = await detectRepository(options.cwd);
16707
17170
  if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
16708
- 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
+ );
16709
17176
  return 0;
16710
17177
  }
16711
17178
  options.stdout(`${repository.repoRoot}
@@ -16721,18 +17188,31 @@ async function runStatusCommand(options) {
16721
17188
  worktrees.map(async (worktree) => buildStatusRow(worktree))
16722
17189
  );
16723
17190
  if (options.json) {
16724
- options.stdout(`${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
16725
- `);
17191
+ options.stdout(
17192
+ `${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
17193
+ `
17194
+ );
16726
17195
  return 0;
16727
17196
  }
16728
- options.stdout(`${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
16729
- `);
17197
+ options.stdout(
17198
+ `${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
17199
+ `
17200
+ );
16730
17201
  return 0;
16731
17202
  }
16732
17203
  function formatStatusOutput(repoRoot, currentRoot, rows) {
16733
- const currentWidth = Math.max("CURRENT".length, ...rows.map((row) => row.current ? 1 : 0));
16734
- const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => formatBranch(row.branch).length));
16735
- 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
+ );
16736
17216
  const upstreamWidth = Math.max(
16737
17217
  "UPSTREAM".length,
16738
17218
  ...rows.map((row) => formatUpstreamState2(row.upstream).length)
@@ -16768,7 +17248,9 @@ async function buildStatusRow(worktree) {
16768
17248
  };
16769
17249
  }
16770
17250
  function sortWorktreesByPath(worktrees) {
16771
- return [...worktrees].sort((left, right) => comparePaths(left.path, right.path));
17251
+ return [...worktrees].sort(
17252
+ (left, right) => comparePaths(left.path, right.path)
17253
+ );
16772
17254
  }
16773
17255
  function formatBranch(branch) {
16774
17256
  return branch ?? "(detached)";
@@ -16814,7 +17296,11 @@ function formatUpstreamState2(upstream) {
16814
17296
  // src/sync.ts
16815
17297
  async function runSyncCommand(options) {
16816
17298
  const repository = await detectRepository(options.cwd);
16817
- 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
+ );
16818
17304
  const worktrees = await listWorktrees(options.cwd);
16819
17305
  const remote = resolveConfiguredString2(config.syncRemote) ?? "origin";
16820
17306
  let defaultBranch;
@@ -16823,22 +17309,33 @@ async function runSyncCommand(options) {
16823
17309
  } catch {
16824
17310
  emitError3(options, `Unable to reach remote '${remote}'`);
16825
17311
  if (!options.json) {
16826
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16827
- `);
17312
+ options.stderr(
17313
+ `Hint: Add the remote with: git remote add ${remote} <url>
17314
+ `
17315
+ );
16828
17316
  }
16829
17317
  return 1;
16830
17318
  }
16831
17319
  if (!defaultBranch) {
16832
17320
  emitError3(options, "Unable to determine the default branch for sync.");
16833
17321
  if (!options.json) {
16834
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16835
- `);
17322
+ options.stderr(
17323
+ `Hint: Add the remote with: git remote add ${remote} <url>
17324
+ `
17325
+ );
16836
17326
  }
16837
17327
  return 1;
16838
17328
  }
16839
- const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
17329
+ const targetWorktrees = selectTargetWorktrees(
17330
+ worktrees,
17331
+ repository.currentRoot,
17332
+ options.all
17333
+ );
16840
17334
  if (targetWorktrees === "detached") {
16841
- emitError3(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
17335
+ emitError3(
17336
+ options,
17337
+ `Cannot sync detached worktree: ${repository.currentRoot}`
17338
+ );
16842
17339
  return 1;
16843
17340
  }
16844
17341
  for (const worktree of targetWorktrees) {
@@ -16852,15 +17349,21 @@ async function runSyncCommand(options) {
16852
17349
  } catch {
16853
17350
  emitError3(options, `Failed to fetch from remote '${remote}'`);
16854
17351
  if (!options.json) {
16855
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16856
- `);
17352
+ options.stderr(
17353
+ `Hint: Add the remote with: git remote add ${remote} <url>
17354
+ `
17355
+ );
16857
17356
  }
16858
17357
  return 1;
16859
17358
  }
16860
17359
  const updatedWorktrees = [];
16861
17360
  for (const worktree of targetWorktrees) {
16862
17361
  if (worktree.branch === defaultBranch) {
16863
- await runGit(worktree.path, ["merge", "--ff-only", `${remote}/${defaultBranch}`]);
17362
+ await runGit(worktree.path, [
17363
+ "merge",
17364
+ "--ff-only",
17365
+ `${remote}/${defaultBranch}`
17366
+ ]);
16864
17367
  } else {
16865
17368
  await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
16866
17369
  }
@@ -16871,7 +17374,10 @@ async function runSyncCommand(options) {
16871
17374
  }
16872
17375
  }
16873
17376
  if (options.json) {
16874
- 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
+ }));
16875
17381
  options.stdout(`${JSON.stringify({ updated }, null, 2)}
16876
17382
  `);
16877
17383
  }
@@ -16890,7 +17396,9 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
16890
17396
  if (all) {
16891
17397
  return worktrees.filter((worktree) => worktree.branch !== null).sort((left, right) => comparePaths(left.path, right.path));
16892
17398
  }
16893
- const currentWorktree = worktrees.find((worktree) => worktree.path === currentRoot);
17399
+ const currentWorktree = worktrees.find(
17400
+ (worktree) => worktree.path === currentRoot
17401
+ );
16894
17402
  if (!currentWorktree) {
16895
17403
  return [];
16896
17404
  }
@@ -16903,8 +17411,153 @@ function resolveConfiguredString2(value) {
16903
17411
  return typeof value === "string" && value.length > 0 ? value : null;
16904
17412
  }
16905
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
+
16906
17555
  // src/trigger-hook.ts
16907
- var VALID_HOOKS = ["afterCreate", "afterEnter", "beforeRemove"];
17556
+ var VALID_HOOKS = [
17557
+ "afterCreate",
17558
+ "afterEnter",
17559
+ "beforeRemove"
17560
+ ];
16908
17561
  function isValidHook(hook) {
16909
17562
  return VALID_HOOKS.includes(hook);
16910
17563
  }
@@ -16918,10 +17571,16 @@ async function runTriggerHookCommand(options) {
16918
17571
  }
16919
17572
  const hookName = options.hook;
16920
17573
  const repository = await detectRepository(options.cwd);
16921
- 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
+ );
16922
17579
  const hooks = extractHooks(config);
16923
17580
  const worktrees = await listWorktrees(options.cwd);
16924
- const currentWorktree = worktrees.find((w2) => w2.path === repository.currentRoot);
17581
+ const currentWorktree = worktrees.find(
17582
+ (w2) => w2.path === repository.currentRoot
17583
+ );
16925
17584
  await runHook(
16926
17585
  hooks[hookName],
16927
17586
  repository.currentRoot,
@@ -17001,46 +17660,117 @@ function maybeRegisterCurrentRepo(cwd) {
17001
17660
  detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
17002
17661
  }
17003
17662
  function registerCommands(program2) {
17004
- 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"));
17005
17676
  program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
17006
17677
  program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
17007
- 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"));
17008
- 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"));
17009
17690
  program2.command("history").description("show navigation history").option("--json", "print history as JSON").action(notImplemented("history"));
17010
- 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"));
17011
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"));
17012
17699
  program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
17013
17700
  program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
17014
- 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"));
17015
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"));
17016
- 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"));
17017
- 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"));
17018
- program2.command("trigger-hook <hook>").description("run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree").action(notImplemented("trigger-hook"));
17019
- 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"));
17020
17737
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
17021
17738
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
17022
17739
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
17023
17740
  configCommand.command("unset <key>").description("remove a global config value").action(notImplemented("config unset"));
17024
17741
  }
17025
17742
  function attachCommandActions(program2, options) {
17026
- program2.commands.find((command) => command.name() === "new")?.action(async (branch, commandOptions) => {
17027
- const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, editor: commandOptions.editor, force: commandOptions.force, json: commandOptions.json, open: commandOptions.open });
17028
- if (exitCode !== 0) {
17029
- 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
+ }
17030
17758
  }
17031
- });
17032
- program2.commands.find((command) => command.name() === "init")?.action(async (shell, commandOptions) => {
17033
- const exitCode = await runInitCommand({
17034
- cwd: options.cwd,
17035
- shell,
17036
- stderr: options.stderr,
17037
- stdout: options.stdout,
17038
- write: commandOptions.write
17039
- });
17040
- if (exitCode !== 0) {
17041
- 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
+ }
17042
17772
  }
17043
- });
17773
+ );
17044
17774
  program2.commands.find((command) => command.name() === "completion")?.action(async (shell) => {
17045
17775
  const exitCode = await runCompletionCommand({
17046
17776
  shell,
@@ -17051,32 +17781,93 @@ function attachCommandActions(program2, options) {
17051
17781
  throw commanderExit(exitCode);
17052
17782
  }
17053
17783
  });
17054
- program2.commands.find((command) => command.name() === "pr")?.action(async (number, commandOptions) => {
17055
- 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
+ });
17056
17825
  if (exitCode !== 0) {
17057
17826
  throw commanderExit(exitCode);
17058
17827
  }
17059
17828
  });
17060
- program2.commands.find((command) => command.name() === "back")?.action(async (n, commandOptions) => {
17061
- if (n !== void 0 && !/^\d+$/.test(n)) {
17062
- options.stderr(`gji back: invalid step count: ${n}
17063
- `);
17064
- 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
+ }
17065
17857
  }
17066
- const steps = n !== void 0 ? parseInt(n, 10) : void 0;
17067
- const exitCode = await runBackCommand({
17858
+ );
17859
+ program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17860
+ const exitCode = await runRootCommand({
17068
17861
  cwd: options.cwd,
17069
- n: steps,
17070
17862
  print: commandOptions.print,
17071
- stderr: options.stderr,
17072
17863
  stdout: options.stdout
17073
17864
  });
17074
17865
  if (exitCode !== 0) {
17075
17866
  throw commanderExit(exitCode);
17076
17867
  }
17077
17868
  });
17078
- program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
17079
- const exitCode = await runHistoryCommand({
17869
+ program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17870
+ const exitCode = await runStatusCommand({
17080
17871
  cwd: options.cwd,
17081
17872
  json: commandOptions.json,
17082
17873
  stdout: options.stdout
@@ -17085,25 +17876,26 @@ function attachCommandActions(program2, options) {
17085
17876
  throw commanderExit(exitCode);
17086
17877
  }
17087
17878
  });
17088
- program2.commands.find((command) => command.name() === "open")?.action(async (branch, commandOptions) => {
17089
- const exitCode = await runOpenCommand({
17090
- branch,
17879
+ program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17880
+ const exitCode = await runSyncCommand({
17881
+ all: commandOptions.all,
17091
17882
  cwd: options.cwd,
17092
- editor: commandOptions.editor,
17093
- save: commandOptions.save,
17883
+ json: commandOptions.json,
17094
17884
  stderr: options.stderr,
17095
- stdout: options.stdout,
17096
- workspace: commandOptions.workspace
17885
+ stdout: options.stdout
17097
17886
  });
17098
17887
  if (exitCode !== 0) {
17099
17888
  throw commanderExit(exitCode);
17100
17889
  }
17101
17890
  });
17102
- program2.commands.find((command) => command.name() === "go")?.action(async (branch, commandOptions) => {
17103
- const exitCode = await runGoCommand({
17104
- 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",
17105
17897
  cwd: options.cwd,
17106
- print: commandOptions.print,
17898
+ json: commandOptions.json,
17107
17899
  stderr: options.stderr,
17108
17900
  stdout: options.stdout
17109
17901
  });
@@ -17111,38 +17903,45 @@ function attachCommandActions(program2, options) {
17111
17903
  throw commanderExit(exitCode);
17112
17904
  }
17113
17905
  });
17114
- program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17115
- const exitCode = await runRootCommand({
17906
+ syncFilesCommand?.commands.find((command) => command.name() === "list")?.action(async (commandOptions) => {
17907
+ const exitCode = await runSyncFilesCommand({
17908
+ action: "list",
17116
17909
  cwd: options.cwd,
17117
- print: commandOptions.print,
17910
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17911
+ stderr: options.stderr,
17118
17912
  stdout: options.stdout
17119
17913
  });
17120
17914
  if (exitCode !== 0) {
17121
17915
  throw commanderExit(exitCode);
17122
17916
  }
17123
17917
  });
17124
- program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17125
- const exitCode = await runStatusCommand({
17918
+ syncFilesCommand?.commands.find((command) => command.name() === "add")?.action(async (paths, commandOptions) => {
17919
+ const exitCode = await runSyncFilesCommand({
17920
+ action: "add",
17126
17921
  cwd: options.cwd,
17127
- json: commandOptions.json,
17922
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17923
+ paths,
17924
+ stderr: options.stderr,
17128
17925
  stdout: options.stdout
17129
17926
  });
17130
17927
  if (exitCode !== 0) {
17131
17928
  throw commanderExit(exitCode);
17132
17929
  }
17133
17930
  });
17134
- program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17135
- const exitCode = await runSyncCommand({
17136
- all: commandOptions.all,
17931
+ const runSyncFilesRemoveCommand = async (paths, commandOptions) => {
17932
+ const exitCode = await runSyncFilesCommand({
17933
+ action: "remove",
17137
17934
  cwd: options.cwd,
17138
- json: commandOptions.json,
17935
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17936
+ paths,
17139
17937
  stderr: options.stderr,
17140
17938
  stdout: options.stdout
17141
17939
  });
17142
17940
  if (exitCode !== 0) {
17143
17941
  throw commanderExit(exitCode);
17144
17942
  }
17145
- });
17943
+ };
17944
+ syncFilesCommand?.commands.find((command) => command.name() === "remove")?.action(runSyncFilesRemoveCommand);
17146
17945
  program2.commands.find((command) => command.name() === "ls")?.action(async (commandOptions) => {
17147
17946
  const exitCode = await runLsCommand({
17148
17947
  compact: commandOptions.compact,
@@ -17154,20 +17953,22 @@ function attachCommandActions(program2, options) {
17154
17953
  throw commanderExit(exitCode);
17155
17954
  }
17156
17955
  });
17157
- program2.commands.find((command) => command.name() === "clean")?.action(async (commandOptions) => {
17158
- const exitCode = await runCleanCommand({
17159
- cwd: options.cwd,
17160
- dryRun: commandOptions.dryRun,
17161
- force: commandOptions.force,
17162
- json: commandOptions.json,
17163
- stale: commandOptions.stale,
17164
- stderr: options.stderr,
17165
- stdout: options.stdout
17166
- });
17167
- if (exitCode !== 0) {
17168
- 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
+ }
17169
17970
  }
17170
- });
17971
+ );
17171
17972
  const runRemovalCommand = async (branch, commandOptions = {}) => {
17172
17973
  const exitCode = await runRemoveCommand({
17173
17974
  branch,
@@ -17193,23 +17994,27 @@ function attachCommandActions(program2, options) {
17193
17994
  throw commanderExit(exitCode);
17194
17995
  }
17195
17996
  });
17196
- program2.commands.find((command) => command.name() === "warp")?.action(async (branch, commandOptions) => {
17197
- const newFlag = commandOptions.new;
17198
- const newWorktree = newFlag !== void 0 && newFlag !== false;
17199
- const newBranch = typeof newFlag === "string" ? newFlag : void 0;
17200
- const exitCode = await runWarpCommand({
17201
- branch: newWorktree ? newBranch ?? branch : branch,
17202
- cwd: options.cwd,
17203
- json: commandOptions.json,
17204
- newWorktree,
17205
- stderr: options.stderr,
17206
- stdout: options.stdout
17207
- });
17208
- if (exitCode !== 0) {
17209
- 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
+ }
17210
18013
  }
17211
- });
17212
- const configCommand = program2.commands.find((command) => command.name() === "config");
18014
+ );
18015
+ const configCommand = program2.commands.find(
18016
+ (command) => command.name() === "config"
18017
+ );
17213
18018
  configCommand?.action(async () => {
17214
18019
  const exitCode = await runConfigCommand({
17215
18020
  cwd: options.cwd,
@@ -17291,7 +18096,7 @@ async function main() {
17291
18096
  }
17292
18097
  async function warnIfMissingShellIntegration() {
17293
18098
  try {
17294
- const { config } = await loadGlobalConfig(homedir6());
18099
+ const { config } = await loadGlobalConfig(homedir7());
17295
18100
  if (!config.shellIntegration) {
17296
18101
  const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
17297
18102
  const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";