@solaqua/gji 0.6.2 → 0.7.1

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 (84) 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 +266 -165
  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 +1688 -865
  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.d.ts +3 -3
  25. package/dist/hooks.js +19 -19
  26. package/dist/index.js +13 -9
  27. package/dist/init.d.ts +5 -5
  28. package/dist/init.js +115 -103
  29. package/dist/install-prompt.d.ts +3 -3
  30. package/dist/install-prompt.js +57 -38
  31. package/dist/ls.d.ts +2 -2
  32. package/dist/ls.js +29 -29
  33. package/dist/new.d.ts +2 -2
  34. package/dist/new.js +97 -82
  35. package/dist/open.d.ts +2 -2
  36. package/dist/open.js +24 -21
  37. package/dist/package-manager.js +96 -45
  38. package/dist/pr.d.ts +2 -2
  39. package/dist/pr.js +47 -34
  40. package/dist/remove.d.ts +1 -1
  41. package/dist/remove.js +39 -27
  42. package/dist/repo-registry.js +14 -14
  43. package/dist/repo.js +29 -28
  44. package/dist/root.js +3 -3
  45. package/dist/run-hook.d.ts +6 -0
  46. package/dist/{trigger-hook.js → run-hook.js} +18 -8
  47. package/dist/shell-completion.d.ts +1 -1
  48. package/dist/shell-completion.js +67 -39
  49. package/dist/shell-handoff.js +2 -2
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +4 -4
  52. package/dist/status.d.ts +5 -5
  53. package/dist/status.js +23 -23
  54. package/dist/sync-files-command.d.ts +10 -0
  55. package/dist/sync-files-command.js +137 -0
  56. package/dist/sync.js +23 -15
  57. package/dist/warp.js +37 -33
  58. package/dist/worktree-info.d.ts +9 -9
  59. package/dist/worktree-info.js +31 -29
  60. package/dist/worktree-management.d.ts +1 -1
  61. package/dist/worktree-management.js +26 -11
  62. package/dist/worktree-prompts.js +5 -5
  63. package/man/man1/gji-back.1 +1 -1
  64. package/man/man1/gji-clean.1 +1 -1
  65. package/man/man1/gji-completion.1 +1 -1
  66. package/man/man1/gji-config.1 +1 -1
  67. package/man/man1/gji-go.1 +1 -1
  68. package/man/man1/gji-history.1 +1 -1
  69. package/man/man1/gji-init.1 +1 -1
  70. package/man/man1/gji-ls.1 +1 -1
  71. package/man/man1/gji-new.1 +1 -1
  72. package/man/man1/gji-open.1 +1 -1
  73. package/man/man1/gji-pr.1 +1 -1
  74. package/man/man1/gji-remove.1 +1 -1
  75. package/man/man1/gji-root.1 +1 -1
  76. package/man/man1/gji-run-hook.1 +9 -0
  77. package/man/man1/gji-status.1 +1 -1
  78. package/man/man1/gji-sync-files.1 +23 -0
  79. package/man/man1/gji-sync.1 +1 -1
  80. package/man/man1/gji-warp.1 +1 -1
  81. package/man/man1/gji.1 +10 -4
  82. package/package.json +8 -2
  83. package/dist/trigger-hook.d.ts +0 -6
  84. package/man/man1/gji-trigger-hook.1 +0 -9
@@ -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
  });
@@ -13119,9 +13176,13 @@ function extractHooks(config) {
13119
13176
  }
13120
13177
  const hooks = raw;
13121
13178
  return {
13122
- afterCreate: parseHookCommand(hooks.afterCreate),
13123
- afterEnter: parseHookCommand(hooks.afterEnter),
13124
- beforeRemove: parseHookCommand(hooks.beforeRemove)
13179
+ "after-create": parseHookCommand(
13180
+ hooks["after-create"] ?? hooks.afterCreate
13181
+ ),
13182
+ "after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
13183
+ "before-remove": parseHookCommand(
13184
+ hooks["before-remove"] ?? hooks.beforeRemove
13185
+ )
13125
13186
  };
13126
13187
  }
13127
13188
  function parseHookCommand(value) {
@@ -13132,49 +13193,9 @@ function parseHookCommand(value) {
13132
13193
  return void 0;
13133
13194
  }
13134
13195
 
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
13196
  // src/repo.ts
13176
- import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13177
13197
  import { homedir as homedir3 } from "node:os";
13198
+ import { basename, dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
13178
13199
 
13179
13200
  // src/git.ts
13180
13201
  import { execFile } from "node:child_process";
@@ -13190,7 +13211,11 @@ async function runGit(cwd, args) {
13190
13211
  }
13191
13212
  }
13192
13213
  async function readWorktreeHealth(cwd) {
13193
- const { stdout } = await execFileAsync("git", ["status", "--porcelain=v2", "--branch"], { cwd });
13214
+ const { stdout } = await execFileAsync(
13215
+ "git",
13216
+ ["status", "--porcelain=v2", "--branch"],
13217
+ { cwd }
13218
+ );
13194
13219
  return parseWorktreeHealth(stdout);
13195
13220
  }
13196
13221
  async function isDirtyWorktree(cwd) {
@@ -13199,7 +13224,9 @@ async function isDirtyWorktree(cwd) {
13199
13224
  }
13200
13225
  async function isBranchMergedInto(cwd, branch, base = "HEAD") {
13201
13226
  try {
13202
- await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], { cwd });
13227
+ await execFileAsync("git", ["merge-base", "--is-ancestor", branch, base], {
13228
+ cwd
13229
+ });
13203
13230
  return true;
13204
13231
  } catch (error) {
13205
13232
  if (hasExitCode(error, 1)) {
@@ -13209,7 +13236,11 @@ async function isBranchMergedInto(cwd, branch, base = "HEAD") {
13209
13236
  }
13210
13237
  }
13211
13238
  async function resolveRemoteDefaultBranch(cwd, remote) {
13212
- const { stdout } = await execFileAsync("git", ["ls-remote", "--symref", remote, "HEAD"], { cwd });
13239
+ const { stdout } = await execFileAsync(
13240
+ "git",
13241
+ ["ls-remote", "--symref", remote, "HEAD"],
13242
+ { cwd }
13243
+ );
13213
13244
  const refLine = stdout.split("\n").find((line) => line.startsWith("ref: refs/heads/"));
13214
13245
  if (!refLine) {
13215
13246
  return null;
@@ -13219,7 +13250,11 @@ async function resolveRemoteDefaultBranch(cwd, remote) {
13219
13250
  }
13220
13251
  async function readBranchLastCommitTimestamp(cwd, branch) {
13221
13252
  try {
13222
- const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%ct", branch], { cwd });
13253
+ const { stdout } = await execFileAsync(
13254
+ "git",
13255
+ ["log", "-1", "--format=%ct", branch],
13256
+ { cwd }
13257
+ );
13223
13258
  const timestamp = Number(stdout.trim());
13224
13259
  return Number.isFinite(timestamp) ? timestamp : null;
13225
13260
  } catch {
@@ -13283,7 +13318,9 @@ function resolveWorktreePath(repoRoot, branch, basePath) {
13283
13318
  throw new Error("Branch name must not be empty.");
13284
13319
  }
13285
13320
  if (segments.some((segment) => segment === "." || segment === "..")) {
13286
- throw new Error(`Branch name '${branch}' contains an invalid path segment.`);
13321
+ throw new Error(
13322
+ `Branch name '${branch}' contains an invalid path segment.`
13323
+ );
13287
13324
  }
13288
13325
  const base = basePath ? expandTildeInPath(basePath) : join3(dirname3(repoRoot), "worktrees", basename(repoRoot));
13289
13326
  return join3(base, ...segments);
@@ -13403,17 +13440,27 @@ async function runBackCommand(options) {
13403
13440
  }
13404
13441
  if (!target) {
13405
13442
  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");
13443
+ options.stderr(
13444
+ "Hint: Use 'gji go', 'gji new', or 'gji pr' to navigate between worktrees\n"
13445
+ );
13407
13446
  return 1;
13408
13447
  }
13409
13448
  try {
13410
13449
  const repository = await detectRepository(target.path);
13411
- const config = await loadEffectiveConfig(repository.repoRoot, options.home, options.stderr);
13450
+ const config = await loadEffectiveConfig(
13451
+ repository.repoRoot,
13452
+ options.home,
13453
+ options.stderr
13454
+ );
13412
13455
  const hooks = extractHooks(config);
13413
13456
  await runHook(
13414
- hooks.afterEnter,
13457
+ hooks["after-enter"],
13415
13458
  target.path,
13416
- { branch: target.branch ?? void 0, path: target.path, repo: basename2(repository.repoRoot) },
13459
+ {
13460
+ branch: target.branch ?? void 0,
13461
+ path: target.path,
13462
+ repo: basename2(repository.repoRoot)
13463
+ },
13417
13464
  options.stderr
13418
13465
  );
13419
13466
  } catch {
@@ -13427,7 +13474,9 @@ function formatHistoryList(history, cwd) {
13427
13474
  "BRANCH".length,
13428
13475
  ...history.map((e2) => (e2.branch ?? "(detached)").length)
13429
13476
  );
13430
- const lines = [" " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"];
13477
+ const lines = [
13478
+ " " + "BRANCH".padEnd(branchWidth) + " WHEN PATH"
13479
+ ];
13431
13480
  for (const entry of history) {
13432
13481
  const isCurrent = entry.path === cwd;
13433
13482
  const branch = (entry.branch ?? "(detached)").padEnd(branchWidth);
@@ -14121,7 +14170,10 @@ function formatLastCommit(timestampSeconds) {
14121
14170
  return timestampSeconds === null ? "n/a" : formatRelativeAge(timestampSeconds);
14122
14171
  }
14123
14172
  function formatRelativeAge(timestampSeconds) {
14124
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1e3) - timestampSeconds);
14173
+ const ageSeconds = Math.max(
14174
+ 0,
14175
+ Math.floor(Date.now() / 1e3) - timestampSeconds
14176
+ );
14125
14177
  const units = [
14126
14178
  { label: "y", seconds: 365 * 24 * 60 * 60 },
14127
14179
  { label: "mo", seconds: 30 * 24 * 60 * 60 },
@@ -14154,16 +14206,28 @@ async function loadLinkedWorktrees(cwd) {
14154
14206
  };
14155
14207
  }
14156
14208
  async function removeWorktree(repoRoot, worktreePath) {
14157
- await execFileAsync2("git", ["worktree", "remove", worktreePath], { cwd: repoRoot, env: GIT_ENV });
14209
+ await execFileAsync2("git", ["worktree", "remove", worktreePath], {
14210
+ cwd: repoRoot,
14211
+ env: GIT_ENV
14212
+ });
14158
14213
  }
14159
14214
  async function forceRemoveWorktree(repoRoot, worktreePath) {
14160
- await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, env: GIT_ENV });
14215
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
14216
+ cwd: repoRoot,
14217
+ env: GIT_ENV
14218
+ });
14161
14219
  }
14162
14220
  async function deleteBranch(repoRoot, branch) {
14163
- await execFileAsync2("git", ["branch", "-d", branch], { cwd: repoRoot, env: GIT_ENV });
14221
+ await execFileAsync2("git", ["branch", "-d", branch], {
14222
+ cwd: repoRoot,
14223
+ env: GIT_ENV
14224
+ });
14164
14225
  }
14165
14226
  async function forceDeleteBranch(repoRoot, branch) {
14166
- await execFileAsync2("git", ["branch", "-D", branch], { cwd: repoRoot, env: GIT_ENV });
14227
+ await execFileAsync2("git", ["branch", "-D", branch], {
14228
+ cwd: repoRoot,
14229
+ env: GIT_ENV
14230
+ });
14167
14231
  }
14168
14232
  function isWorktreeDirtyError(error) {
14169
14233
  return hasStderr(error) && error.stderr.includes("contains modified or untracked files");
@@ -14202,12 +14266,18 @@ function createCleanCommand(dependencies = {}) {
14202
14266
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
14203
14267
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
14204
14268
  return async function runCleanCommand2(options) {
14205
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
14269
+ const { linkedWorktrees, repository } = await loadLinkedWorktrees(
14270
+ options.cwd
14271
+ );
14206
14272
  const linkedCleanupCandidates = linkedWorktrees.filter(
14207
14273
  (worktree) => worktree.path !== repository.currentRoot
14208
14274
  );
14209
14275
  const staleBaseRef = options.stale ? await resolveStaleBaseRef(repository.repoRoot, options.stderr) : null;
14210
- const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(repository.repoRoot, linkedCleanupCandidates, staleBaseRef) : linkedCleanupCandidates;
14276
+ const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
14277
+ repository.repoRoot,
14278
+ linkedCleanupCandidates,
14279
+ staleBaseRef
14280
+ ) : linkedCleanupCandidates;
14211
14281
  if (cleanupCandidates.length === 0) {
14212
14282
  if (options.stale) {
14213
14283
  emitNoStaleCandidates(options);
@@ -14221,8 +14291,10 @@ function createCleanCommand(dependencies = {}) {
14221
14291
  if (options.json) {
14222
14292
  emitError(options, message);
14223
14293
  } else {
14224
- options.stderr(`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
14225
- `);
14294
+ options.stderr(
14295
+ `gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
14296
+ `
14297
+ );
14226
14298
  }
14227
14299
  return 1;
14228
14300
  }
@@ -14232,7 +14304,10 @@ function createCleanCommand(dependencies = {}) {
14232
14304
  options.stderr("Aborted\n");
14233
14305
  return 1;
14234
14306
  }
14235
- const selectedWorktrees = resolveSelectedWorktrees(cleanupCandidates, selections);
14307
+ const selectedWorktrees = resolveSelectedWorktrees(
14308
+ cleanupCandidates,
14309
+ selections
14310
+ );
14236
14311
  if (selectedWorktrees.length !== selections.length) {
14237
14312
  options.stderr("Selected worktree no longer exists\n");
14238
14313
  return 1;
@@ -14247,13 +14322,19 @@ function createCleanCommand(dependencies = {}) {
14247
14322
  }
14248
14323
  if (options.dryRun) {
14249
14324
  if (options.json) {
14250
- const removed = selectedWorktreeInfos.map((info) => serializeWorktreeInfo(info));
14251
- options.stdout(`${JSON.stringify({ removed, dryRun: true }, null, 2)}
14252
- `);
14325
+ const removed = selectedWorktreeInfos.map(
14326
+ (info) => serializeWorktreeInfo(info)
14327
+ );
14328
+ options.stdout(
14329
+ `${JSON.stringify({ removed, dryRun: true }, null, 2)}
14330
+ `
14331
+ );
14253
14332
  } else {
14254
14333
  for (const info of selectedWorktreeInfos) {
14255
- options.stdout(`Would remove worktree at ${info.path} (${formatCleanInfo(info)})
14256
- `);
14334
+ options.stdout(
14335
+ `Would remove worktree at ${info.path} (${formatCleanInfo(info)})
14336
+ `
14337
+ );
14257
14338
  }
14258
14339
  }
14259
14340
  return 0;
@@ -14261,9 +14342,15 @@ function createCleanCommand(dependencies = {}) {
14261
14342
  const removedPaths = [];
14262
14343
  const removedWorktrees = [];
14263
14344
  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
- `);
14345
+ if (options.stale && !await isStaleCleanupCandidate(
14346
+ repository.repoRoot,
14347
+ worktree,
14348
+ staleBaseRef
14349
+ )) {
14350
+ options.stderr(
14351
+ `Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14352
+ `
14353
+ );
14267
14354
  continue;
14268
14355
  }
14269
14356
  try {
@@ -14273,8 +14360,10 @@ function createCleanCommand(dependencies = {}) {
14273
14360
  throw error;
14274
14361
  }
14275
14362
  if (options.stale) {
14276
- options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14277
- `);
14363
+ options.stderr(
14364
+ `Skipped ${worktree.path}: no longer a safe stale cleanup candidate
14365
+ `
14366
+ );
14278
14367
  continue;
14279
14368
  }
14280
14369
  if (!options.force && !await confirmForceRemoveWorktree(worktree.path)) {
@@ -14288,7 +14377,10 @@ function createCleanCommand(dependencies = {}) {
14288
14377
  if (!options.json) {
14289
14378
  reportRemovedPaths(removedPaths, options.stderr);
14290
14379
  }
14291
- emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
14380
+ emitError(
14381
+ options,
14382
+ `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`
14383
+ );
14292
14384
  return 1;
14293
14385
  }
14294
14386
  }
@@ -14305,12 +14397,16 @@ function createCleanCommand(dependencies = {}) {
14305
14397
  try {
14306
14398
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
14307
14399
  } catch (forceError) {
14308
- options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
14309
- `);
14400
+ options.stderr(
14401
+ `Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}
14402
+ `
14403
+ );
14310
14404
  }
14311
14405
  } else {
14312
- options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)
14313
- `);
14406
+ options.stderr(
14407
+ `Branch ${worktree.branch} was not deleted (has unmerged commits)
14408
+ `
14409
+ );
14314
14410
  }
14315
14411
  }
14316
14412
  }
@@ -14335,19 +14431,30 @@ async function filterStaleCleanupCandidates(repoRoot, worktrees, baseBranch) {
14335
14431
  return [];
14336
14432
  }
14337
14433
  const results = await Promise.all(
14338
- worktrees.map((worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch))
14434
+ worktrees.map(
14435
+ (worktree) => isStaleCleanupCandidate(repoRoot, worktree, baseBranch)
14436
+ )
14339
14437
  );
14340
14438
  return worktrees.filter((_3, index) => results[index]);
14341
14439
  }
14342
14440
  async function resolveStaleBaseRef(repoRoot, stderr) {
14343
14441
  const config = await loadEffectiveConfig(repoRoot, void 0, stderr);
14344
14442
  const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
14345
- const configuredDefaultBranch = resolveConfiguredString(config.syncDefaultBranch);
14443
+ const configuredDefaultBranch = resolveConfiguredString(
14444
+ config.syncDefaultBranch
14445
+ );
14346
14446
  if (configuredDefaultBranch) {
14347
- return await resolveFetchedRemoteRef(repoRoot, remote, configuredDefaultBranch);
14447
+ return await resolveFetchedRemoteRef(
14448
+ repoRoot,
14449
+ remote,
14450
+ configuredDefaultBranch
14451
+ );
14348
14452
  }
14349
14453
  try {
14350
- const remoteDefaultBranch = await resolveRemoteDefaultBranch(repoRoot, remote);
14454
+ const remoteDefaultBranch = await resolveRemoteDefaultBranch(
14455
+ repoRoot,
14456
+ remote
14457
+ );
14351
14458
  return remoteDefaultBranch === null ? null : await resolveFetchedRemoteRef(repoRoot, remote, remoteDefaultBranch);
14352
14459
  } catch {
14353
14460
  return null;
@@ -14442,14 +14549,22 @@ async function defaultPromptForWorktrees(worktrees) {
14442
14549
  return pD(choice) ? null : choice;
14443
14550
  }
14444
14551
  async function defaultConfirmRemoval(worktrees) {
14445
- const branchCount = worktrees.filter((worktree) => worktree.branch !== null).length;
14552
+ const branchCount = worktrees.filter(
14553
+ (worktree) => worktree.branch !== null
14554
+ ).length;
14446
14555
  const detachedCount = worktrees.length - branchCount;
14447
- const messageParts = [`Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`];
14556
+ const messageParts = [
14557
+ `Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? "" : "s"}`
14558
+ ];
14448
14559
  if (branchCount > 0) {
14449
- messageParts.push(`delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`);
14560
+ messageParts.push(
14561
+ `delete ${branchCount} branch${branchCount === 1 ? "" : "es"}`
14562
+ );
14450
14563
  }
14451
14564
  if (detachedCount > 0) {
14452
- messageParts.push(`remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`);
14565
+ messageParts.push(
14566
+ `remove ${detachedCount} detached worktree${detachedCount === 1 ? "" : "s"}`
14567
+ );
14453
14568
  }
14454
14569
  const choice = await ye({
14455
14570
  active: "Yes",
@@ -14460,25 +14575,35 @@ async function defaultConfirmRemoval(worktrees) {
14460
14575
  return !pD(choice) && choice;
14461
14576
  }
14462
14577
 
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;
14578
+ // src/shell.ts
14579
+ function resolveSupportedShell(requestedShell, detectedShell) {
14580
+ const requested = normalizeShell(requestedShell);
14581
+ if (requested) {
14582
+ return requested;
14470
14583
  }
14471
- if (history.length === 0) {
14472
- options.stdout("No navigation history.\n");
14473
- return 0;
14584
+ return normalizeShell(detectedShell);
14585
+ }
14586
+ function normalizeShell(value) {
14587
+ if (!value) {
14588
+ return null;
14589
+ }
14590
+ const candidate = value.split("/").at(-1)?.toLowerCase();
14591
+ switch (candidate) {
14592
+ case "bash":
14593
+ case "fish":
14594
+ case "zsh":
14595
+ return candidate;
14596
+ default:
14597
+ return null;
14474
14598
  }
14475
- options.stdout(formatHistoryList(history, options.cwd));
14476
- return 0;
14477
14599
  }
14478
14600
 
14479
14601
  // src/shell-completion.ts
14480
14602
  var TOP_LEVEL_COMMANDS = [
14481
- { name: "new", description: "create a new branch or detached linked worktree" },
14603
+ {
14604
+ name: "new",
14605
+ description: "create a new branch or detached linked worktree"
14606
+ },
14482
14607
  { name: "init", description: "print or install shell integration" },
14483
14608
  { name: "completion", description: "print shell completion definitions" },
14484
14609
  { name: "pr", description: "fetch a pull request into a linked worktree" },
@@ -14490,16 +14615,26 @@ var TOP_LEVEL_COMMANDS = [
14490
14615
  { name: "root", description: "print the main repository root path" },
14491
14616
  { name: "status", description: "summarize repository and worktree health" },
14492
14617
  { name: "sync", description: "fetch and update one or all worktrees" },
14618
+ {
14619
+ name: "sync-files",
14620
+ description: "manage local files copied into new worktrees"
14621
+ },
14493
14622
  { name: "ls", description: "list active worktrees" },
14494
14623
  { name: "clean", description: "interactively prune linked worktrees" },
14495
- { name: "remove", description: "remove a linked worktree and delete its branch when present" },
14624
+ {
14625
+ name: "remove",
14626
+ description: "remove a linked worktree and delete its branch when present"
14627
+ },
14496
14628
  { name: "rm", description: "alias of remove" },
14497
- { name: "trigger-hook", description: "run a named hook in the current worktree" },
14629
+ {
14630
+ name: "run-hook",
14631
+ description: "run a named hook in the current worktree"
14632
+ },
14498
14633
  { name: "warp", description: "jump to any worktree across all known repos" },
14499
14634
  { name: "config", description: "manage global config defaults" }
14500
14635
  ];
14501
14636
  var SHELL_NAMES = ["bash", "fish", "zsh"];
14502
- var HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
14637
+ var HOOK_NAMES = ["after-create", "after-enter", "before-remove"];
14503
14638
  var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
14504
14639
  function renderShellCompletion(shell) {
14505
14640
  switch (shell) {
@@ -14512,7 +14647,9 @@ function renderShellCompletion(shell) {
14512
14647
  }
14513
14648
  }
14514
14649
  function renderBashCompletion() {
14515
- const topLevelCommands = TOP_LEVEL_COMMANDS.map((command) => command.name).join(" ");
14650
+ const topLevelCommands = TOP_LEVEL_COMMANDS.map(
14651
+ (command) => command.name
14652
+ ).join(" ");
14516
14653
  const shells = SHELL_NAMES.join(" ");
14517
14654
  const hooks = HOOK_NAMES.join(" ");
14518
14655
  const configKeys = CONFIG_KEYS.join(" ");
@@ -14566,6 +14703,12 @@ _gji_completion() {
14566
14703
  sync)
14567
14704
  COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
14568
14705
  ;;
14706
+ sync-files)
14707
+ if [ "$COMP_CWORD" -eq 2 ]; then
14708
+ COMPREPLY=( $(compgen -W "list add remove rm --json --help" -- "$cur") )
14709
+ return 0
14710
+ fi
14711
+ ;;
14569
14712
  ls)
14570
14713
  COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
14571
14714
  ;;
@@ -14575,7 +14718,7 @@ _gji_completion() {
14575
14718
  remove|rm)
14576
14719
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
14577
14720
  ;;
14578
- trigger-hook)
14721
+ run-hook)
14579
14722
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
14580
14723
  ;;
14581
14724
  warp)
@@ -14613,7 +14756,7 @@ function renderFishCompletion() {
14613
14756
  (shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`
14614
14757
  ).join("\n");
14615
14758
  const hookLines = HOOK_NAMES.map(
14616
- (hook) => `complete -c gji -n '__fish_seen_subcommand_from trigger-hook' -a '${hook}' -d 'hook'`
14759
+ (hook) => `complete -c gji -n '__fish_seen_subcommand_from run-hook' -a '${hook}' -d 'hook'`
14617
14760
  ).join("\n");
14618
14761
  const configKeyLines = CONFIG_KEYS.map(
14619
14762
  (key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`
@@ -14679,6 +14822,12 @@ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print reposi
14679
14822
  complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
14680
14823
  complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
14681
14824
 
14825
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -a 'list add remove rm' -d 'sync-files action'
14826
+ complete -c gji -n '__fish_seen_subcommand_from sync-files' -l json -d 'emit JSON instead of human-readable output'
14827
+ 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'
14828
+ 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'
14829
+ 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'
14830
+
14682
14831
  complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
14683
14832
  complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
14684
14833
 
@@ -14760,6 +14909,9 @@ case "\${words[2]}" in
14760
14909
  sync)
14761
14910
  _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14762
14911
  ;;
14912
+ sync-files)
14913
+ _arguments '--json[emit JSON instead of human-readable output]' '2:action:(list add remove rm)' '*:path: '
14914
+ ;;
14763
14915
  ls)
14764
14916
  _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14765
14917
  ;;
@@ -14769,7 +14921,7 @@ case "\${words[2]}" in
14769
14921
  remove|rm)
14770
14922
  _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
14771
14923
  ;;
14772
- trigger-hook)
14924
+ run-hook)
14773
14925
  _arguments "2:hook:(${hooks})"
14774
14926
  ;;
14775
14927
  warp)
@@ -14806,29 +14958,6 @@ function escapeSingleQuotes(value) {
14806
14958
  return value.replace(/'/g, `'\\''`);
14807
14959
  }
14808
14960
 
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
14961
  // src/completion.ts
14833
14962
  async function runCompletionCommand(options) {
14834
14963
  const shell = resolveSupportedShell(options.shell, process.env.SHELL);
@@ -14860,7 +14989,10 @@ async function runConfigCommand(options) {
14860
14989
  }
14861
14990
  case "set":
14862
14991
  if (options.key && options.value !== void 0) {
14863
- await updateGlobalConfigKey(options.key, parseConfigValue(options.value));
14992
+ await updateGlobalConfigKey(
14993
+ options.key,
14994
+ parseConfigValue(options.value)
14995
+ );
14864
14996
  return 0;
14865
14997
  }
14866
14998
  break;
@@ -14888,19 +15020,72 @@ import { realpath as realpath2 } from "node:fs/promises";
14888
15020
  import { basename as basename5, resolve as resolve5 } from "node:path";
14889
15021
 
14890
15022
  // src/new.ts
15023
+ import { execFile as execFile3 } from "node:child_process";
14891
15024
  import { mkdir as mkdir4 } from "node:fs/promises";
14892
15025
  import { basename as basename3, dirname as dirname5 } from "node:path";
14893
- import { execFile as execFile3 } from "node:child_process";
14894
15026
  import { promisify as promisify4 } from "node:util";
14895
15027
 
15028
+ // src/conflict.ts
15029
+ import { constants } from "node:fs";
15030
+ import { access as access2 } from "node:fs/promises";
15031
+ async function pathExists(path9) {
15032
+ try {
15033
+ await access2(path9, constants.F_OK);
15034
+ return true;
15035
+ } catch {
15036
+ return false;
15037
+ }
15038
+ }
15039
+ async function promptForPathConflict(path9) {
15040
+ const choice = await ve({
15041
+ message: `Target path already exists: ${path9}`,
15042
+ options: [
15043
+ {
15044
+ value: "abort",
15045
+ label: "Abort",
15046
+ hint: "Keep the existing directory untouched"
15047
+ },
15048
+ {
15049
+ value: "reuse",
15050
+ label: "Reuse path",
15051
+ hint: "Print the existing path and stop"
15052
+ }
15053
+ ]
15054
+ });
15055
+ if (pD(choice)) {
15056
+ return "abort";
15057
+ }
15058
+ return choice;
15059
+ }
15060
+
14896
15061
  // src/editor.ts
14897
15062
  import { spawn as spawn3 } from "node:child_process";
14898
15063
  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 },
15064
+ {
15065
+ cli: "cursor",
15066
+ name: "Cursor",
15067
+ newWindowFlag: "--new-window",
15068
+ supportsWorkspace: true
15069
+ },
15070
+ {
15071
+ cli: "code",
15072
+ name: "VS Code",
15073
+ newWindowFlag: "--new-window",
15074
+ supportsWorkspace: true
15075
+ },
15076
+ {
15077
+ cli: "windsurf",
15078
+ name: "Windsurf",
15079
+ newWindowFlag: "--new-window",
15080
+ supportsWorkspace: true
15081
+ },
14902
15082
  { cli: "zed", name: "Zed", supportsWorkspace: false },
14903
- { cli: "subl", name: "Sublime Text", newWindowFlag: "--new-window", supportsWorkspace: false }
15083
+ {
15084
+ cli: "subl",
15085
+ name: "Sublime Text",
15086
+ newWindowFlag: "--new-window",
15087
+ supportsWorkspace: false
15088
+ }
14904
15089
  ];
14905
15090
  async function defaultSpawnEditor(cli, args) {
14906
15091
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
@@ -14916,13 +15101,7 @@ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
14916
15101
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
14917
15102
  async function syncFiles(mainRoot, targetPath, patterns) {
14918
15103
  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
- }
15104
+ const normalized = validateSyncFilePattern(pattern);
14926
15105
  const sourcePath = join4(mainRoot, normalized);
14927
15106
  const destPath = join4(targetPath, normalized);
14928
15107
  const sourceExists = await fileExists(sourcePath);
@@ -14937,6 +15116,20 @@ async function syncFiles(mainRoot, targetPath, patterns) {
14937
15116
  await copyFile(sourcePath, destPath);
14938
15117
  }
14939
15118
  }
15119
+ function validateSyncFilePattern(pattern) {
15120
+ if (isAbsolute2(pattern)) {
15121
+ throw new Error(
15122
+ `syncFiles: pattern must be a relative path, got: ${pattern}`
15123
+ );
15124
+ }
15125
+ const normalized = normalize(pattern);
15126
+ if (normalized.startsWith("..")) {
15127
+ throw new Error(
15128
+ `syncFiles: pattern must not contain '..' segments, got: ${pattern}`
15129
+ );
15130
+ }
15131
+ return normalized;
15132
+ }
14940
15133
  async function fileExists(path9) {
14941
15134
  try {
14942
15135
  await stat(path9);
@@ -14956,7 +15149,7 @@ function isNotFoundError(error) {
14956
15149
  import { spawn as spawn4 } from "node:child_process";
14957
15150
 
14958
15151
  // src/package-manager.ts
14959
- import { access as access2, readdir } from "node:fs/promises";
15152
+ import { access as access3, readdir } from "node:fs/promises";
14960
15153
  import { join as join5 } from "node:path";
14961
15154
  var ENTRIES = [
14962
15155
  // JavaScript / TypeScript
@@ -14970,10 +15163,22 @@ var ENTRIES = [
14970
15163
  { name: "uv", signals: ["uv.lock"], command: "uv sync" },
14971
15164
  { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14972
15165
  { 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" },
15166
+ {
15167
+ name: "conda-lock",
15168
+ signals: ["conda-lock.yml"],
15169
+ command: "conda-lock install"
15170
+ },
15171
+ {
15172
+ name: "conda",
15173
+ signals: ["environment.yml"],
15174
+ command: "conda env update --file environment.yml"
15175
+ },
14975
15176
  // R
14976
- { name: "renv", signals: ["renv.lock"], command: "Rscript -e 'renv::restore()'" },
15177
+ {
15178
+ name: "renv",
15179
+ signals: ["renv.lock"],
15180
+ command: "Rscript -e 'renv::restore()'"
15181
+ },
14977
15182
  // Rust
14978
15183
  { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
14979
15184
  // Go
@@ -14990,25 +15195,56 @@ var ENTRIES = [
14990
15195
  // Java / Kotlin / Scala
14991
15196
  { name: "maven", signals: ["pom.xml"], command: "mvn install" },
14992
15197
  { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
14993
- { name: "gradle", signals: ["build.gradle", "build.gradle.kts"], command: "gradle build" },
15198
+ {
15199
+ name: "gradle",
15200
+ signals: ["build.gradle", "build.gradle.kts"],
15201
+ command: "gradle build"
15202
+ },
14994
15203
  { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
14995
15204
  // .NET (C# / F# / VB)
14996
- { name: "dotnet", signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"], command: "dotnet restore", glob: true },
15205
+ {
15206
+ name: "dotnet",
15207
+ signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
15208
+ command: "dotnet restore",
15209
+ glob: true
15210
+ },
14997
15211
  // Swift
14998
- { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
15212
+ {
15213
+ name: "swift",
15214
+ signals: ["Package.swift"],
15215
+ command: "swift package resolve"
15216
+ },
14999
15217
  // Haskell
15000
15218
  { 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 },
15219
+ {
15220
+ name: "cabal",
15221
+ signals: ["cabal.project"],
15222
+ command: "cabal install --only-dependencies"
15223
+ },
15224
+ {
15225
+ name: "cabal",
15226
+ signals: ["*.cabal"],
15227
+ command: "cabal install --only-dependencies",
15228
+ glob: true
15229
+ },
15003
15230
  // Clojure
15004
15231
  { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
15005
15232
  { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
15006
15233
  // OCaml
15007
15234
  { name: "dune", signals: ["dune-project"], command: "dune build" },
15008
15235
  // Julia
15009
- { name: "julia", signals: ["Manifest.toml"], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
15236
+ {
15237
+ name: "julia",
15238
+ signals: ["Manifest.toml"],
15239
+ command: "julia --project -e 'using Pkg; Pkg.instantiate()'"
15240
+ },
15010
15241
  // Nim
15011
- { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
15242
+ {
15243
+ name: "nimble",
15244
+ signals: ["*.nimble"],
15245
+ command: "nimble install",
15246
+ glob: true
15247
+ },
15012
15248
  // Crystal
15013
15249
  { name: "shards", signals: ["shard.yml"], command: "shards install" },
15014
15250
  // Perl
@@ -15017,12 +15253,20 @@ var ENTRIES = [
15017
15253
  { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
15018
15254
  // C / C++
15019
15255
  { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
15020
- { name: "conan", signals: ["conanfile.py", "conanfile.txt"], command: "conan install ." },
15256
+ {
15257
+ name: "conan",
15258
+ signals: ["conanfile.py", "conanfile.txt"],
15259
+ command: "conan install ."
15260
+ },
15021
15261
  // Nix
15022
15262
  { name: "nix", signals: ["flake.nix"], command: "nix develop" },
15023
15263
  { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
15024
15264
  // Terraform / OpenTofu
15025
- { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
15265
+ {
15266
+ name: "terraform",
15267
+ signals: ["terraform.lock.hcl"],
15268
+ command: "terraform init"
15269
+ }
15026
15270
  ];
15027
15271
  async function detectPackageManager(repoRoot) {
15028
15272
  for (const entry of ENTRIES) {
@@ -15036,7 +15280,7 @@ async function detectPackageManager(repoRoot) {
15036
15280
  async function matchesExact(repoRoot, signals) {
15037
15281
  for (const signal of signals) {
15038
15282
  try {
15039
- await access2(join5(repoRoot, signal));
15283
+ await access3(join5(repoRoot, signal));
15040
15284
  return true;
15041
15285
  } catch {
15042
15286
  }
@@ -15063,8 +15307,7 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15063
15307
  if (isHeadless() || nonInteractive) {
15064
15308
  return;
15065
15309
  }
15066
- const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15067
- if (isConfiguredHookCommand(hooks?.afterCreate)) {
15310
+ if (extractHooks(config)["after-create"]) {
15068
15311
  return;
15069
15312
  }
15070
15313
  if (config.skipInstallPrompt === true) {
@@ -15085,8 +15328,10 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15085
15328
  try {
15086
15329
  await runner(pm.installCommand, worktreePath, stderr);
15087
15330
  } catch (error) {
15088
- stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15089
- `);
15331
+ stderr(
15332
+ `gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15333
+ `
15334
+ );
15090
15335
  }
15091
15336
  }
15092
15337
  const saveGlobal = config.installSaveTarget === "global";
@@ -15095,16 +15340,35 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15095
15340
  if (choice === "always") {
15096
15341
  try {
15097
15342
  if (saveGlobal) {
15098
- const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15099
- await writeGlobalKey(repoRoot, "hooks", { ...existingHooks, afterCreate: pm.installCommand });
15343
+ const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
15344
+ const existing = extractHooks({ hooks: existingRaw });
15345
+ await writeGlobalKey(repoRoot, "hooks", {
15346
+ ...existing["after-enter"] !== void 0 && {
15347
+ "after-enter": existing["after-enter"]
15348
+ },
15349
+ ...existing["before-remove"] !== void 0 && {
15350
+ "before-remove": existing["before-remove"]
15351
+ },
15352
+ "after-create": pm.installCommand
15353
+ });
15100
15354
  } else {
15101
15355
  const { config: localConfig } = await loadConfig(repoRoot);
15102
- const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15103
- await writeKey(repoRoot, "hooks", { ...existingLocalHooks, afterCreate: pm.installCommand });
15356
+ const existing = extractHooks(localConfig);
15357
+ await writeKey(repoRoot, "hooks", {
15358
+ ...existing["after-enter"] !== void 0 && {
15359
+ "after-enter": existing["after-enter"]
15360
+ },
15361
+ ...existing["before-remove"] !== void 0 && {
15362
+ "before-remove": existing["before-remove"]
15363
+ },
15364
+ "after-create": pm.installCommand
15365
+ });
15104
15366
  }
15105
15367
  } catch (error) {
15106
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15107
- `);
15368
+ stderr(
15369
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15370
+ `
15371
+ );
15108
15372
  }
15109
15373
  }
15110
15374
  if (choice === "never") {
@@ -15115,14 +15379,20 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15115
15379
  await writeKey(repoRoot, "skipInstallPrompt", true);
15116
15380
  }
15117
15381
  } catch (error) {
15118
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15119
- `);
15382
+ stderr(
15383
+ `gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15384
+ `
15385
+ );
15120
15386
  }
15121
15387
  }
15122
15388
  }
15123
15389
  async function defaultRunInstallCommand(command, cwd, stderr) {
15124
15390
  await new Promise((resolve6, reject) => {
15125
- const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15391
+ const child = spawn4(command, {
15392
+ cwd,
15393
+ shell: true,
15394
+ stdio: ["ignore", "inherit", "pipe"]
15395
+ });
15126
15396
  child.stderr.on("data", (chunk) => {
15127
15397
  stderr(chunk.toString());
15128
15398
  });
@@ -15156,8 +15426,12 @@ async function defaultPromptForInstallChoice(pm) {
15156
15426
  options: [
15157
15427
  { value: "yes", label: "Yes", hint: "run once" },
15158
15428
  { value: "no", label: "No", hint: "skip this time" },
15159
- { value: "always", label: "Always", hint: "save as afterCreate hook" },
15160
- { value: "never", label: "Never", hint: "disable this prompt for this repo" }
15429
+ { value: "always", label: "Always", hint: "save as after-create hook" },
15430
+ {
15431
+ value: "never",
15432
+ label: "Never",
15433
+ hint: "disable this prompt for this repo"
15434
+ }
15161
15435
  ]
15162
15436
  });
15163
15437
  if (pD(choice)) {
@@ -15168,35 +15442,6 @@ async function defaultPromptForInstallChoice(pm) {
15168
15442
  function isPlainObject2(value) {
15169
15443
  return typeof value === "object" && value !== null && !Array.isArray(value);
15170
15444
  }
15171
- function isConfiguredHookCommand(value) {
15172
- if (typeof value === "string") return value.length > 0;
15173
- return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15174
- }
15175
-
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
15445
 
15201
15446
  // src/new.ts
15202
15447
  var execFileAsync3 = promisify4(execFile3);
@@ -15208,7 +15453,11 @@ function createNewCommand(dependencies = {}) {
15208
15453
  const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15209
15454
  return async function runNewCommand2(options) {
15210
15455
  const repository = await detectRepository(options.cwd);
15211
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15456
+ const config = await loadEffectiveConfig(
15457
+ repository.repoRoot,
15458
+ void 0,
15459
+ options.stderr
15460
+ );
15212
15461
  const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15213
15462
  if (options.editor && !options.open) {
15214
15463
  options.stderr("gji new: --editor has no effect without --open\n");
@@ -15219,8 +15468,10 @@ function createNewCommand(dependencies = {}) {
15219
15468
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15220
15469
  `);
15221
15470
  } else {
15222
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15223
- `);
15471
+ options.stderr(
15472
+ `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15473
+ `
15474
+ );
15224
15475
  }
15225
15476
  return 1;
15226
15477
  }
@@ -15238,8 +15489,10 @@ function createNewCommand(dependencies = {}) {
15238
15489
  const branchError = validateBranchName(rawBranch);
15239
15490
  if (branchError) {
15240
15491
  if (options.json) {
15241
- options.stderr(`${JSON.stringify({ error: branchError }, null, 2)}
15242
- `);
15492
+ options.stderr(
15493
+ `${JSON.stringify({ error: branchError }, null, 2)}
15494
+ `
15495
+ );
15243
15496
  } else {
15244
15497
  options.stderr(`gji new: ${branchError}
15245
15498
  `);
@@ -15250,18 +15503,32 @@ function createNewCommand(dependencies = {}) {
15250
15503
  const rawBasePath = resolveConfigString(config, "worktreePath");
15251
15504
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
15252
15505
  const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
15253
- const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath) : resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
15506
+ const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(
15507
+ repository.repoRoot,
15508
+ worktreeName,
15509
+ configuredBasePath
15510
+ ) : resolveWorktreePath(
15511
+ repository.repoRoot,
15512
+ worktreeName,
15513
+ configuredBasePath
15514
+ );
15254
15515
  if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
15255
15516
  if (options.force) {
15256
15517
  if (!options.dryRun) {
15257
15518
  try {
15258
- await execFileAsync3("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
15519
+ await execFileAsync3(
15520
+ "git",
15521
+ ["worktree", "remove", "--force", worktreePath],
15522
+ { cwd: repository.repoRoot }
15523
+ );
15259
15524
  } catch (err) {
15260
15525
  if (!isNotRegisteredWorktreeError(err)) {
15261
15526
  const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
15262
15527
  if (options.json) {
15263
- options.stderr(`${JSON.stringify({ warning: msg }, null, 2)}
15264
- `);
15528
+ options.stderr(
15529
+ `${JSON.stringify({ warning: msg }, null, 2)}
15530
+ `
15531
+ );
15265
15532
  } else {
15266
15533
  options.stderr(`Warning: ${msg}
15267
15534
  `);
@@ -15270,7 +15537,9 @@ function createNewCommand(dependencies = {}) {
15270
15537
  }
15271
15538
  if (!options.detached) {
15272
15539
  try {
15273
- await execFileAsync3("git", ["branch", "-D", worktreeName], { cwd: repository.repoRoot });
15540
+ await execFileAsync3("git", ["branch", "-D", worktreeName], {
15541
+ cwd: repository.repoRoot
15542
+ });
15274
15543
  } catch {
15275
15544
  }
15276
15545
  }
@@ -15281,12 +15550,18 @@ function createNewCommand(dependencies = {}) {
15281
15550
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15282
15551
  `);
15283
15552
  } 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
- `);
15553
+ options.stderr(
15554
+ `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15555
+ `
15556
+ );
15557
+ options.stderr(
15558
+ `Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15559
+ `
15560
+ );
15561
+ options.stderr(
15562
+ `Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks
15563
+ `
15564
+ );
15290
15565
  }
15291
15566
  return 1;
15292
15567
  } else {
@@ -15296,53 +15571,81 @@ function createNewCommand(dependencies = {}) {
15296
15571
  await writeOutput(worktreePath, options.stdout);
15297
15572
  return 0;
15298
15573
  }
15299
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
15300
- `);
15574
+ options.stderr(
15575
+ `Aborted because target worktree path already exists: ${worktreePath}
15576
+ `
15577
+ );
15301
15578
  return 1;
15302
15579
  }
15303
15580
  }
15304
15581
  if (options.dryRun) {
15305
15582
  if (options.json) {
15306
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15307
- `);
15583
+ options.stdout(
15584
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15585
+ `
15586
+ );
15308
15587
  } else {
15309
15588
  const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
15310
15589
  const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
15311
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15312
- `);
15590
+ options.stdout(
15591
+ `Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15592
+ `
15593
+ );
15313
15594
  }
15314
15595
  return 0;
15315
15596
  }
15316
15597
  await mkdir4(dirname5(worktreePath), { recursive: true });
15317
15598
  const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15318
15599
  await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15319
- const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15600
+ const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
15601
+ (p2) => typeof p2 === "string"
15602
+ ) : [];
15320
15603
  for (const pattern of syncPatterns) {
15321
15604
  try {
15322
15605
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
15323
15606
  } catch (error) {
15324
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15325
- `);
15607
+ options.stderr(
15608
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15609
+ `
15610
+ );
15326
15611
  }
15327
15612
  }
15328
- await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
15613
+ await maybeRunInstallPrompt(
15614
+ worktreePath,
15615
+ repository.repoRoot,
15616
+ config,
15617
+ options.stderr,
15618
+ dependencies,
15619
+ !!options.json
15620
+ );
15329
15621
  const hooks = extractHooks(config);
15330
15622
  await runHook(
15331
- hooks.afterCreate,
15623
+ hooks["after-create"],
15332
15624
  worktreePath,
15333
- { branch: worktreeName, path: worktreePath, repo: basename3(repository.repoRoot) },
15625
+ {
15626
+ branch: worktreeName,
15627
+ path: worktreePath,
15628
+ repo: basename3(repository.repoRoot)
15629
+ },
15334
15630
  options.stderr
15335
15631
  );
15336
15632
  if (options.json) {
15337
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15338
- `);
15633
+ options.stdout(
15634
+ `${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15635
+ `
15636
+ );
15339
15637
  } else {
15340
15638
  await appendHistory(worktreePath, worktreeName);
15341
15639
  await writeOutput(worktreePath, options.stdout);
15342
15640
  }
15343
15641
  if (options.open) {
15344
15642
  const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
15345
- await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
15643
+ await openWorktree(
15644
+ worktreePath,
15645
+ resolvedEditor,
15646
+ spawnEditor,
15647
+ options.stderr
15648
+ );
15346
15649
  }
15347
15650
  return 0;
15348
15651
  };
@@ -15403,7 +15706,11 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15403
15706
  let attempt = 1;
15404
15707
  while (true) {
15405
15708
  const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15406
- const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15709
+ const candidatePath = resolveWorktreePath(
15710
+ repoRoot,
15711
+ candidateName,
15712
+ basePath
15713
+ );
15407
15714
  if (!await pathExists(candidatePath)) {
15408
15715
  return candidatePath;
15409
15716
  }
@@ -15431,7 +15738,11 @@ function pickRandom(values, random) {
15431
15738
  }
15432
15739
  async function localBranchExists(repoRoot, branchName) {
15433
15740
  try {
15434
- await execFileAsync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
15741
+ await execFileAsync3(
15742
+ "git",
15743
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
15744
+ { cwd: repoRoot }
15745
+ );
15435
15746
  return true;
15436
15747
  } catch {
15437
15748
  return false;
@@ -15452,7 +15763,9 @@ function toExecMessage(error) {
15452
15763
  }
15453
15764
  async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15454
15765
  if (!editorCli) {
15455
- stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
15766
+ stderr(
15767
+ "gji new: --open requires --editor <cli> or a saved editor in config\n"
15768
+ );
15456
15769
  return;
15457
15770
  }
15458
15771
  const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
@@ -15566,11 +15879,17 @@ async function runWarpNavigate(options) {
15566
15879
  }
15567
15880
  return 1;
15568
15881
  }
15569
- const target = await resolveWarpTarget({ ...options, commandName: "gji warp", json: options.json });
15882
+ const target = await resolveWarpTarget({
15883
+ ...options,
15884
+ commandName: "gji warp",
15885
+ json: options.json
15886
+ });
15570
15887
  if (!target) return 1;
15571
15888
  if (options.json) {
15572
- options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15573
- `);
15889
+ options.stdout(
15890
+ `${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15891
+ `
15892
+ );
15574
15893
  return 0;
15575
15894
  }
15576
15895
  appendHistory(target.path, target.branch).catch(() => void 0);
@@ -15771,14 +16090,19 @@ function createGoCommand(dependencies = {}) {
15771
16090
  );
15772
16091
  return 1;
15773
16092
  }
15774
- const target = await resolveWarpTarget({ ...options, commandName: "gji go" });
16093
+ const target = await resolveWarpTarget({
16094
+ ...options,
16095
+ commandName: "gji go"
16096
+ });
15775
16097
  if (!target) return 1;
15776
16098
  appendHistory(target.path, target.branch).catch(() => void 0);
15777
16099
  await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
15778
16100
  return 0;
15779
16101
  }
15780
16102
  if (!options.branch && isHeadless()) {
15781
- options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
16103
+ options.stderr(
16104
+ "gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
16105
+ );
15782
16106
  return 1;
15783
16107
  }
15784
16108
  const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
@@ -15795,15 +16119,25 @@ function createGoCommand(dependencies = {}) {
15795
16119
  return 1;
15796
16120
  }
15797
16121
  const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
15798
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
16122
+ const config = await loadEffectiveConfig(
16123
+ repository.repoRoot,
16124
+ void 0,
16125
+ options.stderr
16126
+ );
15799
16127
  const hooks = extractHooks(config);
15800
16128
  await runHook(
15801
- hooks.afterEnter,
16129
+ hooks["after-enter"],
15802
16130
  resolvedPath,
15803
- { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename6(repository.repoRoot) },
16131
+ {
16132
+ branch: chosenWorktree?.branch ?? void 0,
16133
+ path: resolvedPath,
16134
+ repo: basename6(repository.repoRoot)
16135
+ },
15804
16136
  options.stderr
15805
16137
  );
15806
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
16138
+ appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
16139
+ () => void 0
16140
+ );
15807
16141
  await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15808
16142
  return 0;
15809
16143
  };
@@ -15811,187 +16145,56 @@ function createGoCommand(dependencies = {}) {
15811
16145
  var runGoCommand = createGoCommand();
15812
16146
  async function promptForWorktree(worktrees) {
15813
16147
  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
- `);
15940
- return 0;
15941
- };
15942
- }
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) }))
15947
- );
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
- 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
- }))
15966
- });
15967
- if (pD(choice)) return null;
15968
- return choice;
15969
- }
15970
- async function defaultPromptForEditor(editors) {
16148
+ worktrees.map((w2) => readWorktreeHealth(w2.path))
16149
+ );
15971
16150
  const choice = await ve({
15972
- message: "Choose an editor",
15973
- options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
16151
+ message: "Choose a worktree",
16152
+ options: worktrees.map((worktree, i) => {
16153
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
16154
+ const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
16155
+ const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
16156
+ return {
16157
+ value: worktree.path,
16158
+ label: worktree.branch ?? "(detached)",
16159
+ hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
16160
+ };
16161
+ })
15974
16162
  });
15975
- if (pD(choice)) return null;
16163
+ if (pD(choice)) {
16164
+ return null;
16165
+ }
15976
16166
  return choice;
15977
16167
  }
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 {
16168
+ function formatUpstreamHint(branch, health) {
16169
+ if (branch === null) return null;
16170
+ if (!health.hasUpstream) return "no upstream";
16171
+ if (health.upstreamGone) return "upstream gone";
16172
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
16173
+ if (health.ahead === 0) return `behind ${health.behind}`;
16174
+ if (health.behind === 0) return `ahead ${health.ahead}`;
16175
+ return `ahead ${health.ahead}, behind ${health.behind}`;
16176
+ }
16177
+
16178
+ // src/history-command.ts
16179
+ async function runHistoryCommand(options) {
16180
+ const history = await loadHistory(options.home);
16181
+ if (options.json) {
16182
+ options.stdout(`${JSON.stringify(history, null, 2)}
16183
+ `);
16184
+ return 0;
15984
16185
  }
15985
- const workspace = { folders: [{ path: "." }], settings: {} };
15986
- await writeFile5(workspacePath, `${JSON.stringify(workspace, null, 2)}
15987
- `, "utf8");
15988
- return workspacePath;
16186
+ if (history.length === 0) {
16187
+ options.stdout("No navigation history.\n");
16188
+ return 0;
16189
+ }
16190
+ options.stdout(formatHistoryList(history, options.cwd));
16191
+ return 0;
15989
16192
  }
15990
16193
 
15991
16194
  // src/init.ts
15992
- import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile6 } from "node:fs/promises";
16195
+ import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
15993
16196
  import { homedir as homedir5 } from "node:os";
15994
- import { dirname as dirname7, join as join8 } from "node:path";
16197
+ import { dirname as dirname7, join as join7 } from "node:path";
15995
16198
  var START_MARKER = "# >>> gji init >>>";
15996
16199
  var END_MARKER = "# <<< gji init <<<";
15997
16200
  var SHELL_WRAPPED_COMMANDS = [
@@ -16063,7 +16266,7 @@ async function runInitCommand(options) {
16063
16266
  await mkdir6(dirname7(rcPath), { recursive: true });
16064
16267
  const current = await readExistingConfig(rcPath);
16065
16268
  const next = upsertShellIntegration(current, script);
16066
- await writeFile6(rcPath, next, "utf8");
16269
+ await writeFile5(rcPath, next, "utf8");
16067
16270
  options.stdout(`${rcPath}
16068
16271
  `);
16069
16272
  const { config: globalConfig } = await loadGlobalConfig(home);
@@ -16074,7 +16277,11 @@ async function runInitCommand(options) {
16074
16277
  const prompt = options.promptForSetup ?? defaultPromptForSetup;
16075
16278
  const result = await prompt();
16076
16279
  if (result) {
16077
- await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
16280
+ await updateGlobalConfigKey(
16281
+ "installSaveTarget",
16282
+ result.installSaveTarget,
16283
+ home
16284
+ );
16078
16285
  await saveWizardConfig(result, options.cwd, home);
16079
16286
  }
16080
16287
  }
@@ -16132,9 +16339,12 @@ async function saveWizardConfig(result, cwd, home) {
16132
16339
  if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
16133
16340
  if (result.worktreePath) values.worktreePath = result.worktreePath;
16134
16341
  const hooks = {};
16135
- if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16136
- if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16137
- if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
16342
+ if (result.hooks?.["after-create"])
16343
+ hooks["after-create"] = result.hooks["after-create"];
16344
+ if (result.hooks?.["after-enter"])
16345
+ hooks["after-enter"] = result.hooks["after-enter"];
16346
+ if (result.hooks?.["before-remove"])
16347
+ hooks["before-remove"] = result.hooks["before-remove"];
16138
16348
  if (Object.keys(hooks).length > 0) values.hooks = hooks;
16139
16349
  if (Object.keys(values).length === 0) return;
16140
16350
  if (result.installSaveTarget === "local") {
@@ -16148,11 +16358,11 @@ async function saveWizardConfig(result, cwd, home) {
16148
16358
  function resolveShellConfigPath(shell, home) {
16149
16359
  switch (shell) {
16150
16360
  case "bash":
16151
- return join8(home, ".bashrc");
16361
+ return join7(home, ".bashrc");
16152
16362
  case "fish":
16153
- return join8(home, ".config", "fish", "config.fish");
16363
+ return join7(home, ".config", "fish", "config.fish");
16154
16364
  case "zsh":
16155
- return join8(home, ".zshrc");
16365
+ return join7(home, ".zshrc");
16156
16366
  }
16157
16367
  }
16158
16368
  async function readExistingConfig(path9) {
@@ -16178,7 +16388,9 @@ function isMissingFileError2(error) {
16178
16388
  function renderFishWrapper(command) {
16179
16389
  const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
16180
16390
  const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
16181
- const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
16391
+ const bypassTests = command.bypassOptions.map(
16392
+ (opt) => `test $argv[1] = ${opt}`
16393
+ );
16182
16394
  const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
16183
16395
  return `if test (count $argv) -gt 0; and ${nameCondition}
16184
16396
  set -e argv[1]
@@ -16230,8 +16442,16 @@ async function defaultPromptForSetup() {
16230
16442
  const installSaveTarget = await ve({
16231
16443
  message: "Where should preferences be saved?",
16232
16444
  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" }
16445
+ {
16446
+ value: "global",
16447
+ label: "~/.config/gji/config.json",
16448
+ hint: "personal \u2014 never committed"
16449
+ },
16450
+ {
16451
+ value: "local",
16452
+ label: ".gji.json",
16453
+ hint: "repo \u2014 committed with the project"
16454
+ }
16235
16455
  ]
16236
16456
  });
16237
16457
  if (pD(installSaveTarget)) {
@@ -16255,7 +16475,7 @@ async function defaultPromptForSetup() {
16255
16475
  return null;
16256
16476
  }
16257
16477
  const afterCreate = await he({
16258
- message: "afterCreate hook \u2014 run after creating a worktree?",
16478
+ message: "after-create hook \u2014 run after creating a worktree?",
16259
16479
  placeholder: "e.g. pnpm install \u2014 leave blank to skip"
16260
16480
  });
16261
16481
  if (pD(afterCreate)) {
@@ -16263,7 +16483,7 @@ async function defaultPromptForSetup() {
16263
16483
  return null;
16264
16484
  }
16265
16485
  const afterEnter = await he({
16266
- message: "afterEnter hook \u2014 run after entering a worktree?",
16486
+ message: "after-enter hook \u2014 run after entering a worktree?",
16267
16487
  placeholder: "e.g. nvm use \u2014 leave blank to skip"
16268
16488
  });
16269
16489
  if (pD(afterEnter)) {
@@ -16271,7 +16491,7 @@ async function defaultPromptForSetup() {
16271
16491
  return null;
16272
16492
  }
16273
16493
  const beforeRemove = await he({
16274
- message: "beforeRemove hook \u2014 run before removing a worktree?",
16494
+ message: "before-remove hook \u2014 run before removing a worktree?",
16275
16495
  placeholder: "leave blank to skip"
16276
16496
  });
16277
16497
  if (pD(beforeRemove)) {
@@ -16280,9 +16500,9 @@ async function defaultPromptForSetup() {
16280
16500
  }
16281
16501
  Se("Setup complete!");
16282
16502
  const hooks = {};
16283
- if (afterCreate) hooks.afterCreate = afterCreate;
16284
- if (afterEnter) hooks.afterEnter = afterEnter;
16285
- if (beforeRemove) hooks.beforeRemove = beforeRemove;
16503
+ if (afterCreate) hooks["after-create"] = afterCreate;
16504
+ if (afterEnter) hooks["after-enter"] = afterEnter;
16505
+ if (beforeRemove) hooks["before-remove"] = beforeRemove;
16286
16506
  return {
16287
16507
  branchPrefix: branchPrefix || void 0,
16288
16508
  hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
@@ -16334,10 +16554,22 @@ function formatDetailedWorktreeTable(worktrees) {
16334
16554
  status: worktree.status,
16335
16555
  upstream: formatUpstreamState(worktree.upstream)
16336
16556
  }));
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));
16557
+ const branchWidth = Math.max(
16558
+ "BRANCH".length,
16559
+ ...rows.map((row) => row.branch.length)
16560
+ );
16561
+ const statusWidth = Math.max(
16562
+ "STATUS".length,
16563
+ ...rows.map((row) => row.status.length)
16564
+ );
16565
+ const upstreamWidth = Math.max(
16566
+ "UPSTREAM".length,
16567
+ ...rows.map((row) => row.upstream.length)
16568
+ );
16569
+ const lastCommitWidth = Math.max(
16570
+ "LAST".length,
16571
+ ...rows.map((row) => row.lastCommit.length)
16572
+ );
16341
16573
  const lines = [
16342
16574
  " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
16343
16575
  ];
@@ -16360,7 +16592,9 @@ function formatWorktreeTable(worktrees) {
16360
16592
  );
16361
16593
  const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
16362
16594
  for (const row of rows) {
16363
- lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
16595
+ lines.push(
16596
+ `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`
16597
+ );
16364
16598
  }
16365
16599
  return lines.join("\n");
16366
16600
  }
@@ -16372,10 +16606,172 @@ function sortWorktrees(worktrees) {
16372
16606
  });
16373
16607
  }
16374
16608
 
16609
+ // src/open.ts
16610
+ import { execFile as execFile4 } from "node:child_process";
16611
+ import { access as access4, writeFile as writeFile6 } from "node:fs/promises";
16612
+ import { join as join8 } from "node:path";
16613
+ import { promisify as promisify5 } from "node:util";
16614
+ var execFileAsync4 = promisify5(execFile4);
16615
+ function createOpenCommand(dependencies = {}) {
16616
+ const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
16617
+ const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
16618
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
16619
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
16620
+ return async function runOpenCommand2(options) {
16621
+ const [worktrees, repository] = await Promise.all([
16622
+ listWorktrees(options.cwd),
16623
+ detectRepository(options.cwd)
16624
+ ]);
16625
+ let targetPath;
16626
+ if (options.branch) {
16627
+ const entry = worktrees.find((w2) => w2.branch === options.branch);
16628
+ if (!entry) {
16629
+ options.stderr(
16630
+ `gji open: no worktree found for branch: ${options.branch}
16631
+ `
16632
+ );
16633
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
16634
+ `);
16635
+ return 1;
16636
+ }
16637
+ targetPath = entry.path;
16638
+ } else if (isHeadless()) {
16639
+ targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
16640
+ } else {
16641
+ const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
16642
+ if (!chosen) {
16643
+ options.stderr("Aborted\n");
16644
+ return 1;
16645
+ }
16646
+ targetPath = chosen;
16647
+ }
16648
+ const config = await loadEffectiveConfig(
16649
+ repository.repoRoot,
16650
+ void 0,
16651
+ options.stderr
16652
+ );
16653
+ const savedEditor = resolveConfigString(config, "editor");
16654
+ let editorCli;
16655
+ if (options.editor) {
16656
+ editorCli = options.editor;
16657
+ } else if (savedEditor) {
16658
+ editorCli = savedEditor;
16659
+ } else {
16660
+ const installed = await detectEditors();
16661
+ if (installed.length === 0) {
16662
+ options.stderr(
16663
+ "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
16664
+ );
16665
+ return 1;
16666
+ }
16667
+ if (installed.length === 1 || isHeadless()) {
16668
+ editorCli = installed[0].cli;
16669
+ } else {
16670
+ const chosen = await promptForEditor(installed);
16671
+ if (!chosen) {
16672
+ options.stderr("Aborted\n");
16673
+ return 1;
16674
+ }
16675
+ editorCli = chosen;
16676
+ }
16677
+ }
16678
+ if (options.save && editorCli !== savedEditor) {
16679
+ await updateGlobalConfigKey("editor", editorCli);
16680
+ const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
16681
+ options.stdout(`Saved editor "${displayName2}" to global config
16682
+ `);
16683
+ }
16684
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
16685
+ let openTarget = targetPath;
16686
+ if (options.workspace) {
16687
+ if (editorDef?.supportsWorkspace) {
16688
+ openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
16689
+ } else {
16690
+ const displayName2 = editorDef?.name ?? editorCli;
16691
+ options.stderr(
16692
+ `gji open: --workspace is not supported for ${displayName2}, ignoring
16693
+ `
16694
+ );
16695
+ }
16696
+ }
16697
+ const args = [];
16698
+ if (editorDef?.newWindowFlag) {
16699
+ args.push(editorDef.newWindowFlag);
16700
+ }
16701
+ args.push(openTarget);
16702
+ try {
16703
+ await spawnEditor(editorCli, args);
16704
+ } catch (error) {
16705
+ const message = error instanceof Error ? error.message : String(error);
16706
+ options.stderr(`gji open: failed to launch editor: ${message}
16707
+ `);
16708
+ return 1;
16709
+ }
16710
+ const displayName = editorDef?.name ?? editorCli;
16711
+ options.stdout(`Opened ${targetPath} in ${displayName}
16712
+ `);
16713
+ return 0;
16714
+ };
16715
+ }
16716
+ var runOpenCommand = createOpenCommand();
16717
+ async function detectInstalledEditors() {
16718
+ const results = await Promise.all(
16719
+ EDITORS.map(async (editor) => ({
16720
+ editor,
16721
+ available: await isCommandAvailable(editor.cli)
16722
+ }))
16723
+ );
16724
+ return results.filter((r2) => r2.available).map((r2) => r2.editor);
16725
+ }
16726
+ async function isCommandAvailable(command) {
16727
+ try {
16728
+ await execFileAsync4("which", [command]);
16729
+ return true;
16730
+ } catch {
16731
+ return false;
16732
+ }
16733
+ }
16734
+ async function defaultPromptForWorktree(worktrees) {
16735
+ const choice = await ve({
16736
+ message: "Choose a worktree to open",
16737
+ options: worktrees.map((w2) => ({
16738
+ value: w2.path,
16739
+ label: w2.branch ?? "(detached)",
16740
+ hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
16741
+ }))
16742
+ });
16743
+ if (pD(choice)) return null;
16744
+ return choice;
16745
+ }
16746
+ async function defaultPromptForEditor(editors) {
16747
+ const choice = await ve({
16748
+ message: "Choose an editor",
16749
+ options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
16750
+ });
16751
+ if (pD(choice)) return null;
16752
+ return choice;
16753
+ }
16754
+ async function ensureWorkspaceFile(worktreePath, repoName) {
16755
+ const workspacePath = join8(worktreePath, `${repoName}.code-workspace`);
16756
+ try {
16757
+ await access4(workspacePath);
16758
+ return workspacePath;
16759
+ } catch {
16760
+ }
16761
+ const workspace = { folders: [{ path: "." }], settings: {} };
16762
+ await writeFile6(
16763
+ workspacePath,
16764
+ `${JSON.stringify(workspace, null, 2)}
16765
+ `,
16766
+ "utf8"
16767
+ );
16768
+ return workspacePath;
16769
+ }
16770
+
16375
16771
  // src/pr.ts
16772
+ import { execFile as execFile5 } from "node:child_process";
16376
16773
  import { mkdir as mkdir7 } from "node:fs/promises";
16377
16774
  import { basename as basename7, dirname as dirname8 } from "node:path";
16378
- import { execFile as execFile5 } from "node:child_process";
16379
16775
  import { promisify as promisify6 } from "node:util";
16380
16776
  var execFileAsync5 = promisify6(execFile5);
16381
16777
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
@@ -16383,7 +16779,9 @@ function parsePrInput(input) {
16383
16779
  if (/^\d+$/.test(input)) return input;
16384
16780
  const hashMatch = input.match(/^#(\d+)$/);
16385
16781
  if (hashMatch) return hashMatch[1];
16386
- const urlMatch = input.match(/\/(?:pull|pull-requests|merge_requests)\/(\d+)/);
16782
+ const urlMatch = input.match(
16783
+ /\/(?:pull|pull-requests|merge_requests)\/(\d+)/
16784
+ );
16387
16785
  if (urlMatch) return urlMatch[1];
16388
16786
  return null;
16389
16787
  }
@@ -16403,12 +16801,20 @@ function createPrCommand(dependencies = {}) {
16403
16801
  return 1;
16404
16802
  }
16405
16803
  const repository = await detectRepository(options.cwd);
16406
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
16804
+ const config = await loadEffectiveConfig(
16805
+ repository.repoRoot,
16806
+ void 0,
16807
+ options.stderr
16808
+ );
16407
16809
  const branchName = `pr/${prNumber}`;
16408
16810
  const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
16409
16811
  const rawBasePath = resolveConfigString(config, "worktreePath");
16410
16812
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
16411
- const worktreePath = resolveWorktreePath(repository.repoRoot, branchName, configuredBasePath);
16813
+ const worktreePath = resolveWorktreePath(
16814
+ repository.repoRoot,
16815
+ branchName,
16816
+ configuredBasePath
16817
+ );
16412
16818
  if (await pathExists(worktreePath)) {
16413
16819
  if (options.json || isHeadless()) {
16414
16820
  const message = `target worktree path already exists: ${worktreePath}`;
@@ -16416,10 +16822,14 @@ function createPrCommand(dependencies = {}) {
16416
16822
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
16417
16823
  `);
16418
16824
  } 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
- `);
16825
+ options.stderr(
16826
+ `gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
16827
+ `
16828
+ );
16829
+ options.stderr(
16830
+ `Hint: Use 'gji remove pr/${prNumber}' or 'gji clean' to remove the existing worktree
16831
+ `
16832
+ );
16423
16833
  }
16424
16834
  return 1;
16425
16835
  }
@@ -16429,22 +16839,33 @@ function createPrCommand(dependencies = {}) {
16429
16839
  await writeOutput2(worktreePath, options.stdout);
16430
16840
  return 0;
16431
16841
  }
16432
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
16433
- `);
16842
+ options.stderr(
16843
+ `Aborted because target worktree path already exists: ${worktreePath}
16844
+ `
16845
+ );
16434
16846
  return 1;
16435
16847
  }
16436
16848
  if (options.dryRun) {
16437
16849
  if (options.json) {
16438
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16439
- `);
16850
+ options.stdout(
16851
+ `${JSON.stringify({ branch: branchName, path: worktreePath, dryRun: true }, null, 2)}
16852
+ `
16853
+ );
16440
16854
  } else {
16441
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})
16442
- `);
16855
+ options.stdout(
16856
+ `Would create worktree at ${worktreePath} (branch: ${branchName})
16857
+ `
16858
+ );
16443
16859
  }
16444
16860
  return 0;
16445
16861
  }
16446
16862
  try {
16447
- await fetchPullRequestRef(repository.repoRoot, options.number, prNumber, remoteRef);
16863
+ await fetchPullRequestRef(
16864
+ repository.repoRoot,
16865
+ options.number,
16866
+ prNumber,
16867
+ remoteRef
16868
+ );
16448
16869
  } catch {
16449
16870
  const message = `Failed to fetch PR #${prNumber} from origin`;
16450
16871
  if (options.json) {
@@ -16453,35 +16874,57 @@ function createPrCommand(dependencies = {}) {
16453
16874
  } else {
16454
16875
  options.stderr(`${message}
16455
16876
  `);
16456
- options.stderr(`Hint: Verify the remote is reachable: git fetch origin
16457
- `);
16877
+ options.stderr(
16878
+ `Hint: Verify the remote is reachable: git fetch origin
16879
+ `
16880
+ );
16458
16881
  }
16459
16882
  return 1;
16460
16883
  }
16461
16884
  await mkdir7(dirname8(worktreePath), { recursive: true });
16462
- const branchAlreadyExists = await localBranchExists2(repository.repoRoot, branchName);
16885
+ const branchAlreadyExists = await localBranchExists2(
16886
+ repository.repoRoot,
16887
+ branchName
16888
+ );
16463
16889
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
16464
16890
  await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
16465
- const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
16891
+ const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter(
16892
+ (p2) => typeof p2 === "string"
16893
+ ) : [];
16466
16894
  for (const pattern of syncPatterns) {
16467
16895
  try {
16468
16896
  await syncFiles(repository.repoRoot, worktreePath, [pattern]);
16469
16897
  } catch (error) {
16470
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16471
- `);
16898
+ options.stderr(
16899
+ `Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
16900
+ `
16901
+ );
16472
16902
  }
16473
16903
  }
16474
- await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
16904
+ await maybeRunInstallPrompt(
16905
+ worktreePath,
16906
+ repository.repoRoot,
16907
+ config,
16908
+ options.stderr,
16909
+ dependencies,
16910
+ !!options.json
16911
+ );
16475
16912
  const hooks = extractHooks(config);
16476
16913
  await runHook(
16477
- hooks.afterCreate,
16914
+ hooks["after-create"],
16478
16915
  worktreePath,
16479
- { branch: branchName, path: worktreePath, repo: basename7(repository.repoRoot) },
16916
+ {
16917
+ branch: branchName,
16918
+ path: worktreePath,
16919
+ repo: basename7(repository.repoRoot)
16920
+ },
16480
16921
  options.stderr
16481
16922
  );
16482
16923
  if (options.json) {
16483
- options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16484
- `);
16924
+ options.stdout(
16925
+ `${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}
16926
+ `
16927
+ );
16485
16928
  } else {
16486
16929
  await appendHistory(worktreePath, branchName);
16487
16930
  await writeOutput2(worktreePath, options.stdout);
@@ -16517,9 +16960,16 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
16517
16960
  throw new Error(`No pull request ref found for #${prNumber}`);
16518
16961
  }
16519
16962
  function listPullRequestSourceRefs(input, prNumber) {
16520
- const allForges = ["github", "gitlab", "bitbucket"];
16963
+ const allForges = [
16964
+ "github",
16965
+ "gitlab",
16966
+ "bitbucket"
16967
+ ];
16521
16968
  const preferredForge = detectPullRequestForge(input);
16522
- const orderedForges = preferredForge === "unknown" ? allForges : [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
16969
+ const orderedForges = preferredForge === "unknown" ? allForges : [
16970
+ preferredForge,
16971
+ ...allForges.filter((forge) => forge !== preferredForge)
16972
+ ];
16523
16973
  return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
16524
16974
  }
16525
16975
  function detectPullRequestForge(input) {
@@ -16557,7 +17007,9 @@ function createRemoveCommand(dependencies = {}) {
16557
17007
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
16558
17008
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
16559
17009
  return async function runRemoveCommand2(options) {
16560
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
17010
+ const { linkedWorktrees, repository } = await loadLinkedWorktrees(
17011
+ options.cwd
17012
+ );
16561
17013
  if (linkedWorktrees.length === 0) {
16562
17014
  emitError2(options, "No linked worktrees to finish");
16563
17015
  return 1;
@@ -16567,8 +17019,10 @@ function createRemoveCommand(dependencies = {}) {
16567
17019
  if (options.json) {
16568
17020
  emitError2(options, message);
16569
17021
  } else {
16570
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16571
- `);
17022
+ options.stderr(
17023
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17024
+ `
17025
+ );
16572
17026
  }
16573
17027
  return 1;
16574
17028
  }
@@ -16589,8 +17043,10 @@ function createRemoveCommand(dependencies = {}) {
16589
17043
  if (options.json) {
16590
17044
  emitError2(options, message);
16591
17045
  } else {
16592
- options.stderr(`gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
16593
- `);
17046
+ options.stderr(
17047
+ `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
17048
+ `
17049
+ );
16594
17050
  }
16595
17051
  return 1;
16596
17052
  }
@@ -16600,8 +17056,10 @@ function createRemoveCommand(dependencies = {}) {
16600
17056
  }
16601
17057
  if (options.dryRun) {
16602
17058
  if (options.json) {
16603
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
16604
- `);
17059
+ options.stdout(
17060
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
17061
+ `
17062
+ );
16605
17063
  } else {
16606
17064
  const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
16607
17065
  options.stdout(`Would remove worktree at ${worktree.path} (${desc})
@@ -16609,12 +17067,20 @@ function createRemoveCommand(dependencies = {}) {
16609
17067
  }
16610
17068
  return 0;
16611
17069
  }
16612
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
17070
+ const config = await loadEffectiveConfig(
17071
+ repository.repoRoot,
17072
+ void 0,
17073
+ options.stderr
17074
+ );
16613
17075
  const hooks = extractHooks(config);
16614
17076
  await runHook(
16615
- hooks.beforeRemove,
17077
+ hooks["before-remove"],
16616
17078
  worktree.path,
16617
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename8(repository.repoRoot) },
17079
+ {
17080
+ branch: worktree.branch ?? void 0,
17081
+ path: worktree.path,
17082
+ repo: basename8(repository.repoRoot)
17083
+ },
16618
17084
  options.stderr
16619
17085
  );
16620
17086
  try {
@@ -16630,7 +17096,10 @@ function createRemoveCommand(dependencies = {}) {
16630
17096
  try {
16631
17097
  await forceRemoveWorktree(repository.repoRoot, worktree.path);
16632
17098
  } catch (forceError) {
16633
- emitError2(options, `Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`);
17099
+ emitError2(
17100
+ options,
17101
+ `Failed to remove worktree at ${worktree.path}: ${toMessage2(forceError)}`
17102
+ );
16634
17103
  return 1;
16635
17104
  }
16636
17105
  }
@@ -16645,18 +17114,24 @@ function createRemoveCommand(dependencies = {}) {
16645
17114
  try {
16646
17115
  await forceDeleteBranch(repository.repoRoot, worktree.branch);
16647
17116
  } catch (forceError) {
16648
- options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
16649
- `);
17117
+ options.stderr(
17118
+ `Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
17119
+ `
17120
+ );
16650
17121
  }
16651
17122
  } else {
16652
- options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)
16653
- `);
17123
+ options.stderr(
17124
+ `Branch ${worktree.branch} was not deleted (has unmerged commits)
17125
+ `
17126
+ );
16654
17127
  }
16655
17128
  }
16656
17129
  }
16657
17130
  if (options.json) {
16658
- options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
16659
- `);
17131
+ options.stdout(
17132
+ `${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
17133
+ `
17134
+ );
16660
17135
  } else {
16661
17136
  await writeOutput3(repository.repoRoot, options.stdout);
16662
17137
  }
@@ -16705,7 +17180,11 @@ var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
16705
17180
  async function runRootCommand(options) {
16706
17181
  const repository = await detectRepository(options.cwd);
16707
17182
  if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
16708
- await writeShellOutput(ROOT_OUTPUT_FILE_ENV, repository.repoRoot, options.stdout);
17183
+ await writeShellOutput(
17184
+ ROOT_OUTPUT_FILE_ENV,
17185
+ repository.repoRoot,
17186
+ options.stdout
17187
+ );
16709
17188
  return 0;
16710
17189
  }
16711
17190
  options.stdout(`${repository.repoRoot}
@@ -16713,6 +17192,54 @@ async function runRootCommand(options) {
16713
17192
  return 0;
16714
17193
  }
16715
17194
 
17195
+ // src/run-hook.ts
17196
+ var VALID_HOOKS = [
17197
+ "after-create",
17198
+ "after-enter",
17199
+ "before-remove"
17200
+ ];
17201
+ var CAMEL_ALIASES = {
17202
+ afterCreate: "after-create",
17203
+ afterEnter: "after-enter",
17204
+ beforeRemove: "before-remove"
17205
+ };
17206
+ function isValidHook(hook) {
17207
+ return VALID_HOOKS.includes(hook);
17208
+ }
17209
+ async function runHookCommand(options) {
17210
+ const normalized = CAMEL_ALIASES[options.hook] ?? options.hook;
17211
+ if (!isValidHook(normalized)) {
17212
+ options.stderr(
17213
+ `gji run-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
17214
+ `
17215
+ );
17216
+ return 1;
17217
+ }
17218
+ const hookName = normalized;
17219
+ const repository = await detectRepository(options.cwd);
17220
+ const config = await loadEffectiveConfig(
17221
+ repository.repoRoot,
17222
+ void 0,
17223
+ options.stderr
17224
+ );
17225
+ const hooks = extractHooks(config);
17226
+ const worktrees = await listWorktrees(options.cwd);
17227
+ const currentWorktree = worktrees.find(
17228
+ (w2) => w2.path === repository.currentRoot
17229
+ );
17230
+ await runHook(
17231
+ hooks[hookName],
17232
+ repository.currentRoot,
17233
+ {
17234
+ branch: currentWorktree?.branch ?? void 0,
17235
+ path: repository.currentRoot,
17236
+ repo: repository.repoName
17237
+ },
17238
+ options.stderr
17239
+ );
17240
+ return 0;
17241
+ }
17242
+
16716
17243
  // src/status.ts
16717
17244
  async function runStatusCommand(options) {
16718
17245
  const repository = await detectRepository(options.cwd);
@@ -16721,18 +17248,31 @@ async function runStatusCommand(options) {
16721
17248
  worktrees.map(async (worktree) => buildStatusRow(worktree))
16722
17249
  );
16723
17250
  if (options.json) {
16724
- options.stdout(`${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
16725
- `);
17251
+ options.stdout(
17252
+ `${JSON.stringify(formatStatusJson(repository.repoRoot, repository.currentRoot, rows), null, 2)}
17253
+ `
17254
+ );
16726
17255
  return 0;
16727
17256
  }
16728
- options.stdout(`${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
16729
- `);
17257
+ options.stdout(
17258
+ `${formatStatusOutput(repository.repoRoot, repository.currentRoot, rows)}
17259
+ `
17260
+ );
16730
17261
  return 0;
16731
17262
  }
16732
17263
  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));
17264
+ const currentWidth = Math.max(
17265
+ "CURRENT".length,
17266
+ ...rows.map((row) => row.current ? 1 : 0)
17267
+ );
17268
+ const branchWidth = Math.max(
17269
+ "BRANCH".length,
17270
+ ...rows.map((row) => formatBranch(row.branch).length)
17271
+ );
17272
+ const statusWidth = Math.max(
17273
+ "STATUS".length,
17274
+ ...rows.map((row) => row.status.length)
17275
+ );
16736
17276
  const upstreamWidth = Math.max(
16737
17277
  "UPSTREAM".length,
16738
17278
  ...rows.map((row) => formatUpstreamState2(row.upstream).length)
@@ -16768,7 +17308,9 @@ async function buildStatusRow(worktree) {
16768
17308
  };
16769
17309
  }
16770
17310
  function sortWorktreesByPath(worktrees) {
16771
- return [...worktrees].sort((left, right) => comparePaths(left.path, right.path));
17311
+ return [...worktrees].sort(
17312
+ (left, right) => comparePaths(left.path, right.path)
17313
+ );
16772
17314
  }
16773
17315
  function formatBranch(branch) {
16774
17316
  return branch ?? "(detached)";
@@ -16814,7 +17356,11 @@ function formatUpstreamState2(upstream) {
16814
17356
  // src/sync.ts
16815
17357
  async function runSyncCommand(options) {
16816
17358
  const repository = await detectRepository(options.cwd);
16817
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
17359
+ const config = await loadEffectiveConfig(
17360
+ repository.repoRoot,
17361
+ void 0,
17362
+ options.stderr
17363
+ );
16818
17364
  const worktrees = await listWorktrees(options.cwd);
16819
17365
  const remote = resolveConfiguredString2(config.syncRemote) ?? "origin";
16820
17366
  let defaultBranch;
@@ -16823,22 +17369,33 @@ async function runSyncCommand(options) {
16823
17369
  } catch {
16824
17370
  emitError3(options, `Unable to reach remote '${remote}'`);
16825
17371
  if (!options.json) {
16826
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16827
- `);
17372
+ options.stderr(
17373
+ `Hint: Add the remote with: git remote add ${remote} <url>
17374
+ `
17375
+ );
16828
17376
  }
16829
17377
  return 1;
16830
17378
  }
16831
17379
  if (!defaultBranch) {
16832
17380
  emitError3(options, "Unable to determine the default branch for sync.");
16833
17381
  if (!options.json) {
16834
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16835
- `);
17382
+ options.stderr(
17383
+ `Hint: Add the remote with: git remote add ${remote} <url>
17384
+ `
17385
+ );
16836
17386
  }
16837
17387
  return 1;
16838
17388
  }
16839
- const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
17389
+ const targetWorktrees = selectTargetWorktrees(
17390
+ worktrees,
17391
+ repository.currentRoot,
17392
+ options.all
17393
+ );
16840
17394
  if (targetWorktrees === "detached") {
16841
- emitError3(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
17395
+ emitError3(
17396
+ options,
17397
+ `Cannot sync detached worktree: ${repository.currentRoot}`
17398
+ );
16842
17399
  return 1;
16843
17400
  }
16844
17401
  for (const worktree of targetWorktrees) {
@@ -16852,15 +17409,21 @@ async function runSyncCommand(options) {
16852
17409
  } catch {
16853
17410
  emitError3(options, `Failed to fetch from remote '${remote}'`);
16854
17411
  if (!options.json) {
16855
- options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>
16856
- `);
17412
+ options.stderr(
17413
+ `Hint: Add the remote with: git remote add ${remote} <url>
17414
+ `
17415
+ );
16857
17416
  }
16858
17417
  return 1;
16859
17418
  }
16860
17419
  const updatedWorktrees = [];
16861
17420
  for (const worktree of targetWorktrees) {
16862
17421
  if (worktree.branch === defaultBranch) {
16863
- await runGit(worktree.path, ["merge", "--ff-only", `${remote}/${defaultBranch}`]);
17422
+ await runGit(worktree.path, [
17423
+ "merge",
17424
+ "--ff-only",
17425
+ `${remote}/${defaultBranch}`
17426
+ ]);
16864
17427
  } else {
16865
17428
  await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
16866
17429
  }
@@ -16871,7 +17434,10 @@ async function runSyncCommand(options) {
16871
17434
  }
16872
17435
  }
16873
17436
  if (options.json) {
16874
- const updated = updatedWorktrees.map((w2) => ({ branch: w2.branch, path: w2.path }));
17437
+ const updated = updatedWorktrees.map((w2) => ({
17438
+ branch: w2.branch,
17439
+ path: w2.path
17440
+ }));
16875
17441
  options.stdout(`${JSON.stringify({ updated }, null, 2)}
16876
17442
  `);
16877
17443
  }
@@ -16890,7 +17456,9 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
16890
17456
  if (all) {
16891
17457
  return worktrees.filter((worktree) => worktree.branch !== null).sort((left, right) => comparePaths(left.path, right.path));
16892
17458
  }
16893
- const currentWorktree = worktrees.find((worktree) => worktree.path === currentRoot);
17459
+ const currentWorktree = worktrees.find(
17460
+ (worktree) => worktree.path === currentRoot
17461
+ );
16894
17462
  if (!currentWorktree) {
16895
17463
  return [];
16896
17464
  }
@@ -16903,36 +17471,145 @@ function resolveConfiguredString2(value) {
16903
17471
  return typeof value === "string" && value.length > 0 ? value : null;
16904
17472
  }
16905
17473
 
16906
- // src/trigger-hook.ts
16907
- var VALID_HOOKS = ["afterCreate", "afterEnter", "beforeRemove"];
16908
- function isValidHook(hook) {
16909
- return VALID_HOOKS.includes(hook);
16910
- }
16911
- async function runTriggerHookCommand(options) {
16912
- if (!isValidHook(options.hook)) {
16913
- options.stderr(
16914
- `gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
16915
- `
16916
- );
16917
- return 1;
16918
- }
16919
- const hookName = options.hook;
17474
+ // src/sync-files-command.ts
17475
+ import { homedir as homedir6 } from "node:os";
17476
+ import { join as join9 } from "node:path";
17477
+ async function runSyncFilesCommand(options) {
16920
17478
  const repository = await detectRepository(options.cwd);
16921
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
16922
- const hooks = extractHooks(config);
16923
- const worktrees = await listWorktrees(options.cwd);
16924
- const currentWorktree = worktrees.find((w2) => w2.path === repository.currentRoot);
16925
- await runHook(
16926
- hooks[hookName],
16927
- repository.currentRoot,
16928
- {
16929
- branch: currentWorktree?.branch ?? void 0,
16930
- path: repository.currentRoot,
16931
- repo: repository.repoName
16932
- },
16933
- options.stderr
17479
+ const home = options.home ?? homedir6();
17480
+ const loaded = await loadGlobalConfig(home);
17481
+ const repoEntry = findRepoConfigEntry(
17482
+ loaded.config,
17483
+ repository.repoRoot,
17484
+ home
16934
17485
  );
16935
- return 0;
17486
+ const repoConfig = repoEntry?.config ?? {};
17487
+ switch (options.action) {
17488
+ case void 0:
17489
+ case "list": {
17490
+ writeSyncFiles(options.stdout, readSyncFiles(repoConfig), !!options.json);
17491
+ return 0;
17492
+ }
17493
+ case "add": {
17494
+ const paths = validatePaths(options.paths ?? [], options);
17495
+ if (!paths) return 1;
17496
+ const nextFiles = mergeSyncFiles(readSyncFiles(repoConfig), paths);
17497
+ await saveRepoSyncFiles(
17498
+ loaded.config,
17499
+ repoEntry?.key ?? repository.repoRoot,
17500
+ nextFiles,
17501
+ home
17502
+ );
17503
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
17504
+ return 0;
17505
+ }
17506
+ case "remove": {
17507
+ const paths = validatePaths(options.paths ?? [], options);
17508
+ if (!paths) return 1;
17509
+ const existingFiles = readSyncFiles(repoConfig);
17510
+ const nextFiles = removeSyncFiles(existingFiles, paths);
17511
+ if (repoEntry && nextFiles.length !== existingFiles.length) {
17512
+ await saveRepoSyncFiles(loaded.config, repoEntry.key, nextFiles, home);
17513
+ }
17514
+ writeSyncFiles(options.stdout, nextFiles, !!options.json);
17515
+ return 0;
17516
+ }
17517
+ }
17518
+ writeError(options, `unknown action: ${options.action}`);
17519
+ return 1;
17520
+ }
17521
+ function findRepoConfigEntry(config, repoRoot, home) {
17522
+ const repos = config.repos;
17523
+ if (!isPlainObject3(repos)) return null;
17524
+ for (const [key, value] of Object.entries(repos)) {
17525
+ if (expandTilde2(key, home) === repoRoot && isPlainObject3(value)) {
17526
+ return { config: value, key };
17527
+ }
17528
+ }
17529
+ return null;
17530
+ }
17531
+ function readSyncFiles(config) {
17532
+ const syncFiles2 = config.syncFiles;
17533
+ if (!Array.isArray(syncFiles2)) return [];
17534
+ return syncFiles2.filter((item) => typeof item === "string");
17535
+ }
17536
+ function writeSyncFiles(stdout, files, json) {
17537
+ if (json) {
17538
+ stdout(`${JSON.stringify(files, null, 2)}
17539
+ `);
17540
+ return;
17541
+ }
17542
+ if (files.length === 0) {
17543
+ stdout("No sync files configured for this repo.\n");
17544
+ return;
17545
+ }
17546
+ stdout(`${files.join("\n")}
17547
+ `);
17548
+ }
17549
+ function validatePaths(paths, options) {
17550
+ if (paths.length === 0) {
17551
+ writeError(options, "at least one path is required");
17552
+ return null;
17553
+ }
17554
+ const validatedPaths = [];
17555
+ for (const path9 of paths) {
17556
+ try {
17557
+ validatedPaths.push(validateSyncFilePattern(path9));
17558
+ } catch (error) {
17559
+ writeError(
17560
+ options,
17561
+ error instanceof Error ? error.message : String(error)
17562
+ );
17563
+ return null;
17564
+ }
17565
+ }
17566
+ return validatedPaths;
17567
+ }
17568
+ function mergeSyncFiles(existing, additions) {
17569
+ const nextFiles = [...existing];
17570
+ for (const path9 of additions) {
17571
+ if (!nextFiles.includes(path9)) {
17572
+ nextFiles.push(path9);
17573
+ }
17574
+ }
17575
+ return nextFiles;
17576
+ }
17577
+ function removeSyncFiles(existing, removals) {
17578
+ const removalSet = new Set(removals);
17579
+ return existing.filter((path9) => !removalSet.has(path9));
17580
+ }
17581
+ async function saveRepoSyncFiles(config, repoKey, syncFiles2, home) {
17582
+ const repos = isPlainObject3(config.repos) ? { ...config.repos } : {};
17583
+ const repoConfig = isPlainObject3(repos[repoKey]) ? repos[repoKey] : {};
17584
+ const nextRepoConfig = { ...repoConfig };
17585
+ if (syncFiles2.length > 0) {
17586
+ nextRepoConfig.syncFiles = syncFiles2;
17587
+ } else {
17588
+ delete nextRepoConfig.syncFiles;
17589
+ }
17590
+ if (Object.keys(nextRepoConfig).length > 0) {
17591
+ repos[repoKey] = nextRepoConfig;
17592
+ } else {
17593
+ delete repos[repoKey];
17594
+ }
17595
+ await saveGlobalConfig({ ...config, repos }, home);
17596
+ }
17597
+ function isPlainObject3(value) {
17598
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17599
+ }
17600
+ function writeError(options, message) {
17601
+ if (options.json) {
17602
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
17603
+ `);
17604
+ return;
17605
+ }
17606
+ options.stderr(`gji sync-files: ${message}
17607
+ `);
17608
+ }
17609
+ function expandTilde2(value, home) {
17610
+ if (value === "~") return home;
17611
+ if (value.startsWith("~/")) return join9(home, value.slice(2));
17612
+ return value;
16936
17613
  }
16937
17614
 
16938
17615
  // src/cli.ts
@@ -17001,46 +17678,117 @@ function maybeRegisterCurrentRepo(cwd) {
17001
17678
  detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
17002
17679
  }
17003
17680
  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"));
17681
+ program2.command("new [branch]").description("create a new branch or detached linked worktree").option(
17682
+ "-f, --force",
17683
+ "remove and recreate the worktree if the target path already exists"
17684
+ ).option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option(
17685
+ "--editor <cli>",
17686
+ "editor CLI to use with --open (code, cursor, zed, \u2026)"
17687
+ ).option(
17688
+ "--dry-run",
17689
+ "show what would be created without executing any git commands or writing files"
17690
+ ).option(
17691
+ "--json",
17692
+ "emit JSON on success or error instead of human-readable output"
17693
+ ).action(notImplemented("new"));
17005
17694
  program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
17006
17695
  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"));
17696
+ program2.command("pr <ref>").description(
17697
+ "fetch a pull request by number, #number, or URL into a linked worktree"
17698
+ ).option(
17699
+ "--dry-run",
17700
+ "show what would be created without executing any git commands or writing files"
17701
+ ).option(
17702
+ "--json",
17703
+ "emit JSON on success or error instead of human-readable output"
17704
+ ).action(notImplemented("pr"));
17705
+ program2.command("back [n]").description(
17706
+ "navigate to the previously visited worktree, optionally N steps back"
17707
+ ).option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
17009
17708
  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"));
17709
+ program2.command("open [branch]").description("open the worktree in an editor").option(
17710
+ "--editor <cli>",
17711
+ "editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)"
17712
+ ).option("--save", "save the chosen editor to global config").option(
17713
+ "--workspace",
17714
+ "generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)"
17715
+ ).action(notImplemented("open"));
17011
17716
  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
17717
  program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
17013
17718
  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"));
17719
+ program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option(
17720
+ "--json",
17721
+ "emit JSON on success or error instead of human-readable output"
17722
+ ).action(notImplemented("sync"));
17723
+ 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"));
17724
+ 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"));
17725
+ 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"));
17726
+ 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
17727
  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"));
17728
+ program2.command("clean").description("interactively prune linked worktrees").option(
17729
+ "-f, --force",
17730
+ "bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches"
17731
+ ).option(
17732
+ "--stale",
17733
+ "only target clean worktrees whose upstream is gone and branch is merged into the default branch"
17734
+ ).option("--dry-run", "show what would be deleted without removing anything").option(
17735
+ "--json",
17736
+ "emit JSON on success or error instead of human-readable output"
17737
+ ).action(notImplemented("clean"));
17738
+ program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option(
17739
+ "-f, --force",
17740
+ "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch"
17741
+ ).option("--dry-run", "show what would be deleted without removing anything").option(
17742
+ "--json",
17743
+ "emit JSON on success or error instead of human-readable output"
17744
+ ).action(notImplemented("remove"));
17745
+ program2.command("run-hook <hook>").alias("trigger-hook").description(
17746
+ "run a named hook (after-create, after-enter, before-remove) in the current worktree"
17747
+ ).action(notImplemented("run-hook"));
17748
+ 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(
17749
+ "--print",
17750
+ "print the resolved worktree path without changing directory"
17751
+ ).option(
17752
+ "--json",
17753
+ "emit JSON on success or error instead of human-readable output"
17754
+ ).action(notImplemented("warp"));
17020
17755
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
17021
17756
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
17022
17757
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
17023
17758
  configCommand.command("unset <key>").description("remove a global config value").action(notImplemented("config unset"));
17024
17759
  }
17025
17760
  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);
17761
+ program2.commands.find((command) => command.name() === "new")?.action(
17762
+ async (branch, commandOptions) => {
17763
+ const exitCode = await runNewCommand({
17764
+ ...options,
17765
+ branch,
17766
+ detached: commandOptions.detached,
17767
+ dryRun: commandOptions.dryRun,
17768
+ editor: commandOptions.editor,
17769
+ force: commandOptions.force,
17770
+ json: commandOptions.json,
17771
+ open: commandOptions.open
17772
+ });
17773
+ if (exitCode !== 0) {
17774
+ throw commanderExit(exitCode);
17775
+ }
17030
17776
  }
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);
17777
+ );
17778
+ program2.commands.find((command) => command.name() === "init")?.action(
17779
+ async (shell, commandOptions) => {
17780
+ const exitCode = await runInitCommand({
17781
+ cwd: options.cwd,
17782
+ shell,
17783
+ stderr: options.stderr,
17784
+ stdout: options.stdout,
17785
+ write: commandOptions.write
17786
+ });
17787
+ if (exitCode !== 0) {
17788
+ throw commanderExit(exitCode);
17789
+ }
17042
17790
  }
17043
- });
17791
+ );
17044
17792
  program2.commands.find((command) => command.name() === "completion")?.action(async (shell) => {
17045
17793
  const exitCode = await runCompletionCommand({
17046
17794
  shell,
@@ -17051,32 +17799,93 @@ function attachCommandActions(program2, options) {
17051
17799
  throw commanderExit(exitCode);
17052
17800
  }
17053
17801
  });
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 });
17802
+ program2.commands.find((command) => command.name() === "pr")?.action(
17803
+ async (number, commandOptions) => {
17804
+ const exitCode = await runPrCommand({
17805
+ cwd: options.cwd,
17806
+ dryRun: commandOptions.dryRun,
17807
+ json: commandOptions.json,
17808
+ number,
17809
+ stderr: options.stderr,
17810
+ stdout: options.stdout
17811
+ });
17812
+ if (exitCode !== 0) {
17813
+ throw commanderExit(exitCode);
17814
+ }
17815
+ }
17816
+ );
17817
+ program2.commands.find((command) => command.name() === "back")?.action(
17818
+ async (n, commandOptions) => {
17819
+ if (n !== void 0 && !/^\d+$/.test(n)) {
17820
+ options.stderr(`gji back: invalid step count: ${n}
17821
+ `);
17822
+ throw commanderExit(1);
17823
+ }
17824
+ const steps = n !== void 0 ? parseInt(n, 10) : void 0;
17825
+ const exitCode = await runBackCommand({
17826
+ cwd: options.cwd,
17827
+ n: steps,
17828
+ print: commandOptions.print,
17829
+ stderr: options.stderr,
17830
+ stdout: options.stdout
17831
+ });
17832
+ if (exitCode !== 0) {
17833
+ throw commanderExit(exitCode);
17834
+ }
17835
+ }
17836
+ );
17837
+ program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
17838
+ const exitCode = await runHistoryCommand({
17839
+ cwd: options.cwd,
17840
+ json: commandOptions.json,
17841
+ stdout: options.stdout
17842
+ });
17056
17843
  if (exitCode !== 0) {
17057
17844
  throw commanderExit(exitCode);
17058
17845
  }
17059
17846
  });
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);
17847
+ program2.commands.find((command) => command.name() === "open")?.action(
17848
+ async (branch, commandOptions) => {
17849
+ const exitCode = await runOpenCommand({
17850
+ branch,
17851
+ cwd: options.cwd,
17852
+ editor: commandOptions.editor,
17853
+ save: commandOptions.save,
17854
+ stderr: options.stderr,
17855
+ stdout: options.stdout,
17856
+ workspace: commandOptions.workspace
17857
+ });
17858
+ if (exitCode !== 0) {
17859
+ throw commanderExit(exitCode);
17860
+ }
17861
+ }
17862
+ );
17863
+ program2.commands.find((command) => command.name() === "go")?.action(
17864
+ async (branch, commandOptions) => {
17865
+ const exitCode = await runGoCommand({
17866
+ branch,
17867
+ cwd: options.cwd,
17868
+ print: commandOptions.print,
17869
+ stderr: options.stderr,
17870
+ stdout: options.stdout
17871
+ });
17872
+ if (exitCode !== 0) {
17873
+ throw commanderExit(exitCode);
17874
+ }
17065
17875
  }
17066
- const steps = n !== void 0 ? parseInt(n, 10) : void 0;
17067
- const exitCode = await runBackCommand({
17876
+ );
17877
+ program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17878
+ const exitCode = await runRootCommand({
17068
17879
  cwd: options.cwd,
17069
- n: steps,
17070
17880
  print: commandOptions.print,
17071
- stderr: options.stderr,
17072
17881
  stdout: options.stdout
17073
17882
  });
17074
17883
  if (exitCode !== 0) {
17075
17884
  throw commanderExit(exitCode);
17076
17885
  }
17077
17886
  });
17078
- program2.commands.find((command) => command.name() === "history")?.action(async (commandOptions) => {
17079
- const exitCode = await runHistoryCommand({
17887
+ program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17888
+ const exitCode = await runStatusCommand({
17080
17889
  cwd: options.cwd,
17081
17890
  json: commandOptions.json,
17082
17891
  stdout: options.stdout
@@ -17085,25 +17894,26 @@ function attachCommandActions(program2, options) {
17085
17894
  throw commanderExit(exitCode);
17086
17895
  }
17087
17896
  });
17088
- program2.commands.find((command) => command.name() === "open")?.action(async (branch, commandOptions) => {
17089
- const exitCode = await runOpenCommand({
17090
- branch,
17897
+ program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17898
+ const exitCode = await runSyncCommand({
17899
+ all: commandOptions.all,
17091
17900
  cwd: options.cwd,
17092
- editor: commandOptions.editor,
17093
- save: commandOptions.save,
17901
+ json: commandOptions.json,
17094
17902
  stderr: options.stderr,
17095
- stdout: options.stdout,
17096
- workspace: commandOptions.workspace
17903
+ stdout: options.stdout
17097
17904
  });
17098
17905
  if (exitCode !== 0) {
17099
17906
  throw commanderExit(exitCode);
17100
17907
  }
17101
17908
  });
17102
- program2.commands.find((command) => command.name() === "go")?.action(async (branch, commandOptions) => {
17103
- const exitCode = await runGoCommand({
17104
- branch,
17909
+ const syncFilesCommand = program2.commands.find(
17910
+ (command) => command.name() === "sync-files"
17911
+ );
17912
+ syncFilesCommand?.action(async (commandOptions) => {
17913
+ const exitCode = await runSyncFilesCommand({
17914
+ action: "list",
17105
17915
  cwd: options.cwd,
17106
- print: commandOptions.print,
17916
+ json: commandOptions.json,
17107
17917
  stderr: options.stderr,
17108
17918
  stdout: options.stdout
17109
17919
  });
@@ -17111,38 +17921,45 @@ function attachCommandActions(program2, options) {
17111
17921
  throw commanderExit(exitCode);
17112
17922
  }
17113
17923
  });
17114
- program2.commands.find((command) => command.name() === "root")?.action(async (commandOptions) => {
17115
- const exitCode = await runRootCommand({
17924
+ syncFilesCommand?.commands.find((command) => command.name() === "list")?.action(async (commandOptions) => {
17925
+ const exitCode = await runSyncFilesCommand({
17926
+ action: "list",
17116
17927
  cwd: options.cwd,
17117
- print: commandOptions.print,
17928
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17929
+ stderr: options.stderr,
17118
17930
  stdout: options.stdout
17119
17931
  });
17120
17932
  if (exitCode !== 0) {
17121
17933
  throw commanderExit(exitCode);
17122
17934
  }
17123
17935
  });
17124
- program2.commands.find((command) => command.name() === "status")?.action(async (commandOptions) => {
17125
- const exitCode = await runStatusCommand({
17936
+ syncFilesCommand?.commands.find((command) => command.name() === "add")?.action(async (paths, commandOptions) => {
17937
+ const exitCode = await runSyncFilesCommand({
17938
+ action: "add",
17126
17939
  cwd: options.cwd,
17127
- json: commandOptions.json,
17940
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17941
+ paths,
17942
+ stderr: options.stderr,
17128
17943
  stdout: options.stdout
17129
17944
  });
17130
17945
  if (exitCode !== 0) {
17131
17946
  throw commanderExit(exitCode);
17132
17947
  }
17133
17948
  });
17134
- program2.commands.find((command) => command.name() === "sync")?.action(async (commandOptions) => {
17135
- const exitCode = await runSyncCommand({
17136
- all: commandOptions.all,
17949
+ const runSyncFilesRemoveCommand = async (paths, commandOptions) => {
17950
+ const exitCode = await runSyncFilesCommand({
17951
+ action: "remove",
17137
17952
  cwd: options.cwd,
17138
- json: commandOptions.json,
17953
+ json: commandOptions.json || syncFilesCommand?.opts().json,
17954
+ paths,
17139
17955
  stderr: options.stderr,
17140
17956
  stdout: options.stdout
17141
17957
  });
17142
17958
  if (exitCode !== 0) {
17143
17959
  throw commanderExit(exitCode);
17144
17960
  }
17145
- });
17961
+ };
17962
+ syncFilesCommand?.commands.find((command) => command.name() === "remove")?.action(runSyncFilesRemoveCommand);
17146
17963
  program2.commands.find((command) => command.name() === "ls")?.action(async (commandOptions) => {
17147
17964
  const exitCode = await runLsCommand({
17148
17965
  compact: commandOptions.compact,
@@ -17154,20 +17971,22 @@ function attachCommandActions(program2, options) {
17154
17971
  throw commanderExit(exitCode);
17155
17972
  }
17156
17973
  });
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);
17974
+ program2.commands.find((command) => command.name() === "clean")?.action(
17975
+ async (commandOptions) => {
17976
+ const exitCode = await runCleanCommand({
17977
+ cwd: options.cwd,
17978
+ dryRun: commandOptions.dryRun,
17979
+ force: commandOptions.force,
17980
+ json: commandOptions.json,
17981
+ stale: commandOptions.stale,
17982
+ stderr: options.stderr,
17983
+ stdout: options.stdout
17984
+ });
17985
+ if (exitCode !== 0) {
17986
+ throw commanderExit(exitCode);
17987
+ }
17169
17988
  }
17170
- });
17989
+ );
17171
17990
  const runRemovalCommand = async (branch, commandOptions = {}) => {
17172
17991
  const exitCode = await runRemoveCommand({
17173
17992
  branch,
@@ -17183,8 +18002,8 @@ function attachCommandActions(program2, options) {
17183
18002
  }
17184
18003
  };
17185
18004
  program2.commands.find((command) => command.name() === "remove")?.action(runRemovalCommand);
17186
- program2.commands.find((command) => command.name() === "trigger-hook")?.action(async (hook) => {
17187
- const exitCode = await runTriggerHookCommand({
18005
+ program2.commands.find((command) => command.name() === "run-hook")?.action(async (hook) => {
18006
+ const exitCode = await runHookCommand({
17188
18007
  cwd: options.cwd,
17189
18008
  hook,
17190
18009
  stderr: options.stderr
@@ -17193,23 +18012,27 @@ function attachCommandActions(program2, options) {
17193
18012
  throw commanderExit(exitCode);
17194
18013
  }
17195
18014
  });
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);
18015
+ program2.commands.find((command) => command.name() === "warp")?.action(
18016
+ async (branch, commandOptions) => {
18017
+ const newFlag = commandOptions.new;
18018
+ const newWorktree = newFlag !== void 0 && newFlag !== false;
18019
+ const newBranch = typeof newFlag === "string" ? newFlag : void 0;
18020
+ const exitCode = await runWarpCommand({
18021
+ branch: newWorktree ? newBranch ?? branch : branch,
18022
+ cwd: options.cwd,
18023
+ json: commandOptions.json,
18024
+ newWorktree,
18025
+ stderr: options.stderr,
18026
+ stdout: options.stdout
18027
+ });
18028
+ if (exitCode !== 0) {
18029
+ throw commanderExit(exitCode);
18030
+ }
17210
18031
  }
17211
- });
17212
- const configCommand = program2.commands.find((command) => command.name() === "config");
18032
+ );
18033
+ const configCommand = program2.commands.find(
18034
+ (command) => command.name() === "config"
18035
+ );
17213
18036
  configCommand?.action(async () => {
17214
18037
  const exitCode = await runConfigCommand({
17215
18038
  cwd: options.cwd,
@@ -17291,7 +18114,7 @@ async function main() {
17291
18114
  }
17292
18115
  async function warnIfMissingShellIntegration() {
17293
18116
  try {
17294
- const { config } = await loadGlobalConfig(homedir6());
18117
+ const { config } = await loadGlobalConfig(homedir7());
17295
18118
  if (!config.shellIntegration) {
17296
18119
  const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
17297
18120
  const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";