@solaqua/gji 0.5.0 → 0.6.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.
@@ -3951,8 +3951,8 @@ var require_graceful_fs = __commonJS({
3951
3951
  fs6.createReadStream = createReadStream;
3952
3952
  fs6.createWriteStream = createWriteStream;
3953
3953
  var fs$readFile = fs6.readFile;
3954
- fs6.readFile = readFile4;
3955
- function readFile4(path9, options, cb) {
3954
+ fs6.readFile = readFile5;
3955
+ function readFile5(path9, options, cb) {
3956
3956
  if (typeof options === "function")
3957
3957
  cb = options, options = null;
3958
3958
  return go$readFile(path9, options, cb);
@@ -3968,8 +3968,8 @@ var require_graceful_fs = __commonJS({
3968
3968
  }
3969
3969
  }
3970
3970
  var fs$writeFile = fs6.writeFile;
3971
- fs6.writeFile = writeFile5;
3972
- function writeFile5(path9, data, options, cb) {
3971
+ fs6.writeFile = writeFile7;
3972
+ function writeFile7(path9, data, options, cb) {
3973
3973
  if (typeof options === "function")
3974
3974
  cb = options, options = null;
3975
3975
  return go$writeFile(path9, data, options, cb);
@@ -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 join7 = __require("path").join;
5389
+ var join9 = __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
- join7(etc, name, "config"),
5415
- join7(etc, name + "rc")
5414
+ join9(etc, name, "config"),
5415
+ join9(etc, name + "rc")
5416
5416
  ].forEach(addConfigFile);
5417
5417
  if (home)
5418
5418
  [
5419
- join7(home, ".config", name, "config"),
5420
- join7(home, ".config", name),
5421
- join7(home, "." + name, "config"),
5422
- join7(home, "." + name + "rc")
5419
+ join9(home, ".config", name, "config"),
5420
+ join9(home, ".config", name),
5421
+ join9(home, "." + name, "config"),
5422
+ join9(home, "." + name + "rc")
5423
5423
  ].forEach(addConfigFile);
5424
5424
  addConfigFile(cc.find("." + name + "rc"));
5425
5425
  if (env4.config) addConfigFile(env4.config);
@@ -5925,8 +5925,8 @@ var require_graceful_fs2 = __commonJS({
5925
5925
  fs6.createReadStream = createReadStream;
5926
5926
  fs6.createWriteStream = createWriteStream;
5927
5927
  var fs$readFile = fs6.readFile;
5928
- fs6.readFile = readFile4;
5929
- function readFile4(path9, options, cb) {
5928
+ fs6.readFile = readFile5;
5929
+ function readFile5(path9, options, cb) {
5930
5930
  if (typeof options === "function")
5931
5931
  cb = options, options = null;
5932
5932
  return go$readFile(path9, options, cb);
@@ -5942,8 +5942,8 @@ var require_graceful_fs2 = __commonJS({
5942
5942
  }
5943
5943
  }
5944
5944
  var fs$writeFile = fs6.writeFile;
5945
- fs6.writeFile = writeFile5;
5946
- function writeFile5(path9, data, options, cb) {
5945
+ fs6.writeFile = writeFile7;
5946
+ function writeFile7(path9, data, options, cb) {
5947
5947
  if (typeof options === "function")
5948
5948
  cb = options, options = null;
5949
5949
  return go$writeFile(path9, data, options, cb);
@@ -6757,11 +6757,11 @@ var require_util = __commonJS({
6757
6757
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
6758
6758
  return name2;
6759
6759
  }
6760
- const dirname8 = path9.dirname(name2);
6761
- if (dirname8 === name2) {
6760
+ const dirname9 = path9.dirname(name2);
6761
+ if (dirname9 === name2) {
6762
6762
  return original;
6763
6763
  }
6764
- return find(dirname8, original);
6764
+ return find(dirname9, original);
6765
6765
  } catch (error) {
6766
6766
  if (name2 === original) {
6767
6767
  if (error.code === "ENOENT") {
@@ -9422,7 +9422,7 @@ var require_picocolors = __commonJS({
9422
9422
  });
9423
9423
 
9424
9424
  // src/index.ts
9425
- import { homedir as homedir5 } from "node:os";
9425
+ import { homedir as homedir6 } from "node:os";
9426
9426
 
9427
9427
  // src/config.ts
9428
9428
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -9433,6 +9433,7 @@ var GLOBAL_CONFIG_DIRECTORY = ".config/gji";
9433
9433
  var GLOBAL_CONFIG_NAME = "config.json";
9434
9434
  var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
9435
9435
  "branchPrefix",
9436
+ "editor",
9436
9437
  "hooks",
9437
9438
  "installSaveTarget",
9438
9439
  "shellIntegration",
@@ -9739,7 +9740,7 @@ var retryifyAsync = (fn, options) => {
9739
9740
  throw error;
9740
9741
  const delay2 = Math.round(interval * Math.random());
9741
9742
  if (delay2 > 0) {
9742
- const delayPromise = new Promise((resolve4) => setTimeout(resolve4, delay2));
9743
+ const delayPromise = new Promise((resolve5) => setTimeout(resolve5, delay2));
9743
9744
  return delayPromise.then(() => attempt.apply(void 0, args));
9744
9745
  } else {
9745
9746
  return attempt.apply(void 0, args);
@@ -9998,14 +9999,14 @@ var Temp = {
9998
9999
  }
9999
10000
  },
10000
10001
  truncate: (filePath) => {
10001
- const basename7 = path2.basename(filePath);
10002
- if (basename7.length <= LIMIT_BASENAME_LENGTH)
10002
+ const basename8 = path2.basename(filePath);
10003
+ if (basename8.length <= LIMIT_BASENAME_LENGTH)
10003
10004
  return filePath;
10004
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename7);
10005
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename8);
10005
10006
  if (!truncable)
10006
10007
  return filePath;
10007
- const truncationLength = basename7.length - LIMIT_BASENAME_LENGTH;
10008
- return `${filePath.slice(0, -basename7.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
10008
+ const truncationLength = basename8.length - LIMIT_BASENAME_LENGTH;
10009
+ return `${filePath.slice(0, -basename8.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
10009
10010
  }
10010
10011
  };
10011
10012
  node_default(Temp.purgeSyncAll);
@@ -11305,14 +11306,14 @@ var TimeoutError = class extends Error {
11305
11306
 
11306
11307
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
11307
11308
  async function timeout(request, init, abortController, options) {
11308
- return new Promise((resolve4, reject) => {
11309
+ return new Promise((resolve5, reject) => {
11309
11310
  const timeoutId = setTimeout(() => {
11310
11311
  if (abortController) {
11311
11312
  abortController.abort();
11312
11313
  }
11313
11314
  reject(new TimeoutError(request));
11314
11315
  }, options.timeout);
11315
- void options.fetch(request, init).then(resolve4).catch(reject).then(() => {
11316
+ void options.fetch(request, init).then(resolve5).catch(reject).then(() => {
11316
11317
  clearTimeout(timeoutId);
11317
11318
  });
11318
11319
  });
@@ -11320,7 +11321,7 @@ async function timeout(request, init, abortController, options) {
11320
11321
 
11321
11322
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
11322
11323
  async function delay(ms, { signal }) {
11323
- return new Promise((resolve4, reject) => {
11324
+ return new Promise((resolve5, reject) => {
11324
11325
  if (signal) {
11325
11326
  signal.throwIfAborted();
11326
11327
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -11331,7 +11332,7 @@ async function delay(ms, { signal }) {
11331
11332
  }
11332
11333
  const timeoutId = setTimeout(() => {
11333
11334
  signal?.removeEventListener("abort", abortHandler);
11334
- resolve4();
11335
+ resolve5();
11335
11336
  }, ms);
11336
11337
  });
11337
11338
  }
@@ -13035,18 +13036,50 @@ import { basename as basename2 } from "node:path";
13035
13036
  import { spawn as spawn2 } from "node:child_process";
13036
13037
  async function runHook(hookCmd, cwd, context, stderr) {
13037
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);
13044
+ }
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");
13049
+ return;
13050
+ }
13051
+ await new Promise((resolve5) => {
13052
+ const child = spawn2(command, args, {
13053
+ cwd,
13054
+ shell: false,
13055
+ stdio: ["ignore", "inherit", "pipe"],
13056
+ env: hookEnvironment(context)
13057
+ });
13058
+ child.stderr.on("data", (chunk) => {
13059
+ stderr(chunk.toString());
13060
+ });
13061
+ child.on("close", (code) => {
13062
+ if (code !== 0) {
13063
+ stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
13064
+ `);
13065
+ }
13066
+ resolve5();
13067
+ });
13068
+ child.on("error", (err) => {
13069
+ stderr(`gji: hook failed to start: ${err.message}
13070
+ `);
13071
+ resolve5();
13072
+ });
13073
+ });
13074
+ }
13075
+ async function runShellHook(hookCmd, cwd, context, stderr) {
13038
13076
  const interpolated = interpolate(hookCmd, context);
13039
- await new Promise((resolve4) => {
13077
+ await new Promise((resolve5) => {
13040
13078
  const child = spawn2(interpolated, {
13041
13079
  cwd,
13042
13080
  shell: true,
13043
13081
  stdio: ["ignore", "inherit", "pipe"],
13044
- env: {
13045
- ...process.env,
13046
- GJI_BRANCH: context.branch ?? "",
13047
- GJI_PATH: context.path,
13048
- GJI_REPO: context.repo
13049
- }
13082
+ env: hookEnvironment(context)
13050
13083
  });
13051
13084
  child.stderr.on("data", (chunk) => {
13052
13085
  stderr(chunk.toString());
@@ -13056,15 +13089,26 @@ async function runHook(hookCmd, cwd, context, stderr) {
13056
13089
  stderr(`gji: hook exited with code ${code}: ${interpolated}
13057
13090
  `);
13058
13091
  }
13059
- resolve4();
13092
+ resolve5();
13060
13093
  });
13061
13094
  child.on("error", (err) => {
13062
13095
  stderr(`gji: hook failed to start: ${err.message}
13063
13096
  `);
13064
- resolve4();
13097
+ resolve5();
13065
13098
  });
13066
13099
  });
13067
13100
  }
13101
+ function hookEnvironment(context) {
13102
+ return {
13103
+ ...process.env,
13104
+ GJI_BRANCH: context.branch ?? "",
13105
+ GJI_PATH: context.path,
13106
+ GJI_REPO: context.repo
13107
+ };
13108
+ }
13109
+ function formatArgvHook(command, args) {
13110
+ return JSON.stringify([command, ...args]);
13111
+ }
13068
13112
  function interpolate(template, context) {
13069
13113
  return template.replace(/\{\{branch\}\}/g, context.branch ?? "").replace(/\{\{path\}\}/g, context.path).replace(/\{\{repo\}\}/g, context.repo);
13070
13114
  }
@@ -13075,11 +13119,18 @@ function extractHooks(config) {
13075
13119
  }
13076
13120
  const hooks = raw;
13077
13121
  return {
13078
- afterCreate: typeof hooks.afterCreate === "string" ? hooks.afterCreate : void 0,
13079
- afterEnter: typeof hooks.afterEnter === "string" ? hooks.afterEnter : void 0,
13080
- beforeRemove: typeof hooks.beforeRemove === "string" ? hooks.beforeRemove : void 0
13122
+ afterCreate: parseHookCommand(hooks.afterCreate),
13123
+ afterEnter: parseHookCommand(hooks.afterEnter),
13124
+ beforeRemove: parseHookCommand(hooks.beforeRemove)
13081
13125
  };
13082
13126
  }
13127
+ function parseHookCommand(value) {
13128
+ if (typeof value === "string") return value;
13129
+ if (Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string")) {
13130
+ return value;
13131
+ }
13132
+ return void 0;
13133
+ }
13083
13134
 
13084
13135
  // src/history.ts
13085
13136
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
@@ -14431,7 +14482,11 @@ var TOP_LEVEL_COMMANDS = [
14431
14482
  { name: "init", description: "print or install shell integration" },
14432
14483
  { name: "completion", description: "print shell completion definitions" },
14433
14484
  { name: "pr", description: "fetch a pull request into a linked worktree" },
14485
+ { name: "back", description: "navigate to the previously visited worktree" },
14486
+ { name: "history", description: "show navigation history" },
14487
+ { name: "open", description: "open the worktree in an editor" },
14434
14488
  { name: "go", description: "print or select a worktree path" },
14489
+ { name: "jump", description: "alias of go" },
14435
14490
  { name: "root", description: "print the main repository root path" },
14436
14491
  { name: "status", description: "summarize repository and worktree health" },
14437
14492
  { name: "sync", description: "fetch and update one or all worktrees" },
@@ -14440,20 +14495,12 @@ var TOP_LEVEL_COMMANDS = [
14440
14495
  { name: "remove", description: "remove a linked worktree and delete its branch when present" },
14441
14496
  { name: "rm", description: "alias of remove" },
14442
14497
  { name: "trigger-hook", description: "run a named hook in the current worktree" },
14498
+ { name: "warp", description: "jump to any worktree across all known repos" },
14443
14499
  { name: "config", description: "manage global config defaults" }
14444
14500
  ];
14445
14501
  var SHELL_NAMES = ["bash", "fish", "zsh"];
14446
14502
  var HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
14447
- var CONFIG_KEYS = [
14448
- "branchPrefix",
14449
- "syncRemote",
14450
- "syncDefaultBranch",
14451
- "syncFiles",
14452
- "skipInstallPrompt",
14453
- "installSaveTarget",
14454
- "hooks",
14455
- "repos"
14456
- ];
14503
+ var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
14457
14504
  function renderShellCompletion(shell) {
14458
14505
  switch (shell) {
14459
14506
  case "bash":
@@ -14487,7 +14534,7 @@ _gji_completion() {
14487
14534
 
14488
14535
  case "$command_name" in
14489
14536
  new)
14490
- COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
14537
+ COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
14491
14538
  ;;
14492
14539
  init)
14493
14540
  COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
@@ -14498,7 +14545,16 @@ _gji_completion() {
14498
14545
  pr)
14499
14546
  COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
14500
14547
  ;;
14501
- go)
14548
+ back)
14549
+ COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
14550
+ ;;
14551
+ history)
14552
+ COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
14553
+ ;;
14554
+ open)
14555
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
14556
+ ;;
14557
+ go|jump)
14502
14558
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
14503
14559
  ;;
14504
14560
  root)
@@ -14522,6 +14578,9 @@ _gji_completion() {
14522
14578
  trigger-hook)
14523
14579
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
14524
14580
  ;;
14581
+ warp)
14582
+ COMPREPLY=( $(compgen -W "-n --new --print --json --help" -- "$cur") )
14583
+ ;;
14525
14584
  config)
14526
14585
  if [ "$COMP_CWORD" -eq 2 ]; then
14527
14586
  COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
@@ -14585,6 +14644,9 @@ complete -c gji -f
14585
14644
  ${commandLines}
14586
14645
 
14587
14646
  complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
14647
+ complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
14648
+ complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
14649
+ complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, \u2026)'
14588
14650
  complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
14589
14651
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
14590
14652
 
@@ -14598,8 +14660,17 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
14598
14660
  complete -c gji -n '__fish_seen_subcommand_from pr' -l dry-run -d 'show what would be created without executing any git commands or writing files'
14599
14661
  complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
14600
14662
 
14601
- complete -c gji -n '__fish_seen_subcommand_from go' -l print -d 'print the resolved worktree path explicitly'
14602
- complete -c gji -n '__fish_seen_subcommand_from go' -a '(__gji_worktree_branches)' -d 'worktree branch'
14663
+ complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
14664
+
14665
+ complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
14666
+
14667
+ complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)'
14668
+ complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
14669
+ complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
14670
+ complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
14671
+
14672
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
14673
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
14603
14674
 
14604
14675
  complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
14605
14676
 
@@ -14623,6 +14694,10 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
14623
14694
 
14624
14695
  ${hookLines}
14625
14696
 
14697
+ complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
14698
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
14699
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
14700
+
14626
14701
  complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
14627
14702
  ${configKeyLines}`;
14628
14703
  }
@@ -14633,89 +14708,99 @@ function renderZshCompletion() {
14633
14708
  const configKeys = CONFIG_KEYS.join(" ");
14634
14709
  const shells = SHELL_NAMES.join(" ");
14635
14710
  const hooks = HOOK_NAMES.join(" ");
14636
- return `__gji_worktree_branches() {
14711
+ return `#compdef gji
14712
+
14713
+ __gji_worktree_branches() {
14637
14714
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
14638
14715
  }
14639
14716
 
14640
- _gji_completion() {
14641
- local context state line
14642
- local -a commands worktree_branches
14643
-
14644
- commands=(
14645
- ${commandLines}
14646
- )
14717
+ local context state line
14718
+ local -a commands worktree_branches
14647
14719
 
14648
- if (( CURRENT == 2 )); then
14649
- _describe 'command' commands
14650
- return
14651
- fi
14720
+ commands=(
14721
+ ${commandLines}
14722
+ )
14652
14723
 
14653
- case "\${words[2]}" in
14654
- new)
14655
- _arguments '--detached[create a detached worktree without a branch]' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
14656
- ;;
14657
- init)
14658
- _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
14659
- ;;
14660
- completion)
14661
- _arguments '2:shell:(${shells})'
14662
- ;;
14663
- pr)
14664
- _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
14665
- ;;
14666
- go)
14667
- _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
14668
- ;;
14669
- root)
14670
- _arguments '--print[print the resolved repository root path explicitly]'
14671
- ;;
14672
- status)
14673
- _arguments '--json[print repository and worktree health as JSON]'
14674
- ;;
14675
- sync)
14676
- _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14677
- ;;
14678
- ls)
14679
- _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14680
- ;;
14681
- clean)
14682
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
14683
- ;;
14684
- remove|rm)
14685
- _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'
14686
- ;;
14687
- trigger-hook)
14688
- _arguments "2:hook:(${hooks})"
14689
- ;;
14690
- config)
14691
- if (( CURRENT == 3 )); then
14692
- _values 'config action' get set unset
14693
- return
14694
- fi
14724
+ if (( CURRENT == 2 )); then
14725
+ _describe 'command' commands
14726
+ return
14727
+ fi
14695
14728
 
14696
- case "\${words[3]}" in
14697
- get|unset)
14698
- _arguments '3:key:->config_keys'
14699
- ;;
14700
- set)
14701
- _arguments '3:key:->config_keys' '4:value: '
14702
- ;;
14703
- esac
14704
- ;;
14705
- esac
14729
+ case "\${words[2]}" in
14730
+ new)
14731
+ _arguments '--detached[create a detached worktree without a branch]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, \u2026)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
14732
+ ;;
14733
+ init)
14734
+ _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
14735
+ ;;
14736
+ completion)
14737
+ _arguments '2:shell:(${shells})'
14738
+ ;;
14739
+ pr)
14740
+ _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
14741
+ ;;
14742
+ back)
14743
+ _arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
14744
+ ;;
14745
+ history)
14746
+ _arguments '--json[print history as JSON]'
14747
+ ;;
14748
+ open)
14749
+ _arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
14750
+ ;;
14751
+ go|jump)
14752
+ _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
14753
+ ;;
14754
+ root)
14755
+ _arguments '--print[print the resolved repository root path explicitly]'
14756
+ ;;
14757
+ status)
14758
+ _arguments '--json[print repository and worktree health as JSON]'
14759
+ ;;
14760
+ sync)
14761
+ _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14762
+ ;;
14763
+ ls)
14764
+ _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14765
+ ;;
14766
+ clean)
14767
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
14768
+ ;;
14769
+ remove|rm)
14770
+ _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
+ ;;
14772
+ trigger-hook)
14773
+ _arguments "2:hook:(${hooks})"
14774
+ ;;
14775
+ warp)
14776
+ _arguments '(-n --new)'{-n,--new}'[create a new worktree in a registered repo]:branch:' '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
14777
+ ;;
14778
+ config)
14779
+ if (( CURRENT == 3 )); then
14780
+ _values 'config action' get set unset
14781
+ return
14782
+ fi
14706
14783
 
14707
- case "$state" in
14708
- worktrees)
14709
- worktree_branches=(\${(@f)$(__gji_worktree_branches)})
14710
- _describe 'worktree branch' worktree_branches
14711
- ;;
14712
- config_keys)
14713
- _values 'config key' ${configKeys}
14714
- ;;
14715
- esac
14716
- }
14784
+ case "\${words[3]}" in
14785
+ get|unset)
14786
+ _arguments '3:key:->config_keys'
14787
+ ;;
14788
+ set)
14789
+ _arguments '3:key:->config_keys' '4:value: '
14790
+ ;;
14791
+ esac
14792
+ ;;
14793
+ esac
14717
14794
 
14718
- compdef _gji_completion gji`;
14795
+ case "$state" in
14796
+ worktrees)
14797
+ worktree_branches=(\${(@f)$(__gji_worktree_branches)})
14798
+ _describe 'worktree branch' worktree_branches
14799
+ ;;
14800
+ config_keys)
14801
+ _values 'config key' ${configKeys}
14802
+ ;;
14803
+ esac`;
14719
14804
  }
14720
14805
  function escapeSingleQuotes(value) {
14721
14806
  return value.replace(/'/g, `'\\''`);
@@ -14796,990 +14881,1449 @@ function writeJson(stdout, value) {
14796
14881
  }
14797
14882
 
14798
14883
  // src/go.ts
14799
- import { basename as basename3 } from "node:path";
14800
- var GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
14801
- function createGoCommand(dependencies = {}) {
14802
- const prompt = dependencies.promptForWorktree ?? promptForWorktree;
14803
- return async function runGoCommand2(options) {
14804
- const [worktrees, repository] = await Promise.all([
14805
- listWorktrees(options.cwd),
14806
- detectRepository(options.cwd)
14807
- ]);
14808
- if (!options.branch && isHeadless()) {
14809
- options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
14810
- return 1;
14884
+ import { basename as basename5 } from "node:path";
14885
+
14886
+ // src/new.ts
14887
+ import { mkdir as mkdir4 } from "node:fs/promises";
14888
+ import { basename as basename3, dirname as dirname5 } from "node:path";
14889
+ import { execFile as execFile3 } from "node:child_process";
14890
+ import { promisify as promisify4 } from "node:util";
14891
+
14892
+ // src/editor.ts
14893
+ import { spawn as spawn3 } from "node:child_process";
14894
+ var EDITORS = [
14895
+ { cli: "cursor", name: "Cursor", newWindowFlag: "--new-window", supportsWorkspace: true },
14896
+ { cli: "code", name: "VS Code", newWindowFlag: "--new-window", supportsWorkspace: true },
14897
+ { cli: "windsurf", name: "Windsurf", newWindowFlag: "--new-window", supportsWorkspace: true },
14898
+ { cli: "zed", name: "Zed", supportsWorkspace: false },
14899
+ { cli: "subl", name: "Sublime Text", newWindowFlag: "--new-window", supportsWorkspace: false }
14900
+ ];
14901
+ async function defaultSpawnEditor(cli, args) {
14902
+ const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
14903
+ await new Promise((resolve5, reject) => {
14904
+ child.once("error", reject);
14905
+ child.once("spawn", resolve5);
14906
+ });
14907
+ child.unref();
14908
+ }
14909
+
14910
+ // src/file-sync.ts
14911
+ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
14912
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
14913
+ async function syncFiles(mainRoot, targetPath, patterns) {
14914
+ for (const pattern of patterns) {
14915
+ if (isAbsolute2(pattern)) {
14916
+ throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
14811
14917
  }
14812
- const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
14813
- const resolvedPath = options.branch ? worktrees.find((entry) => entry.branch === options.branch)?.path : prompted ?? void 0;
14814
- if (!resolvedPath) {
14815
- if (options.branch) {
14816
- options.stderr(`No worktree found for branch: ${options.branch}
14817
- `);
14818
- options.stderr(`Hint: Use 'gji ls' to see available worktrees
14819
- `);
14820
- } else {
14821
- options.stderr("Aborted\n");
14822
- }
14823
- return 1;
14918
+ const normalized = normalize(pattern);
14919
+ if (normalized.startsWith("..")) {
14920
+ throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
14824
14921
  }
14825
- const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
14826
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
14827
- const hooks = extractHooks(config);
14828
- await runHook(
14829
- hooks.afterEnter,
14830
- resolvedPath,
14831
- { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename3(repository.repoRoot) },
14832
- options.stderr
14833
- );
14834
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
14835
- await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
14836
- return 0;
14837
- };
14922
+ const sourcePath = join4(mainRoot, normalized);
14923
+ const destPath = join4(targetPath, normalized);
14924
+ const sourceExists = await fileExists(sourcePath);
14925
+ if (!sourceExists) {
14926
+ continue;
14927
+ }
14928
+ const destExists = await fileExists(destPath);
14929
+ if (destExists) {
14930
+ continue;
14931
+ }
14932
+ await mkdir3(dirname4(destPath), { recursive: true });
14933
+ await copyFile(sourcePath, destPath);
14934
+ }
14838
14935
  }
14839
- var runGoCommand = createGoCommand();
14840
- async function promptForWorktree(worktrees) {
14841
- const healthResults = await Promise.allSettled(
14842
- worktrees.map((w2) => readWorktreeHealth(w2.path))
14843
- );
14844
- const choice = await ve({
14845
- message: "Choose a worktree",
14846
- options: worktrees.map((worktree, i) => {
14847
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
14848
- const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
14849
- const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
14850
- return {
14851
- value: worktree.path,
14852
- label: worktree.branch ?? "(detached)",
14853
- hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
14854
- };
14855
- })
14856
- });
14857
- if (pD(choice)) {
14858
- return null;
14936
+ async function fileExists(path9) {
14937
+ try {
14938
+ await stat(path9);
14939
+ return true;
14940
+ } catch (error) {
14941
+ if (isNotFoundError(error)) {
14942
+ return false;
14943
+ }
14944
+ throw error;
14859
14945
  }
14860
- return choice;
14861
14946
  }
14862
- function formatUpstreamHint(branch, health) {
14863
- if (branch === null) return null;
14864
- if (!health.hasUpstream) return "no upstream";
14865
- if (health.upstreamGone) return "upstream gone";
14866
- if (health.ahead === 0 && health.behind === 0) return "up to date";
14867
- if (health.ahead === 0) return `behind ${health.behind}`;
14868
- if (health.behind === 0) return `ahead ${health.ahead}`;
14869
- return `ahead ${health.ahead}, behind ${health.behind}`;
14947
+ function isNotFoundError(error) {
14948
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
14870
14949
  }
14871
14950
 
14872
- // src/init.ts
14873
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
14874
- import { homedir as homedir4 } from "node:os";
14875
- import { dirname as dirname4, join as join4 } from "node:path";
14876
- var START_MARKER = "# >>> gji init >>>";
14877
- var END_MARKER = "# <<< gji init <<<";
14878
- var SHELL_WRAPPED_COMMANDS = [
14879
- {
14880
- bypassOption: "--help",
14881
- commandName: "new",
14882
- envVar: "GJI_NEW_OUTPUT_FILE",
14883
- names: ["new"],
14884
- tempPrefix: "gji-new"
14885
- },
14886
- {
14887
- bypassOption: "--help",
14888
- commandName: "pr",
14889
- envVar: "GJI_PR_OUTPUT_FILE",
14890
- names: ["pr"],
14891
- tempPrefix: "gji-pr"
14892
- },
14893
- {
14894
- bypassOption: "--print",
14895
- commandName: "back",
14896
- envVar: "GJI_BACK_OUTPUT_FILE",
14897
- names: ["back"],
14898
- tempPrefix: "gji-back"
14899
- },
14900
- {
14901
- bypassOption: "--print",
14902
- commandName: "go",
14903
- envVar: "GJI_GO_OUTPUT_FILE",
14904
- names: ["go"],
14905
- tempPrefix: "gji-go"
14906
- },
14907
- {
14908
- bypassOption: "--print",
14909
- commandName: "root",
14910
- envVar: "GJI_ROOT_OUTPUT_FILE",
14911
- names: ["root"],
14912
- tempPrefix: "gji-root"
14913
- },
14914
- {
14915
- bypassOption: "--help",
14916
- commandName: "remove",
14917
- envVar: "GJI_REMOVE_OUTPUT_FILE",
14918
- names: ["remove", "rm"],
14919
- tempPrefix: "gji-remove"
14920
- }
14921
- ];
14922
- async function runInitCommand(options) {
14923
- const shell = resolveSupportedShell(options.shell, process.env.SHELL);
14924
- const home = options.home ?? homedir4();
14925
- if (!shell) {
14926
- options.stderr?.(
14927
- "Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n"
14928
- );
14929
- return 1;
14930
- }
14931
- const script = renderShellIntegration(shell);
14932
- if (!options.write) {
14933
- options.stdout(script);
14934
- return 0;
14935
- }
14936
- const rcPath = resolveShellConfigPath(shell, home);
14937
- await mkdir3(dirname4(rcPath), { recursive: true });
14938
- const current = await readExistingConfig(rcPath);
14939
- const next = upsertShellIntegration(current, script);
14940
- await writeFile4(rcPath, next, "utf8");
14941
- options.stdout(`${rcPath}
14942
- `);
14943
- const { config: globalConfig } = await loadGlobalConfig(home);
14944
- const alreadyConfigured = "shellIntegration" in globalConfig || "installSaveTarget" in globalConfig;
14945
- const hasCustomPrompt = options.promptForSetup !== void 0;
14946
- const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
14947
- if (!alreadyConfigured && canPrompt) {
14948
- const prompt = options.promptForSetup ?? defaultPromptForSetup;
14949
- const result = await prompt();
14950
- if (result) {
14951
- await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
14952
- await saveWizardConfig(result, options.cwd, home);
14951
+ // src/install-prompt.ts
14952
+ import { spawn as spawn4 } from "node:child_process";
14953
+
14954
+ // src/package-manager.ts
14955
+ import { access as access2, readdir } from "node:fs/promises";
14956
+ import { join as join5 } from "node:path";
14957
+ var ENTRIES = [
14958
+ // JavaScript / TypeScript
14959
+ { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
14960
+ { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
14961
+ { name: "bun", signals: ["bun.lockb"], command: "bun install" },
14962
+ { name: "npm", signals: ["package-lock.json"], command: "npm install" },
14963
+ { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
14964
+ // Python
14965
+ { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
14966
+ { name: "uv", signals: ["uv.lock"], command: "uv sync" },
14967
+ { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14968
+ { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
14969
+ { name: "conda-lock", signals: ["conda-lock.yml"], command: "conda-lock install" },
14970
+ { name: "conda", signals: ["environment.yml"], command: "conda env update --file environment.yml" },
14971
+ // R
14972
+ { name: "renv", signals: ["renv.lock"], command: "Rscript -e 'renv::restore()'" },
14973
+ // Rust
14974
+ { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
14975
+ // Go
14976
+ { name: "go", signals: ["go.sum"], command: "go mod download" },
14977
+ // Ruby
14978
+ { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
14979
+ // PHP
14980
+ { name: "composer", signals: ["composer.lock"], command: "composer install" },
14981
+ // Elixir / Erlang
14982
+ { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
14983
+ { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
14984
+ // Dart / Flutter
14985
+ { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
14986
+ // Java / Kotlin / Scala
14987
+ { name: "maven", signals: ["pom.xml"], command: "mvn install" },
14988
+ { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
14989
+ { name: "gradle", signals: ["build.gradle", "build.gradle.kts"], command: "gradle build" },
14990
+ { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
14991
+ // .NET (C# / F# / VB)
14992
+ { name: "dotnet", signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"], command: "dotnet restore", glob: true },
14993
+ // Swift
14994
+ { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
14995
+ // Haskell
14996
+ { name: "stack", signals: ["stack.yaml"], command: "stack build" },
14997
+ { name: "cabal", signals: ["cabal.project"], command: "cabal install --only-dependencies" },
14998
+ { name: "cabal", signals: ["*.cabal"], command: "cabal install --only-dependencies", glob: true },
14999
+ // Clojure
15000
+ { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
15001
+ { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
15002
+ // OCaml
15003
+ { name: "dune", signals: ["dune-project"], command: "dune build" },
15004
+ // Julia
15005
+ { name: "julia", signals: ["Manifest.toml"], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
15006
+ // Nim
15007
+ { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
15008
+ // Crystal
15009
+ { name: "shards", signals: ["shard.yml"], command: "shards install" },
15010
+ // Perl
15011
+ { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
15012
+ // Zig
15013
+ { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
15014
+ // C / C++
15015
+ { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
15016
+ { name: "conan", signals: ["conanfile.py", "conanfile.txt"], command: "conan install ." },
15017
+ // Nix
15018
+ { name: "nix", signals: ["flake.nix"], command: "nix develop" },
15019
+ { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
15020
+ // Terraform / OpenTofu
15021
+ { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
15022
+ ];
15023
+ async function detectPackageManager(repoRoot) {
15024
+ for (const entry of ENTRIES) {
15025
+ const matched = entry.glob ? await matchesGlob(repoRoot, entry.signals) : await matchesExact(repoRoot, entry.signals);
15026
+ if (matched) {
15027
+ return { name: entry.name, installCommand: entry.command };
14953
15028
  }
14954
15029
  }
14955
- await updateGlobalConfigKey("shellIntegration", true, home);
14956
- return 0;
15030
+ return null;
14957
15031
  }
14958
- function renderShellIntegration(shell) {
14959
- const commandBlocks = SHELL_WRAPPED_COMMANDS.map(
14960
- (command) => shell === "fish" ? renderFishWrapper(command) : renderPosixWrapper(command)
14961
- ).join("\n\n");
14962
- switch (shell) {
14963
- case "fish":
14964
- return `${START_MARKER}
14965
- function gji --wraps gji --description 'gji shell integration'
14966
- ${indentBlock(commandBlocks, 4)}
14967
-
14968
- command gji $argv
14969
- end
14970
- ${END_MARKER}
14971
- `;
14972
- case "bash":
14973
- case "zsh":
14974
- return `${START_MARKER}
14975
- gji() {
14976
- ${indentBlock(commandBlocks, 2)}
14977
-
14978
- command gji "$@"
15032
+ async function matchesExact(repoRoot, signals) {
15033
+ for (const signal of signals) {
15034
+ try {
15035
+ await access2(join5(repoRoot, signal));
15036
+ return true;
15037
+ } catch {
15038
+ }
15039
+ }
15040
+ return false;
14979
15041
  }
14980
- ${END_MARKER}
14981
- `;
15042
+ async function matchesGlob(repoRoot, patterns) {
15043
+ let files;
15044
+ try {
15045
+ files = await readdir(repoRoot);
15046
+ } catch {
15047
+ return false;
14982
15048
  }
15049
+ const regexes = patterns.map(patternToRegex);
15050
+ return files.some((file) => regexes.some((re) => re.test(file)));
14983
15051
  }
14984
- function upsertShellIntegration(existingConfig, script) {
14985
- const trimmedScript = script.trimEnd();
14986
- const blockPattern = new RegExp(
14987
- `${escapeForRegExp(START_MARKER)}[\\s\\S]*?${escapeForRegExp(END_MARKER)}\\n?`,
14988
- "m"
14989
- );
14990
- if (blockPattern.test(existingConfig)) {
14991
- return ensureTrailingNewline(
14992
- existingConfig.replace(blockPattern, `${trimmedScript}
14993
- `)
14994
- );
15052
+ function patternToRegex(pattern) {
15053
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
15054
+ return new RegExp(`^${escaped}$`);
15055
+ }
15056
+
15057
+ // src/install-prompt.ts
15058
+ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
15059
+ if (isHeadless() || nonInteractive) {
15060
+ return;
14995
15061
  }
14996
- const prefix = existingConfig.trimEnd();
14997
- if (prefix.length === 0) {
14998
- return ensureTrailingNewline(trimmedScript);
15062
+ const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15063
+ if (isConfiguredHookCommand(hooks?.afterCreate)) {
15064
+ return;
14999
15065
  }
15000
- return ensureTrailingNewline(`${prefix}
15001
-
15002
- ${trimmedScript}`);
15003
- }
15004
- async function saveWizardConfig(result, cwd, home) {
15005
- const values = {};
15006
- if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
15007
- if (result.worktreePath) values.worktreePath = result.worktreePath;
15008
- const hooks = {};
15009
- if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
15010
- if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
15011
- if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
15012
- if (Object.keys(hooks).length > 0) values.hooks = hooks;
15013
- if (Object.keys(values).length === 0) return;
15014
- if (result.installSaveTarget === "local") {
15015
- const loaded = await loadConfig(cwd);
15016
- await saveLocalConfig(cwd, { ...loaded.config, ...values });
15017
- } else {
15018
- const { config: existing } = await loadGlobalConfig(home);
15019
- await saveGlobalConfig({ ...existing, ...values }, home);
15066
+ if (config.skipInstallPrompt === true) {
15067
+ return;
15020
15068
  }
15021
- }
15022
- function resolveShellConfigPath(shell, home) {
15023
- switch (shell) {
15024
- case "bash":
15025
- return join4(home, ".bashrc");
15026
- case "fish":
15027
- return join4(home, ".config", "fish", "config.fish");
15028
- case "zsh":
15029
- return join4(home, ".zshrc");
15069
+ const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
15070
+ const pm = await detect(worktreePath);
15071
+ if (!pm) {
15072
+ return;
15030
15073
  }
15031
- }
15032
- async function readExistingConfig(path9) {
15033
- try {
15034
- return await readFile3(path9, "utf8");
15035
- } catch (error) {
15036
- if (isMissingFileError2(error)) {
15037
- return "";
15074
+ const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
15075
+ const choice = await prompt(pm);
15076
+ if (!choice || choice === "no") {
15077
+ return;
15078
+ }
15079
+ if (choice === "yes" || choice === "always") {
15080
+ const runner = dependencies.runInstallCommand ?? defaultRunInstallCommand;
15081
+ try {
15082
+ await runner(pm.installCommand, worktreePath, stderr);
15083
+ } catch (error) {
15084
+ stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15085
+ `);
15086
+ }
15087
+ }
15088
+ const saveGlobal = config.installSaveTarget === "global";
15089
+ const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
15090
+ const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
15091
+ if (choice === "always") {
15092
+ try {
15093
+ if (saveGlobal) {
15094
+ const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15095
+ await writeGlobalKey(repoRoot, "hooks", { ...existingHooks, afterCreate: pm.installCommand });
15096
+ } else {
15097
+ const { config: localConfig } = await loadConfig(repoRoot);
15098
+ const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15099
+ await writeKey(repoRoot, "hooks", { ...existingLocalHooks, afterCreate: pm.installCommand });
15100
+ }
15101
+ } catch (error) {
15102
+ stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15103
+ `);
15104
+ }
15105
+ }
15106
+ if (choice === "never") {
15107
+ try {
15108
+ if (saveGlobal) {
15109
+ await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
15110
+ } else {
15111
+ await writeKey(repoRoot, "skipInstallPrompt", true);
15112
+ }
15113
+ } catch (error) {
15114
+ stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15115
+ `);
15038
15116
  }
15039
- throw error;
15040
15117
  }
15041
15118
  }
15042
- function ensureTrailingNewline(value) {
15043
- return value.endsWith("\n") ? value : `${value}
15044
- `;
15119
+ async function defaultRunInstallCommand(command, cwd, stderr) {
15120
+ await new Promise((resolve5, reject) => {
15121
+ const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15122
+ child.stderr.on("data", (chunk) => {
15123
+ stderr(chunk.toString());
15124
+ });
15125
+ child.on("close", (code) => {
15126
+ if (code !== 0) {
15127
+ reject(new Error(`exited with code ${code}`));
15128
+ } else {
15129
+ resolve5();
15130
+ }
15131
+ });
15132
+ child.on("error", (err) => {
15133
+ reject(err);
15134
+ });
15135
+ });
15045
15136
  }
15046
- function escapeForRegExp(value) {
15047
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15137
+ async function defaultWriteConfigKey(root, key, value) {
15138
+ await updateLocalConfigKey(root, key, value);
15048
15139
  }
15049
- function isMissingFileError2(error) {
15050
- return error instanceof Error && "code" in error && error.code === "ENOENT";
15140
+ async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
15141
+ await updateGlobalRepoConfigKey(repoRoot, key, value);
15051
15142
  }
15052
- function renderFishWrapper(command) {
15053
- const tests = command.names.map((name) => `test $argv[1] = ${name}`).join("; or ");
15054
- return `if test (count $argv) -gt 0; and ${tests}
15055
- set -e argv[1]
15056
- if test (count $argv) -gt 0; and test $argv[1] = ${command.bypassOption}
15057
- command gji ${command.commandName} $argv
15058
- return $status
15059
- end
15060
-
15061
- set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
15062
- or return 1
15063
- env ${command.envVar}=$output_file command gji ${command.commandName} $argv
15064
- or begin
15065
- set -l status_code $status
15066
- rm -f $output_file
15067
- return $status_code
15068
- end
15069
- set -l target (cat $output_file)
15070
- rm -f $output_file
15071
- cd $target
15072
- return $status
15073
- end`;
15074
- }
15075
- function renderPosixWrapper(command) {
15076
- const tests = command.names.map((name) => `[ "$1" = "${name}" ]`).join(" || ");
15077
- return `if ${tests}; then
15078
- shift
15079
- if [ "\${1:-}" = "${command.bypassOption}" ]; then
15080
- command gji ${command.commandName} "$@"
15081
- return $?
15082
- fi
15083
-
15084
- local target
15085
- local output_file
15086
- output_file="$(mktemp -t ${command.tempPrefix}.XXXXXX)" || return 1
15087
- ${command.envVar}="$output_file" command gji ${command.commandName} "$@" || { local exit_code=$?; rm -f "$output_file"; return $exit_code; }
15088
- target="$(cat "$output_file")"
15089
- rm -f "$output_file"
15090
- cd "$target" || return $?
15091
- return 0
15092
- fi`;
15093
- }
15094
- function indentBlock(value, spaces) {
15095
- const prefix = " ".repeat(spaces);
15096
- return value.split("\n").map((line) => line.length === 0 ? "" : `${prefix}${line}`).join("\n");
15143
+ async function loadExistingGlobalRepoHooks(repoRoot) {
15144
+ const { config: globalConfig } = await loadGlobalConfig();
15145
+ const repos = isPlainObject2(globalConfig.repos) ? globalConfig.repos : {};
15146
+ const perRepo = isPlainObject2(repos[repoRoot]) ? repos[repoRoot] : {};
15147
+ return isPlainObject2(perRepo.hooks) ? perRepo.hooks : {};
15097
15148
  }
15098
- async function defaultPromptForSetup() {
15099
- Ie("gji setup");
15100
- const installSaveTarget = await ve({
15101
- message: "Where should preferences be saved?",
15149
+ async function defaultPromptForInstallChoice(pm) {
15150
+ const choice = await ve({
15151
+ message: `Run \`${pm.installCommand}\` in the new worktree?`,
15102
15152
  options: [
15103
- { value: "global", label: "~/.config/gji/config.json", hint: "personal \u2014 never committed" },
15104
- { value: "local", label: ".gji.json", hint: "repo \u2014 committed with the project" }
15153
+ { value: "yes", label: "Yes", hint: "run once" },
15154
+ { value: "no", label: "No", hint: "skip this time" },
15155
+ { value: "always", label: "Always", hint: "save as afterCreate hook" },
15156
+ { value: "never", label: "Never", hint: "disable this prompt for this repo" }
15105
15157
  ]
15106
15158
  });
15107
- if (pD(installSaveTarget)) {
15108
- Se("Setup skipped.");
15109
- return null;
15110
- }
15111
- const branchPrefix = await he({
15112
- message: "Default branch prefix?",
15113
- placeholder: "e.g. feat/ or fix/ \u2014 leave blank to skip"
15114
- });
15115
- if (pD(branchPrefix)) {
15116
- Se("Setup skipped.");
15117
- return null;
15118
- }
15119
- const worktreePath = await he({
15120
- message: "Worktree base path?",
15121
- placeholder: "leave blank to use the default path"
15122
- });
15123
- if (pD(worktreePath)) {
15124
- Se("Setup skipped.");
15125
- return null;
15126
- }
15127
- const afterCreate = await he({
15128
- message: "afterCreate hook \u2014 run after creating a worktree?",
15129
- placeholder: "e.g. pnpm install \u2014 leave blank to skip"
15130
- });
15131
- if (pD(afterCreate)) {
15132
- Se("Setup skipped.");
15133
- return null;
15134
- }
15135
- const afterEnter = await he({
15136
- message: "afterEnter hook \u2014 run after entering a worktree?",
15137
- placeholder: "e.g. nvm use \u2014 leave blank to skip"
15138
- });
15139
- if (pD(afterEnter)) {
15140
- Se("Setup skipped.");
15141
- return null;
15142
- }
15143
- const beforeRemove = await he({
15144
- message: "beforeRemove hook \u2014 run before removing a worktree?",
15145
- placeholder: "leave blank to skip"
15146
- });
15147
- if (pD(beforeRemove)) {
15148
- Se("Setup skipped.");
15159
+ if (pD(choice)) {
15149
15160
  return null;
15150
15161
  }
15151
- Se("Setup complete!");
15152
- const hooks = {};
15153
- if (afterCreate) hooks.afterCreate = afterCreate;
15154
- if (afterEnter) hooks.afterEnter = afterEnter;
15155
- if (beforeRemove) hooks.beforeRemove = beforeRemove;
15156
- return {
15157
- branchPrefix: branchPrefix || void 0,
15158
- hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
15159
- installSaveTarget,
15160
- worktreePath: worktreePath || void 0
15161
- };
15162
- }
15163
-
15164
- // src/paths.ts
15165
- function comparePaths(left, right) {
15166
- if (left < right) {
15167
- return -1;
15168
- }
15169
- if (left > right) {
15170
- return 1;
15171
- }
15172
- return 0;
15162
+ return choice;
15173
15163
  }
15174
-
15175
- // src/ls.ts
15176
- async function runLsCommand(options) {
15177
- const worktrees = sortWorktrees(await listWorktrees(options.cwd));
15178
- if (options.compact) {
15179
- if (options.json) {
15180
- options.stdout(`${JSON.stringify(worktrees, null, 2)}
15181
- `);
15182
- return 0;
15183
- }
15184
- options.stdout(`${formatWorktreeTable(worktrees)}
15185
- `);
15186
- return 0;
15187
- }
15188
- const infos = await readWorktreeInfos(worktrees);
15189
- if (options.json) {
15190
- options.stdout(`${JSON.stringify(infos, null, 2)}
15191
- `);
15192
- return 0;
15193
- }
15194
- options.stdout(`${formatDetailedWorktreeTable(infos)}
15195
- `);
15196
- return 0;
15164
+ function isPlainObject2(value) {
15165
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15197
15166
  }
15198
- function formatDetailedWorktreeTable(worktrees) {
15199
- const rows = worktrees.map((worktree) => ({
15200
- branch: worktree.branch ?? "(detached)",
15201
- isCurrent: worktree.isCurrent,
15202
- lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
15203
- path: worktree.path,
15204
- status: worktree.status,
15205
- upstream: formatUpstreamState(worktree.upstream)
15206
- }));
15207
- const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
15208
- const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
15209
- const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
15210
- const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
15211
- const lines = [
15212
- " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
15213
- ];
15214
- for (const row of rows) {
15215
- lines.push(
15216
- `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.status.padEnd(statusWidth, " ")} ${row.upstream.padEnd(upstreamWidth, " ")} ${row.lastCommit.padEnd(lastCommitWidth, " ")} ` + row.path
15217
- );
15218
- }
15219
- return lines.join("\n");
15167
+ function isConfiguredHookCommand(value) {
15168
+ if (typeof value === "string") return value.length > 0;
15169
+ return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15220
15170
  }
15221
- function formatWorktreeTable(worktrees) {
15222
- const rows = worktrees.map((worktree) => ({
15223
- branch: worktree.branch ?? "(detached)",
15224
- isCurrent: worktree.isCurrent,
15225
- path: worktree.path
15226
- }));
15227
- const branchWidth = Math.max(
15228
- "BRANCH".length,
15229
- ...rows.map((row) => row.branch.length)
15230
- );
15231
- const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
15232
- for (const row of rows) {
15233
- lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
15171
+
15172
+ // src/conflict.ts
15173
+ import { access as access3 } from "node:fs/promises";
15174
+ import { constants } from "node:fs";
15175
+ async function pathExists(path9) {
15176
+ try {
15177
+ await access3(path9, constants.F_OK);
15178
+ return true;
15179
+ } catch {
15180
+ return false;
15234
15181
  }
15235
- return lines.join("\n");
15236
15182
  }
15237
- function sortWorktrees(worktrees) {
15238
- return [...worktrees].sort((left, right) => {
15239
- if (left.isCurrent && !right.isCurrent) return -1;
15240
- if (!left.isCurrent && right.isCurrent) return 1;
15241
- return comparePaths(left.path, right.path);
15183
+ async function promptForPathConflict(path9) {
15184
+ const choice = await ve({
15185
+ message: `Target path already exists: ${path9}`,
15186
+ options: [
15187
+ { value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
15188
+ { value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
15189
+ ]
15242
15190
  });
15191
+ if (pD(choice)) {
15192
+ return "abort";
15193
+ }
15194
+ return choice;
15243
15195
  }
15244
15196
 
15245
15197
  // src/new.ts
15246
- import { mkdir as mkdir5 } from "node:fs/promises";
15247
- import { basename as basename4, dirname as dirname6 } from "node:path";
15248
- import { execFile as execFile3 } from "node:child_process";
15249
- import { promisify as promisify4 } from "node:util";
15250
-
15251
- // src/file-sync.ts
15252
- import { copyFile, mkdir as mkdir4, stat } from "node:fs/promises";
15253
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join5, normalize } from "node:path";
15254
- async function syncFiles(mainRoot, targetPath, patterns) {
15255
- for (const pattern of patterns) {
15256
- if (isAbsolute2(pattern)) {
15257
- throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
15198
+ var execFileAsync3 = promisify4(execFile3);
15199
+ var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
15200
+ function createNewCommand(dependencies = {}) {
15201
+ const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
15202
+ const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
15203
+ const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
15204
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15205
+ return async function runNewCommand2(options) {
15206
+ const repository = await detectRepository(options.cwd);
15207
+ const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15208
+ const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15209
+ if (options.editor && !options.open) {
15210
+ options.stderr("gji new: --editor has no effect without --open\n");
15258
15211
  }
15259
- const normalized = normalize(pattern);
15260
- if (normalized.startsWith("..")) {
15261
- throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
15212
+ if (!options.detached && !options.branch && (options.json || isHeadless())) {
15213
+ const message = "branch argument is required";
15214
+ if (options.json) {
15215
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15216
+ `);
15217
+ } else {
15218
+ options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15219
+ `);
15220
+ }
15221
+ return 1;
15262
15222
  }
15263
- const sourcePath = join5(mainRoot, normalized);
15264
- const destPath = join5(targetPath, normalized);
15265
- const sourceExists = await fileExists(sourcePath);
15266
- if (!sourceExists) {
15267
- continue;
15223
+ const rawBranch = options.detached ? options.branch ?? createBranchPlaceholder() : options.branch ?? await promptForBranch(createBranchPlaceholder());
15224
+ if (!rawBranch) {
15225
+ if (options.json) {
15226
+ options.stderr(`${JSON.stringify({ error: "Aborted" }, null, 2)}
15227
+ `);
15228
+ } else {
15229
+ options.stderr("Aborted\n");
15230
+ }
15231
+ return 1;
15268
15232
  }
15269
- const destExists = await fileExists(destPath);
15270
- if (destExists) {
15271
- continue;
15233
+ if (!options.detached) {
15234
+ const branchError = validateBranchName(rawBranch);
15235
+ if (branchError) {
15236
+ if (options.json) {
15237
+ options.stderr(`${JSON.stringify({ error: branchError }, null, 2)}
15238
+ `);
15239
+ } else {
15240
+ options.stderr(`gji new: ${branchError}
15241
+ `);
15242
+ }
15243
+ return 1;
15244
+ }
15272
15245
  }
15273
- await mkdir4(dirname5(destPath), { recursive: true });
15274
- await copyFile(sourcePath, destPath);
15275
- }
15276
- }
15277
- async function fileExists(path9) {
15278
- try {
15279
- await stat(path9);
15280
- return true;
15281
- } catch (error) {
15282
- if (isNotFoundError(error)) {
15283
- return false;
15246
+ const rawBasePath = resolveConfigString(config, "worktreePath");
15247
+ const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
15248
+ const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
15249
+ const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath) : resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
15250
+ if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
15251
+ if (options.force) {
15252
+ if (!options.dryRun) {
15253
+ try {
15254
+ await execFileAsync3("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
15255
+ } catch (err) {
15256
+ if (!isNotRegisteredWorktreeError(err)) {
15257
+ const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
15258
+ if (options.json) {
15259
+ options.stderr(`${JSON.stringify({ warning: msg }, null, 2)}
15260
+ `);
15261
+ } else {
15262
+ options.stderr(`Warning: ${msg}
15263
+ `);
15264
+ }
15265
+ }
15266
+ }
15267
+ if (!options.detached) {
15268
+ try {
15269
+ await execFileAsync3("git", ["branch", "-D", worktreeName], { cwd: repository.repoRoot });
15270
+ } catch {
15271
+ }
15272
+ }
15273
+ }
15274
+ } else if (options.json || isHeadless()) {
15275
+ const message = `target worktree path already exists: ${worktreePath}`;
15276
+ if (options.json) {
15277
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15278
+ `);
15279
+ } else {
15280
+ options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15281
+ `);
15282
+ options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15283
+ `);
15284
+ options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15285
+ `);
15286
+ }
15287
+ return 1;
15288
+ } else {
15289
+ const choice = await prompt(worktreePath);
15290
+ if (choice === "reuse") {
15291
+ appendHistory(worktreePath, worktreeName).catch(() => void 0);
15292
+ await writeOutput(worktreePath, options.stdout);
15293
+ return 0;
15294
+ }
15295
+ options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
15296
+ `);
15297
+ return 1;
15298
+ }
15284
15299
  }
15285
- throw error;
15286
- }
15300
+ if (options.dryRun) {
15301
+ if (options.json) {
15302
+ options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15303
+ `);
15304
+ } else {
15305
+ const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
15306
+ const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
15307
+ options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15308
+ `);
15309
+ }
15310
+ return 0;
15311
+ }
15312
+ await mkdir4(dirname5(worktreePath), { recursive: true });
15313
+ const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15314
+ await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15315
+ const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15316
+ for (const pattern of syncPatterns) {
15317
+ try {
15318
+ await syncFiles(repository.repoRoot, worktreePath, [pattern]);
15319
+ } catch (error) {
15320
+ options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15321
+ `);
15322
+ }
15323
+ }
15324
+ await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
15325
+ const hooks = extractHooks(config);
15326
+ await runHook(
15327
+ hooks.afterCreate,
15328
+ worktreePath,
15329
+ { branch: worktreeName, path: worktreePath, repo: basename3(repository.repoRoot) },
15330
+ options.stderr
15331
+ );
15332
+ if (options.json) {
15333
+ options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15334
+ `);
15335
+ } else {
15336
+ await appendHistory(worktreePath, worktreeName);
15337
+ await writeOutput(worktreePath, options.stdout);
15338
+ }
15339
+ if (options.open) {
15340
+ const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
15341
+ await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
15342
+ }
15343
+ return 0;
15344
+ };
15287
15345
  }
15288
- function isNotFoundError(error) {
15289
- return error instanceof Error && "code" in error && error.code === "ENOENT";
15346
+ var runNewCommand = createNewCommand();
15347
+ function generateBranchPlaceholder(random = Math.random) {
15348
+ const roots = [
15349
+ "socrates",
15350
+ "prometheus",
15351
+ "beethoven",
15352
+ "ada",
15353
+ "turing",
15354
+ "hypatia",
15355
+ "tesla",
15356
+ "curie",
15357
+ "diogenes",
15358
+ "plato",
15359
+ "hephaestus",
15360
+ "athena",
15361
+ "archimedes",
15362
+ "euclid",
15363
+ "heraclitus",
15364
+ "galileo",
15365
+ "newton",
15366
+ "lovelace",
15367
+ "nietzsche",
15368
+ "kafka"
15369
+ ];
15370
+ const antics = [
15371
+ "borrowed-a-bike",
15372
+ "brought-snacks",
15373
+ "missed-the-bus",
15374
+ "lost-the-keys",
15375
+ "spilled-the-coffee",
15376
+ "forgot-the-umbrella",
15377
+ "walked-the-dog",
15378
+ "missed-the-train",
15379
+ "wrote-a-poem",
15380
+ "burned-the-toast",
15381
+ "fed-the-pigeons",
15382
+ "watered-the-plants",
15383
+ "washed-the-dishes",
15384
+ "folded-the-laundry",
15385
+ "took-a-nap"
15386
+ ];
15387
+ return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
15290
15388
  }
15291
-
15292
- // src/install-prompt.ts
15293
- import { spawn as spawn3 } from "node:child_process";
15294
-
15295
- // src/package-manager.ts
15296
- import { access as access2, readdir } from "node:fs/promises";
15297
- import { join as join6 } from "node:path";
15298
- var ENTRIES = [
15299
- // JavaScript / TypeScript
15300
- { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
15301
- { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
15302
- { name: "bun", signals: ["bun.lockb"], command: "bun install" },
15303
- { name: "npm", signals: ["package-lock.json"], command: "npm install" },
15304
- { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
15305
- // Python
15306
- { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
15307
- { name: "uv", signals: ["uv.lock"], command: "uv sync" },
15308
- { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
15309
- { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
15310
- { name: "conda-lock", signals: ["conda-lock.yml"], command: "conda-lock install" },
15311
- { name: "conda", signals: ["environment.yml"], command: "conda env update --file environment.yml" },
15312
- // R
15313
- { name: "renv", signals: ["renv.lock"], command: "Rscript -e 'renv::restore()'" },
15314
- // Rust
15315
- { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
15316
- // Go
15317
- { name: "go", signals: ["go.sum"], command: "go mod download" },
15318
- // Ruby
15319
- { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
15320
- // PHP
15321
- { name: "composer", signals: ["composer.lock"], command: "composer install" },
15322
- // Elixir / Erlang
15323
- { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
15324
- { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
15325
- // Dart / Flutter
15326
- { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
15327
- // Java / Kotlin / Scala
15328
- { name: "maven", signals: ["pom.xml"], command: "mvn install" },
15329
- { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
15330
- { name: "gradle", signals: ["build.gradle", "build.gradle.kts"], command: "gradle build" },
15331
- { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
15332
- // .NET (C# / F# / VB)
15333
- { name: "dotnet", signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"], command: "dotnet restore", glob: true },
15334
- // Swift
15335
- { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
15336
- // Haskell
15337
- { name: "stack", signals: ["stack.yaml"], command: "stack build" },
15338
- { name: "cabal", signals: ["cabal.project"], command: "cabal install --only-dependencies" },
15339
- { name: "cabal", signals: ["*.cabal"], command: "cabal install --only-dependencies", glob: true },
15340
- // Clojure
15341
- { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
15342
- { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
15343
- // OCaml
15344
- { name: "dune", signals: ["dune-project"], command: "dune build" },
15345
- // Julia
15346
- { name: "julia", signals: ["Manifest.toml"], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
15347
- // Nim
15348
- { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
15349
- // Crystal
15350
- { name: "shards", signals: ["shard.yml"], command: "shards install" },
15351
- // Perl
15352
- { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
15353
- // Zig
15354
- { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
15355
- // C / C++
15356
- { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
15357
- { name: "conan", signals: ["conanfile.py", "conanfile.txt"], command: "conan install ." },
15358
- // Nix
15359
- { name: "nix", signals: ["flake.nix"], command: "nix develop" },
15360
- { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
15361
- // Terraform / OpenTofu
15362
- { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
15363
- ];
15364
- async function detectPackageManager(repoRoot) {
15365
- for (const entry of ENTRIES) {
15366
- const matched = entry.glob ? await matchesGlob(repoRoot, entry.signals) : await matchesExact(repoRoot, entry.signals);
15367
- if (matched) {
15368
- return { name: entry.name, installCommand: entry.command };
15389
+ function applyConfiguredBranchPrefix(branch, branchPrefix) {
15390
+ if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
15391
+ return branch;
15392
+ }
15393
+ if (branch.startsWith(branchPrefix)) {
15394
+ return branch;
15395
+ }
15396
+ return `${branchPrefix}${branch}`;
15397
+ }
15398
+ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15399
+ let attempt = 1;
15400
+ while (true) {
15401
+ const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15402
+ const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15403
+ if (!await pathExists(candidatePath)) {
15404
+ return candidatePath;
15369
15405
  }
15406
+ attempt += 1;
15370
15407
  }
15371
- return null;
15372
15408
  }
15373
- async function matchesExact(repoRoot, signals) {
15374
- for (const signal of signals) {
15375
- try {
15376
- await access2(join6(repoRoot, signal));
15377
- return true;
15378
- } catch {
15409
+ async function defaultPromptForBranch(placeholder) {
15410
+ const choice = await he({
15411
+ defaultValue: placeholder,
15412
+ message: "Name the new branch",
15413
+ placeholder,
15414
+ validate: (value) => {
15415
+ const trimmed = value.trim();
15416
+ return validateBranchName(trimmed) ?? void 0;
15379
15417
  }
15418
+ });
15419
+ if (pD(choice)) {
15420
+ return null;
15380
15421
  }
15381
- return false;
15422
+ return choice.trim();
15382
15423
  }
15383
- async function matchesGlob(repoRoot, patterns) {
15384
- let files;
15424
+ function pickRandom(values, random) {
15425
+ const index = Math.floor(random() * values.length);
15426
+ return values[Math.min(index, values.length - 1)];
15427
+ }
15428
+ async function localBranchExists(repoRoot, branchName) {
15385
15429
  try {
15386
- files = await readdir(repoRoot);
15430
+ await execFileAsync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
15431
+ return true;
15387
15432
  } catch {
15388
15433
  return false;
15389
15434
  }
15390
- const regexes = patterns.map(patternToRegex);
15391
- return files.some((file) => regexes.some((re) => re.test(file)));
15392
15435
  }
15393
- function patternToRegex(pattern) {
15394
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
15395
- return new RegExp(`^${escaped}$`);
15436
+ async function writeOutput(worktreePath, stdout) {
15437
+ await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
15396
15438
  }
15397
-
15398
- // src/install-prompt.ts
15399
- async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
15400
- if (isHeadless() || nonInteractive) {
15439
+ function isNotRegisteredWorktreeError(error) {
15440
+ const stderr = hasExecStderr(error) ? error.stderr : String(error);
15441
+ return stderr.includes("is not a working tree") || stderr.includes("not a linked working tree");
15442
+ }
15443
+ function hasExecStderr(error) {
15444
+ return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
15445
+ }
15446
+ function toExecMessage(error) {
15447
+ return hasExecStderr(error) ? error.stderr.trim() : String(error);
15448
+ }
15449
+ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15450
+ if (!editorCli) {
15451
+ stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
15401
15452
  return;
15402
15453
  }
15403
- const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15404
- if (typeof hooks?.afterCreate === "string" && hooks.afterCreate.length > 0) {
15405
- return;
15454
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15455
+ const args = [];
15456
+ if (editorDef?.newWindowFlag) {
15457
+ args.push(editorDef.newWindowFlag);
15406
15458
  }
15407
- if (config.skipInstallPrompt === true) {
15408
- return;
15459
+ args.push(worktreePath);
15460
+ try {
15461
+ await spawnFn(editorCli, args);
15462
+ } catch (error) {
15463
+ const message = error instanceof Error ? error.message : String(error);
15464
+ stderr(`gji new: failed to open editor: ${message}
15465
+ `);
15409
15466
  }
15410
- const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
15411
- const pm = await detect(worktreePath);
15412
- if (!pm) {
15413
- return;
15467
+ }
15468
+
15469
+ // src/repo-registry.ts
15470
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
15471
+ import { homedir as homedir4 } from "node:os";
15472
+ import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
15473
+ var REGISTRY_FILE_NAME = "repos.json";
15474
+ var MAX_REGISTRY_ENTRIES = 100;
15475
+ function REGISTRY_FILE_PATH(home = homedir4()) {
15476
+ const configDir = process.env.GJI_CONFIG_DIR;
15477
+ if (configDir) {
15478
+ return join6(resolve4(configDir), REGISTRY_FILE_NAME);
15414
15479
  }
15415
- const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
15416
- const choice = await prompt(pm);
15417
- if (!choice || choice === "no") {
15418
- return;
15480
+ return join6(home, GLOBAL_CONFIG_DIRECTORY, REGISTRY_FILE_NAME);
15481
+ }
15482
+ async function loadRegistry(home = homedir4()) {
15483
+ const path9 = REGISTRY_FILE_PATH(home);
15484
+ try {
15485
+ const raw = await readFile3(path9, "utf8");
15486
+ const parsed = JSON.parse(raw);
15487
+ if (!Array.isArray(parsed)) return [];
15488
+ return parsed.filter(isRegistryEntry);
15489
+ } catch {
15490
+ return [];
15419
15491
  }
15420
- if (choice === "yes" || choice === "always") {
15421
- const runner = dependencies.runInstallCommand ?? defaultRunInstallCommand;
15422
- try {
15423
- await runner(pm.installCommand, worktreePath, stderr);
15424
- } catch (error) {
15425
- stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15426
- `);
15492
+ }
15493
+ async function registerRepo(repoPath, home = homedir4()) {
15494
+ const registryPath = REGISTRY_FILE_PATH(home);
15495
+ const existing = await loadRegistry(home);
15496
+ if (existing.length > 0 && existing[0].path === repoPath) return;
15497
+ const entry = {
15498
+ lastUsed: Date.now(),
15499
+ name: basename4(repoPath),
15500
+ path: repoPath
15501
+ };
15502
+ const filtered = existing.filter((e2) => e2.path !== repoPath);
15503
+ const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
15504
+ await mkdir5(dirname6(registryPath), { recursive: true });
15505
+ await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
15506
+ `, "utf8");
15507
+ }
15508
+ function isRegistryEntry(value) {
15509
+ return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "name" in value && typeof value.name === "string" && "lastUsed" in value && typeof value.lastUsed === "number";
15510
+ }
15511
+
15512
+ // src/warp.ts
15513
+ var WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
15514
+ async function runWarpCommand(options) {
15515
+ if (options.newWorktree) {
15516
+ const registry = await loadRegistry();
15517
+ if (registry.length === 0) {
15518
+ options.stderr(
15519
+ "gji warp: no repos registered yet.\nUse any gji command in a repository to register it automatically.\n"
15520
+ );
15521
+ return 1;
15427
15522
  }
15523
+ return runWarpNew(options, registry);
15428
15524
  }
15429
- const saveGlobal = config.installSaveTarget === "global";
15430
- const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
15431
- const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
15432
- if (choice === "always") {
15433
- try {
15434
- if (saveGlobal) {
15435
- const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15436
- await writeGlobalKey(repoRoot, "hooks", { ...existingHooks, afterCreate: pm.installCommand });
15437
- } else {
15438
- const { config: localConfig } = await loadConfig(repoRoot);
15439
- const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15440
- await writeKey(repoRoot, "hooks", { ...existingLocalHooks, afterCreate: pm.installCommand });
15441
- }
15442
- } catch (error) {
15443
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15525
+ return runWarpNavigate(options);
15526
+ }
15527
+ async function runWarpNavigate(options) {
15528
+ if ((isHeadless() || options.json) && !options.branch) {
15529
+ const message = "branch argument is required";
15530
+ if (options.json) {
15531
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15444
15532
  `);
15533
+ } else {
15534
+ options.stderr(
15535
+ "gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15536
+ );
15445
15537
  }
15538
+ return 1;
15446
15539
  }
15447
- if (choice === "never") {
15448
- try {
15449
- if (saveGlobal) {
15450
- await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
15451
- } else {
15452
- await writeKey(repoRoot, "skipInstallPrompt", true);
15453
- }
15454
- } catch (error) {
15455
- stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}
15540
+ const target = await resolveWarpTarget({ ...options, commandName: "gji warp", json: options.json });
15541
+ if (!target) return 1;
15542
+ if (options.json) {
15543
+ options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15456
15544
  `);
15457
- }
15545
+ return 0;
15458
15546
  }
15547
+ appendHistory(target.path, target.branch).catch(() => void 0);
15548
+ await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
15549
+ return 0;
15459
15550
  }
15460
- async function defaultRunInstallCommand(command, cwd, stderr) {
15461
- await new Promise((resolve4, reject) => {
15462
- const child = spawn3(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15463
- child.stderr.on("data", (chunk) => {
15464
- stderr(chunk.toString());
15465
- });
15466
- child.on("close", (code) => {
15467
- if (code !== 0) {
15468
- reject(new Error(`exited with code ${code}`));
15469
- } else {
15470
- resolve4();
15471
- }
15551
+ async function runWarpNew(options, registry) {
15552
+ let targetRepoRoot;
15553
+ if (registry.length === 1) {
15554
+ targetRepoRoot = registry[0].path;
15555
+ } else {
15556
+ if (isHeadless()) {
15557
+ options.stderr(
15558
+ "gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15559
+ );
15560
+ return 1;
15561
+ }
15562
+ const choice = await ve({
15563
+ message: "Create worktree in which repo?",
15564
+ options: registry.map((entry) => ({
15565
+ value: entry.path,
15566
+ label: entry.name,
15567
+ hint: entry.path
15568
+ }))
15472
15569
  });
15473
- child.on("error", (err) => {
15474
- reject(err);
15570
+ if (pD(choice)) {
15571
+ options.stderr("Aborted\n");
15572
+ return 1;
15573
+ }
15574
+ targetRepoRoot = choice;
15575
+ }
15576
+ if (options.json) {
15577
+ return runNewCommand({
15578
+ branch: options.branch,
15579
+ cwd: targetRepoRoot,
15580
+ json: true,
15581
+ stderr: options.stderr,
15582
+ stdout: options.stdout
15475
15583
  });
15584
+ }
15585
+ let capturedPath = "";
15586
+ const captureStdout = (chunk) => {
15587
+ capturedPath = chunk.trim();
15588
+ };
15589
+ const exitCode = await runNewCommand({
15590
+ branch: options.branch,
15591
+ cwd: targetRepoRoot,
15592
+ stderr: options.stderr,
15593
+ stdout: captureStdout
15476
15594
  });
15595
+ if (exitCode !== 0) {
15596
+ return exitCode;
15597
+ }
15598
+ if (!capturedPath) {
15599
+ options.stderr("gji warp: could not determine new worktree path\n");
15600
+ return 1;
15601
+ }
15602
+ await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
15603
+ return 0;
15477
15604
  }
15478
- async function defaultWriteConfigKey(root, key, value) {
15479
- await updateLocalConfigKey(root, key, value);
15480
- }
15481
- async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
15482
- await updateGlobalRepoConfigKey(repoRoot, key, value);
15483
- }
15484
- async function loadExistingGlobalRepoHooks(repoRoot) {
15485
- const { config: globalConfig } = await loadGlobalConfig();
15486
- const repos = isPlainObject2(globalConfig.repos) ? globalConfig.repos : {};
15487
- const perRepo = isPlainObject2(repos[repoRoot]) ? repos[repoRoot] : {};
15488
- return isPlainObject2(perRepo.hooks) ? perRepo.hooks : {};
15605
+ function findByQuery(items, query) {
15606
+ const slashIdx = query.indexOf("/");
15607
+ if (slashIdx !== -1) {
15608
+ const repoQuery = query.slice(0, slashIdx);
15609
+ const branchQuery = query.slice(slashIdx + 1);
15610
+ const match = items.find(
15611
+ (item) => item.repoName === repoQuery && item.worktree.branch === branchQuery
15612
+ );
15613
+ if (match) return match;
15614
+ }
15615
+ return items.find((item) => item.worktree.branch === query) ?? null;
15489
15616
  }
15490
- async function defaultPromptForInstallChoice(pm) {
15491
- const choice = await ve({
15492
- message: `Run \`${pm.installCommand}\` in the new worktree?`,
15493
- options: [
15494
- { value: "yes", label: "Yes", hint: "run once" },
15495
- { value: "no", label: "No", hint: "skip this time" },
15496
- { value: "always", label: "Always", hint: "save as afterCreate hook" },
15497
- { value: "never", label: "Never", hint: "disable this prompt for this repo" }
15498
- ]
15499
- });
15500
- if (pD(choice)) {
15617
+ async function resolveWarpTarget(options) {
15618
+ const cmd = options.commandName ?? "gji";
15619
+ const emitError4 = (message, hint) => {
15620
+ if (options.json) {
15621
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15622
+ `);
15623
+ } else {
15624
+ options.stderr(`${cmd}: ${message}
15625
+ `);
15626
+ if (hint) options.stderr(hint);
15627
+ }
15628
+ };
15629
+ const registry = await loadRegistry();
15630
+ if (registry.length === 0) {
15631
+ emitError4(
15632
+ "not in a git repository and no repos registered yet.",
15633
+ "Use any gji command inside a repository to register it.\n"
15634
+ );
15501
15635
  return null;
15502
15636
  }
15503
- return choice;
15504
- }
15505
- function isPlainObject2(value) {
15506
- return typeof value === "object" && value !== null && !Array.isArray(value);
15507
- }
15508
-
15509
- // src/conflict.ts
15510
- import { access as access3 } from "node:fs/promises";
15511
- import { constants } from "node:fs";
15512
- async function pathExists(path9) {
15513
- try {
15514
- await access3(path9, constants.F_OK);
15515
- return true;
15516
- } catch {
15517
- return false;
15637
+ const results = await Promise.allSettled(
15638
+ registry.map(async (entry) => {
15639
+ const worktrees = await listWorktrees(entry.path);
15640
+ return { repoName: entry.name, worktrees };
15641
+ })
15642
+ );
15643
+ const allItems = [];
15644
+ for (const result of results) {
15645
+ if (result.status === "rejected") continue;
15646
+ const { repoName, worktrees } = result.value;
15647
+ for (const worktree of worktrees) {
15648
+ allItems.push({ repoName, worktree });
15649
+ }
15650
+ }
15651
+ if (allItems.length === 0) {
15652
+ emitError4("no accessible worktrees found in any registered repo.");
15653
+ return null;
15654
+ }
15655
+ if (options.branch) {
15656
+ const match = findByQuery(allItems, options.branch);
15657
+ if (!match) {
15658
+ emitError4(`no worktree found matching: ${options.branch}`);
15659
+ return null;
15660
+ }
15661
+ return { branch: match.worktree.branch, path: match.worktree.path };
15518
15662
  }
15663
+ const path9 = await promptForWarpTarget(allItems);
15664
+ if (!path9) {
15665
+ options.stderr("Aborted\n");
15666
+ return null;
15667
+ }
15668
+ const chosen = allItems.find((item) => item.worktree.path === path9);
15669
+ return { branch: chosen?.worktree.branch ?? null, path: path9 };
15519
15670
  }
15520
- async function promptForPathConflict(path9) {
15671
+ async function promptForWarpTarget(items) {
15672
+ const healthResults = await Promise.allSettled(
15673
+ items.map((item) => readWorktreeHealth(item.worktree.path))
15674
+ );
15521
15675
  const choice = await ve({
15522
- message: `Target path already exists: ${path9}`,
15523
- options: [
15524
- { value: "abort", label: "Abort", hint: "Keep the existing directory untouched" },
15525
- { value: "reuse", label: "Reuse path", hint: "Print the existing path and stop" }
15526
- ]
15676
+ message: "Warp to a worktree",
15677
+ options: items.map((item, i) => {
15678
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15679
+ const upstream = health ? formatHint(item.worktree.branch, health) : null;
15680
+ const label = `${item.repoName} \u203A ${item.worktree.branch ?? "(detached)"}`;
15681
+ const pathHint = item.worktree.isCurrent ? `${item.worktree.path} (current)` : item.worktree.path;
15682
+ const hint = upstream ? `${upstream} \xB7 ${pathHint}` : pathHint;
15683
+ return { hint, label, value: item.worktree.path };
15684
+ })
15527
15685
  });
15528
15686
  if (pD(choice)) {
15529
- return "abort";
15687
+ return null;
15530
15688
  }
15531
15689
  return choice;
15532
15690
  }
15691
+ function formatHint(branch, health) {
15692
+ if (branch === null) return null;
15693
+ if (!health.hasUpstream) return "no upstream";
15694
+ if (health.upstreamGone) return "upstream gone";
15695
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
15696
+ if (health.ahead === 0) return `behind ${health.behind}`;
15697
+ if (health.behind === 0) return `ahead ${health.ahead}`;
15698
+ return `ahead ${health.ahead}, behind ${health.behind}`;
15699
+ }
15533
15700
 
15534
- // src/new.ts
15535
- var execFileAsync3 = promisify4(execFile3);
15536
- var NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
15537
- function createNewCommand(dependencies = {}) {
15538
- const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
15539
- const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
15540
- const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
15541
- return async function runNewCommand2(options) {
15542
- const repository = await detectRepository(options.cwd);
15543
- const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15544
- const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15545
- if (!options.detached && !options.branch && (options.json || isHeadless())) {
15546
- const message = "branch argument is required";
15547
- if (options.json) {
15548
- options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15549
- `);
15550
- } else {
15551
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15552
- `);
15701
+ // src/go.ts
15702
+ var GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
15703
+ function createGoCommand(dependencies = {}) {
15704
+ const prompt = dependencies.promptForWorktree ?? promptForWorktree;
15705
+ return async function runGoCommand2(options) {
15706
+ let worktrees;
15707
+ let repository;
15708
+ try {
15709
+ [worktrees, repository] = await Promise.all([
15710
+ listWorktrees(options.cwd),
15711
+ detectRepository(options.cwd)
15712
+ ]);
15713
+ } catch {
15714
+ if (isHeadless() && !options.branch) {
15715
+ options.stderr(
15716
+ "gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15717
+ );
15718
+ return 1;
15553
15719
  }
15720
+ const target = await resolveWarpTarget({ ...options, commandName: "gji go" });
15721
+ if (!target) return 1;
15722
+ appendHistory(target.path, target.branch).catch(() => void 0);
15723
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
15724
+ return 0;
15725
+ }
15726
+ if (!options.branch && isHeadless()) {
15727
+ options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
15554
15728
  return 1;
15555
15729
  }
15556
- const rawBranch = options.detached ? options.branch ?? createBranchPlaceholder() : options.branch ?? await promptForBranch(createBranchPlaceholder());
15557
- if (!rawBranch) {
15558
- if (options.json) {
15559
- options.stderr(`${JSON.stringify({ error: "Aborted" }, null, 2)}
15730
+ const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
15731
+ const resolvedPath = options.branch ? worktrees.find((entry) => entry.branch === options.branch)?.path : prompted ?? void 0;
15732
+ if (!resolvedPath) {
15733
+ if (options.branch) {
15734
+ options.stderr(`No worktree found for branch: ${options.branch}
15735
+ `);
15736
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
15560
15737
  `);
15561
15738
  } else {
15562
15739
  options.stderr("Aborted\n");
15563
15740
  }
15564
15741
  return 1;
15565
15742
  }
15566
- if (!options.detached) {
15567
- const branchError = validateBranchName(rawBranch);
15568
- if (branchError) {
15569
- if (options.json) {
15570
- options.stderr(`${JSON.stringify({ error: branchError }, null, 2)}
15743
+ const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
15744
+ const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15745
+ const hooks = extractHooks(config);
15746
+ await runHook(
15747
+ hooks.afterEnter,
15748
+ resolvedPath,
15749
+ { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename5(repository.repoRoot) },
15750
+ options.stderr
15751
+ );
15752
+ appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
15753
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15754
+ return 0;
15755
+ };
15756
+ }
15757
+ var runGoCommand = createGoCommand();
15758
+ async function promptForWorktree(worktrees) {
15759
+ const healthResults = await Promise.allSettled(
15760
+ worktrees.map((w2) => readWorktreeHealth(w2.path))
15761
+ );
15762
+ const choice = await ve({
15763
+ message: "Choose a worktree",
15764
+ options: worktrees.map((worktree, i) => {
15765
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15766
+ const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
15767
+ const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
15768
+ return {
15769
+ value: worktree.path,
15770
+ label: worktree.branch ?? "(detached)",
15771
+ hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
15772
+ };
15773
+ })
15774
+ });
15775
+ if (pD(choice)) {
15776
+ return null;
15777
+ }
15778
+ return choice;
15779
+ }
15780
+ function formatUpstreamHint(branch, health) {
15781
+ if (branch === null) return null;
15782
+ if (!health.hasUpstream) return "no upstream";
15783
+ if (health.upstreamGone) return "upstream gone";
15784
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
15785
+ if (health.ahead === 0) return `behind ${health.behind}`;
15786
+ if (health.behind === 0) return `ahead ${health.ahead}`;
15787
+ return `ahead ${health.ahead}, behind ${health.behind}`;
15788
+ }
15789
+
15790
+ // src/open.ts
15791
+ import { execFile as execFile4 } from "node:child_process";
15792
+ import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
15793
+ import { join as join7 } from "node:path";
15794
+ import { promisify as promisify5 } from "node:util";
15795
+ var execFileAsync4 = promisify5(execFile4);
15796
+ function createOpenCommand(dependencies = {}) {
15797
+ const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
15798
+ const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
15799
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
15800
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15801
+ return async function runOpenCommand2(options) {
15802
+ const [worktrees, repository] = await Promise.all([
15803
+ listWorktrees(options.cwd),
15804
+ detectRepository(options.cwd)
15805
+ ]);
15806
+ let targetPath;
15807
+ if (options.branch) {
15808
+ const entry = worktrees.find((w2) => w2.branch === options.branch);
15809
+ if (!entry) {
15810
+ options.stderr(`gji open: no worktree found for branch: ${options.branch}
15571
15811
  `);
15572
- } else {
15573
- options.stderr(`gji new: ${branchError}
15812
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
15574
15813
  `);
15575
- }
15576
15814
  return 1;
15577
15815
  }
15816
+ targetPath = entry.path;
15817
+ } else if (isHeadless()) {
15818
+ targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
15819
+ } else {
15820
+ const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
15821
+ if (!chosen) {
15822
+ options.stderr("Aborted\n");
15823
+ return 1;
15824
+ }
15825
+ targetPath = chosen;
15578
15826
  }
15579
- const rawBasePath = resolveConfigString(config, "worktreePath");
15580
- const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~") ? rawBasePath : void 0;
15581
- const worktreeName = options.detached ? rawBranch : applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
15582
- const worktreePath = usesGeneratedDetachedName ? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath) : resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
15583
- if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
15584
- if (options.force) {
15585
- if (!options.dryRun) {
15586
- try {
15587
- await execFileAsync3("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
15588
- } catch (err) {
15589
- if (!isNotRegisteredWorktreeError(err)) {
15590
- const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
15591
- if (options.json) {
15592
- options.stderr(`${JSON.stringify({ warning: msg }, null, 2)}
15593
- `);
15594
- } else {
15595
- options.stderr(`Warning: ${msg}
15596
- `);
15597
- }
15598
- }
15599
- }
15600
- if (!options.detached) {
15601
- try {
15602
- await execFileAsync3("git", ["branch", "-D", worktreeName], { cwd: repository.repoRoot });
15603
- } catch {
15604
- }
15605
- }
15606
- }
15607
- } else if (options.json || isHeadless()) {
15608
- const message = `target worktree path already exists: ${worktreePath}`;
15609
- if (options.json) {
15610
- options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15611
- `);
15612
- } else {
15613
- options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
15614
- `);
15615
- options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree
15616
- `);
15617
- options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15618
- `);
15619
- }
15827
+ const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15828
+ const savedEditor = resolveConfigString(config, "editor");
15829
+ let editorCli;
15830
+ if (options.editor) {
15831
+ editorCli = options.editor;
15832
+ } else if (savedEditor) {
15833
+ editorCli = savedEditor;
15834
+ } else {
15835
+ const installed = await detectEditors();
15836
+ if (installed.length === 0) {
15837
+ options.stderr(
15838
+ "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
15839
+ );
15620
15840
  return 1;
15841
+ }
15842
+ if (installed.length === 1 || isHeadless()) {
15843
+ editorCli = installed[0].cli;
15621
15844
  } else {
15622
- const choice = await prompt(worktreePath);
15623
- if (choice === "reuse") {
15624
- appendHistory(worktreePath, worktreeName).catch(() => void 0);
15625
- await writeOutput(worktreePath, options.stdout);
15626
- return 0;
15845
+ const chosen = await promptForEditor(installed);
15846
+ if (!chosen) {
15847
+ options.stderr("Aborted\n");
15848
+ return 1;
15627
15849
  }
15628
- options.stderr(`Aborted because target worktree path already exists: ${worktreePath}
15629
- `);
15630
- return 1;
15850
+ editorCli = chosen;
15631
15851
  }
15632
15852
  }
15633
- if (options.dryRun) {
15634
- if (options.json) {
15635
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15853
+ if (options.save && editorCli !== savedEditor) {
15854
+ await updateGlobalConfigKey("editor", editorCli);
15855
+ const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
15856
+ options.stdout(`Saved editor "${displayName2}" to global config
15636
15857
  `);
15858
+ }
15859
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15860
+ let openTarget = targetPath;
15861
+ if (options.workspace) {
15862
+ if (editorDef?.supportsWorkspace) {
15863
+ openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
15637
15864
  } else {
15638
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName})
15865
+ const displayName2 = editorDef?.name ?? editorCli;
15866
+ options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
15639
15867
  `);
15640
15868
  }
15641
- return 0;
15642
15869
  }
15643
- await mkdir5(dirname6(worktreePath), { recursive: true });
15644
- const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15645
- await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15646
- const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15647
- for (const pattern of syncPatterns) {
15648
- try {
15649
- await syncFiles(repository.repoRoot, worktreePath, [pattern]);
15650
- } catch (error) {
15651
- options.stderr(`Warning: failed to sync file "${pattern}": ${error instanceof Error ? error.message : String(error)}
15652
- `);
15653
- }
15870
+ const args = [];
15871
+ if (editorDef?.newWindowFlag) {
15872
+ args.push(editorDef.newWindowFlag);
15654
15873
  }
15655
- await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
15656
- const hooks = extractHooks(config);
15657
- await runHook(
15658
- hooks.afterCreate,
15659
- worktreePath,
15660
- { branch: worktreeName, path: worktreePath, repo: basename4(repository.repoRoot) },
15661
- options.stderr
15662
- );
15663
- if (options.json) {
15664
- options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}
15874
+ args.push(openTarget);
15875
+ try {
15876
+ await spawnEditor(editorCli, args);
15877
+ } catch (error) {
15878
+ const message = error instanceof Error ? error.message : String(error);
15879
+ options.stderr(`gji open: failed to launch editor: ${message}
15665
15880
  `);
15666
- } else {
15667
- await appendHistory(worktreePath, worktreeName);
15668
- await writeOutput(worktreePath, options.stdout);
15881
+ return 1;
15669
15882
  }
15883
+ const displayName = editorDef?.name ?? editorCli;
15884
+ options.stdout(`Opened ${targetPath} in ${displayName}
15885
+ `);
15670
15886
  return 0;
15671
15887
  };
15672
15888
  }
15673
- var runNewCommand = createNewCommand();
15674
- function generateBranchPlaceholder(random = Math.random) {
15675
- const roots = [
15676
- "socrates",
15677
- "prometheus",
15678
- "beethoven",
15679
- "ada",
15680
- "turing",
15681
- "hypatia",
15682
- "tesla",
15683
- "curie",
15684
- "diogenes",
15685
- "plato",
15686
- "hephaestus",
15687
- "athena",
15688
- "archimedes",
15689
- "euclid",
15690
- "heraclitus",
15691
- "galileo",
15692
- "newton",
15693
- "lovelace",
15694
- "nietzsche",
15695
- "kafka"
15696
- ];
15697
- const antics = [
15698
- "borrowed-a-bike",
15699
- "brought-snacks",
15700
- "missed-the-bus",
15701
- "lost-the-keys",
15702
- "spilled-the-coffee",
15703
- "forgot-the-umbrella",
15704
- "walked-the-dog",
15705
- "missed-the-train",
15706
- "wrote-a-poem",
15707
- "burned-the-toast",
15708
- "fed-the-pigeons",
15709
- "watered-the-plants",
15710
- "washed-the-dishes",
15711
- "folded-the-laundry",
15712
- "took-a-nap"
15713
- ];
15714
- return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
15889
+ var runOpenCommand = createOpenCommand();
15890
+ async function detectInstalledEditors() {
15891
+ const results = await Promise.all(
15892
+ EDITORS.map(async (editor) => ({ editor, available: await isCommandAvailable(editor.cli) }))
15893
+ );
15894
+ return results.filter((r2) => r2.available).map((r2) => r2.editor);
15895
+ }
15896
+ async function isCommandAvailable(command) {
15897
+ try {
15898
+ await execFileAsync4("which", [command]);
15899
+ return true;
15900
+ } catch {
15901
+ return false;
15902
+ }
15903
+ }
15904
+ async function defaultPromptForWorktree(worktrees) {
15905
+ const choice = await ve({
15906
+ message: "Choose a worktree to open",
15907
+ options: worktrees.map((w2) => ({
15908
+ value: w2.path,
15909
+ label: w2.branch ?? "(detached)",
15910
+ hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
15911
+ }))
15912
+ });
15913
+ if (pD(choice)) return null;
15914
+ return choice;
15915
+ }
15916
+ async function defaultPromptForEditor(editors) {
15917
+ const choice = await ve({
15918
+ message: "Choose an editor",
15919
+ options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
15920
+ });
15921
+ if (pD(choice)) return null;
15922
+ return choice;
15923
+ }
15924
+ async function ensureWorkspaceFile(worktreePath, repoName) {
15925
+ const workspacePath = join7(worktreePath, `${repoName}.code-workspace`);
15926
+ try {
15927
+ await access4(workspacePath);
15928
+ return workspacePath;
15929
+ } catch {
15930
+ }
15931
+ const workspace = { folders: [{ path: "." }], settings: {} };
15932
+ await writeFile5(workspacePath, `${JSON.stringify(workspace, null, 2)}
15933
+ `, "utf8");
15934
+ return workspacePath;
15935
+ }
15936
+
15937
+ // src/init.ts
15938
+ import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile6 } from "node:fs/promises";
15939
+ import { homedir as homedir5 } from "node:os";
15940
+ import { dirname as dirname7, join as join8 } from "node:path";
15941
+ var START_MARKER = "# >>> gji init >>>";
15942
+ var END_MARKER = "# <<< gji init <<<";
15943
+ var SHELL_WRAPPED_COMMANDS = [
15944
+ {
15945
+ bypassOptions: ["--help"],
15946
+ commandName: "new",
15947
+ envVar: "GJI_NEW_OUTPUT_FILE",
15948
+ names: ["new"],
15949
+ tempPrefix: "gji-new"
15950
+ },
15951
+ {
15952
+ bypassOptions: ["--help"],
15953
+ commandName: "pr",
15954
+ envVar: "GJI_PR_OUTPUT_FILE",
15955
+ names: ["pr"],
15956
+ tempPrefix: "gji-pr"
15957
+ },
15958
+ {
15959
+ bypassOptions: ["--print"],
15960
+ commandName: "back",
15961
+ envVar: "GJI_BACK_OUTPUT_FILE",
15962
+ names: ["back"],
15963
+ tempPrefix: "gji-back"
15964
+ },
15965
+ {
15966
+ bypassOptions: ["--print"],
15967
+ commandName: "go",
15968
+ envVar: "GJI_GO_OUTPUT_FILE",
15969
+ names: ["go", "jump"],
15970
+ tempPrefix: "gji-go"
15971
+ },
15972
+ {
15973
+ bypassOptions: ["--print"],
15974
+ commandName: "root",
15975
+ envVar: "GJI_ROOT_OUTPUT_FILE",
15976
+ names: ["root"],
15977
+ tempPrefix: "gji-root"
15978
+ },
15979
+ {
15980
+ bypassOptions: ["--help"],
15981
+ commandName: "remove",
15982
+ envVar: "GJI_REMOVE_OUTPUT_FILE",
15983
+ names: ["remove", "rm"],
15984
+ tempPrefix: "gji-remove"
15985
+ },
15986
+ {
15987
+ bypassOptions: ["--print", "--json"],
15988
+ commandName: "warp",
15989
+ envVar: "GJI_WARP_OUTPUT_FILE",
15990
+ names: ["warp"],
15991
+ tempPrefix: "gji-warp"
15992
+ }
15993
+ ];
15994
+ async function runInitCommand(options) {
15995
+ const shell = resolveSupportedShell(options.shell, process.env.SHELL);
15996
+ const home = options.home ?? homedir5();
15997
+ if (!shell) {
15998
+ options.stderr?.(
15999
+ "Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n"
16000
+ );
16001
+ return 1;
16002
+ }
16003
+ const script = renderShellIntegration(shell);
16004
+ if (!options.write) {
16005
+ options.stdout(script);
16006
+ return 0;
16007
+ }
16008
+ const rcPath = resolveShellConfigPath(shell, home);
16009
+ await mkdir6(dirname7(rcPath), { recursive: true });
16010
+ const current = await readExistingConfig(rcPath);
16011
+ const next = upsertShellIntegration(current, script);
16012
+ await writeFile6(rcPath, next, "utf8");
16013
+ options.stdout(`${rcPath}
16014
+ `);
16015
+ const { config: globalConfig } = await loadGlobalConfig(home);
16016
+ const alreadyConfigured = "shellIntegration" in globalConfig || "installSaveTarget" in globalConfig;
16017
+ const hasCustomPrompt = options.promptForSetup !== void 0;
16018
+ const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
16019
+ if (!alreadyConfigured && canPrompt) {
16020
+ const prompt = options.promptForSetup ?? defaultPromptForSetup;
16021
+ const result = await prompt();
16022
+ if (result) {
16023
+ await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
16024
+ await saveWizardConfig(result, options.cwd, home);
16025
+ }
16026
+ }
16027
+ await updateGlobalConfigKey("shellIntegration", true, home);
16028
+ return 0;
16029
+ }
16030
+ function renderShellIntegration(shell) {
16031
+ const commandBlocks = SHELL_WRAPPED_COMMANDS.map(
16032
+ (command) => shell === "fish" ? renderFishWrapper(command) : renderPosixWrapper(command)
16033
+ ).join("\n\n");
16034
+ switch (shell) {
16035
+ case "fish":
16036
+ return `${START_MARKER}
16037
+ function gji --wraps gji --description 'gji shell integration'
16038
+ ${indentBlock(commandBlocks, 4)}
16039
+
16040
+ command gji $argv
16041
+ end
16042
+ ${END_MARKER}
16043
+ `;
16044
+ case "bash":
16045
+ case "zsh":
16046
+ return `${START_MARKER}
16047
+ gji() {
16048
+ ${indentBlock(commandBlocks, 2)}
16049
+
16050
+ command gji "$@"
16051
+ }
16052
+ ${END_MARKER}
16053
+ `;
16054
+ }
16055
+ }
16056
+ function upsertShellIntegration(existingConfig, script) {
16057
+ const trimmedScript = script.trimEnd();
16058
+ const blockPattern = new RegExp(
16059
+ `${escapeForRegExp(START_MARKER)}[\\s\\S]*?${escapeForRegExp(END_MARKER)}\\n?`,
16060
+ "m"
16061
+ );
16062
+ if (blockPattern.test(existingConfig)) {
16063
+ return ensureTrailingNewline(
16064
+ existingConfig.replace(blockPattern, `${trimmedScript}
16065
+ `)
16066
+ );
16067
+ }
16068
+ const prefix = existingConfig.trimEnd();
16069
+ if (prefix.length === 0) {
16070
+ return ensureTrailingNewline(trimmedScript);
16071
+ }
16072
+ return ensureTrailingNewline(`${prefix}
16073
+
16074
+ ${trimmedScript}`);
16075
+ }
16076
+ async function saveWizardConfig(result, cwd, home) {
16077
+ const values = {};
16078
+ if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
16079
+ if (result.worktreePath) values.worktreePath = result.worktreePath;
16080
+ const hooks = {};
16081
+ if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16082
+ if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16083
+ if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
16084
+ if (Object.keys(hooks).length > 0) values.hooks = hooks;
16085
+ if (Object.keys(values).length === 0) return;
16086
+ if (result.installSaveTarget === "local") {
16087
+ const loaded = await loadConfig(cwd);
16088
+ await saveLocalConfig(cwd, { ...loaded.config, ...values });
16089
+ } else {
16090
+ const { config: existing } = await loadGlobalConfig(home);
16091
+ await saveGlobalConfig({ ...existing, ...values }, home);
16092
+ }
16093
+ }
16094
+ function resolveShellConfigPath(shell, home) {
16095
+ switch (shell) {
16096
+ case "bash":
16097
+ return join8(home, ".bashrc");
16098
+ case "fish":
16099
+ return join8(home, ".config", "fish", "config.fish");
16100
+ case "zsh":
16101
+ return join8(home, ".zshrc");
16102
+ }
16103
+ }
16104
+ async function readExistingConfig(path9) {
16105
+ try {
16106
+ return await readFile4(path9, "utf8");
16107
+ } catch (error) {
16108
+ if (isMissingFileError2(error)) {
16109
+ return "";
16110
+ }
16111
+ throw error;
16112
+ }
16113
+ }
16114
+ function ensureTrailingNewline(value) {
16115
+ return value.endsWith("\n") ? value : `${value}
16116
+ `;
16117
+ }
16118
+ function escapeForRegExp(value) {
16119
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16120
+ }
16121
+ function isMissingFileError2(error) {
16122
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
16123
+ }
16124
+ function renderFishWrapper(command) {
16125
+ const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
16126
+ const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
16127
+ const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
16128
+ const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
16129
+ return `if test (count $argv) -gt 0; and ${nameCondition}
16130
+ set -e argv[1]
16131
+ if test (count $argv) -gt 0; and ${bypassCondition}
16132
+ command gji ${command.commandName} $argv
16133
+ return $status
16134
+ end
16135
+
16136
+ set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
16137
+ or return 1
16138
+ env ${command.envVar}=$output_file command gji ${command.commandName} $argv
16139
+ or begin
16140
+ set -l status_code $status
16141
+ rm -f $output_file
16142
+ return $status_code
16143
+ end
16144
+ set -l target (cat $output_file)
16145
+ rm -f $output_file
16146
+ cd $target
16147
+ return $status
16148
+ end`;
16149
+ }
16150
+ function renderPosixWrapper(command) {
16151
+ const tests = command.names.map((name) => `[ "$1" = "${name}" ]`).join(" || ");
16152
+ const bypassTests = command.bypassOptions.map((opt) => `[ "\${1:-}" = "${opt}" ]`).join(" || ");
16153
+ return `if ${tests}; then
16154
+ shift
16155
+ if ${bypassTests}; then
16156
+ command gji ${command.commandName} "$@"
16157
+ return $?
16158
+ fi
16159
+
16160
+ local target
16161
+ local output_file
16162
+ output_file="$(mktemp -t ${command.tempPrefix}.XXXXXX)" || return 1
16163
+ ${command.envVar}="$output_file" command gji ${command.commandName} "$@" || { local exit_code=$?; rm -f "$output_file"; return $exit_code; }
16164
+ target="$(cat "$output_file")"
16165
+ rm -f "$output_file"
16166
+ cd "$target" || return $?
16167
+ return 0
16168
+ fi`;
16169
+ }
16170
+ function indentBlock(value, spaces) {
16171
+ const prefix = " ".repeat(spaces);
16172
+ return value.split("\n").map((line) => line.length === 0 ? "" : `${prefix}${line}`).join("\n");
15715
16173
  }
15716
- function applyConfiguredBranchPrefix(branch, branchPrefix) {
15717
- if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
15718
- return branch;
16174
+ async function defaultPromptForSetup() {
16175
+ Ie("gji setup");
16176
+ const installSaveTarget = await ve({
16177
+ message: "Where should preferences be saved?",
16178
+ options: [
16179
+ { value: "global", label: "~/.config/gji/config.json", hint: "personal \u2014 never committed" },
16180
+ { value: "local", label: ".gji.json", hint: "repo \u2014 committed with the project" }
16181
+ ]
16182
+ });
16183
+ if (pD(installSaveTarget)) {
16184
+ Se("Setup skipped.");
16185
+ return null;
15719
16186
  }
15720
- if (branch.startsWith(branchPrefix)) {
15721
- return branch;
16187
+ const branchPrefix = await he({
16188
+ message: "Default branch prefix?",
16189
+ placeholder: "e.g. feat/ or fix/ \u2014 leave blank to skip"
16190
+ });
16191
+ if (pD(branchPrefix)) {
16192
+ Se("Setup skipped.");
16193
+ return null;
15722
16194
  }
15723
- return `${branchPrefix}${branch}`;
15724
- }
15725
- async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15726
- let attempt = 1;
15727
- while (true) {
15728
- const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15729
- const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15730
- if (!await pathExists(candidatePath)) {
15731
- return candidatePath;
15732
- }
15733
- attempt += 1;
16195
+ const worktreePath = await he({
16196
+ message: "Worktree base path?",
16197
+ placeholder: "leave blank to use the default path"
16198
+ });
16199
+ if (pD(worktreePath)) {
16200
+ Se("Setup skipped.");
16201
+ return null;
15734
16202
  }
15735
- }
15736
- async function defaultPromptForBranch(placeholder) {
15737
- const choice = await he({
15738
- defaultValue: placeholder,
15739
- message: "Name the new branch",
15740
- placeholder,
15741
- validate: (value) => {
15742
- const trimmed = value.trim();
15743
- return validateBranchName(trimmed) ?? void 0;
15744
- }
16203
+ const afterCreate = await he({
16204
+ message: "afterCreate hook \u2014 run after creating a worktree?",
16205
+ placeholder: "e.g. pnpm install \u2014 leave blank to skip"
15745
16206
  });
15746
- if (pD(choice)) {
16207
+ if (pD(afterCreate)) {
16208
+ Se("Setup skipped.");
15747
16209
  return null;
15748
16210
  }
15749
- return choice.trim();
15750
- }
15751
- function pickRandom(values, random) {
15752
- const index = Math.floor(random() * values.length);
15753
- return values[Math.min(index, values.length - 1)];
16211
+ const afterEnter = await he({
16212
+ message: "afterEnter hook \u2014 run after entering a worktree?",
16213
+ placeholder: "e.g. nvm use \u2014 leave blank to skip"
16214
+ });
16215
+ if (pD(afterEnter)) {
16216
+ Se("Setup skipped.");
16217
+ return null;
16218
+ }
16219
+ const beforeRemove = await he({
16220
+ message: "beforeRemove hook \u2014 run before removing a worktree?",
16221
+ placeholder: "leave blank to skip"
16222
+ });
16223
+ if (pD(beforeRemove)) {
16224
+ Se("Setup skipped.");
16225
+ return null;
16226
+ }
16227
+ Se("Setup complete!");
16228
+ const hooks = {};
16229
+ if (afterCreate) hooks.afterCreate = afterCreate;
16230
+ if (afterEnter) hooks.afterEnter = afterEnter;
16231
+ if (beforeRemove) hooks.beforeRemove = beforeRemove;
16232
+ return {
16233
+ branchPrefix: branchPrefix || void 0,
16234
+ hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
16235
+ installSaveTarget,
16236
+ worktreePath: worktreePath || void 0
16237
+ };
15754
16238
  }
15755
- async function localBranchExists(repoRoot, branchName) {
15756
- try {
15757
- await execFileAsync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
15758
- return true;
15759
- } catch {
15760
- return false;
16239
+
16240
+ // src/paths.ts
16241
+ function comparePaths(left, right) {
16242
+ if (left < right) {
16243
+ return -1;
16244
+ }
16245
+ if (left > right) {
16246
+ return 1;
15761
16247
  }
16248
+ return 0;
15762
16249
  }
15763
- async function writeOutput(worktreePath, stdout) {
15764
- await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
16250
+
16251
+ // src/ls.ts
16252
+ async function runLsCommand(options) {
16253
+ const worktrees = sortWorktrees(await listWorktrees(options.cwd));
16254
+ if (options.compact) {
16255
+ if (options.json) {
16256
+ options.stdout(`${JSON.stringify(worktrees, null, 2)}
16257
+ `);
16258
+ return 0;
16259
+ }
16260
+ options.stdout(`${formatWorktreeTable(worktrees)}
16261
+ `);
16262
+ return 0;
16263
+ }
16264
+ const infos = await readWorktreeInfos(worktrees);
16265
+ if (options.json) {
16266
+ options.stdout(`${JSON.stringify(infos, null, 2)}
16267
+ `);
16268
+ return 0;
16269
+ }
16270
+ options.stdout(`${formatDetailedWorktreeTable(infos)}
16271
+ `);
16272
+ return 0;
15765
16273
  }
15766
- function isNotRegisteredWorktreeError(error) {
15767
- const stderr = hasExecStderr(error) ? error.stderr : String(error);
15768
- return stderr.includes("is not a working tree") || stderr.includes("not a linked working tree");
16274
+ function formatDetailedWorktreeTable(worktrees) {
16275
+ const rows = worktrees.map((worktree) => ({
16276
+ branch: worktree.branch ?? "(detached)",
16277
+ isCurrent: worktree.isCurrent,
16278
+ lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
16279
+ path: worktree.path,
16280
+ status: worktree.status,
16281
+ upstream: formatUpstreamState(worktree.upstream)
16282
+ }));
16283
+ const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
16284
+ const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
16285
+ const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
16286
+ const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
16287
+ const lines = [
16288
+ " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
16289
+ ];
16290
+ for (const row of rows) {
16291
+ lines.push(
16292
+ `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.status.padEnd(statusWidth, " ")} ${row.upstream.padEnd(upstreamWidth, " ")} ${row.lastCommit.padEnd(lastCommitWidth, " ")} ` + row.path
16293
+ );
16294
+ }
16295
+ return lines.join("\n");
15769
16296
  }
15770
- function hasExecStderr(error) {
15771
- return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
16297
+ function formatWorktreeTable(worktrees) {
16298
+ const rows = worktrees.map((worktree) => ({
16299
+ branch: worktree.branch ?? "(detached)",
16300
+ isCurrent: worktree.isCurrent,
16301
+ path: worktree.path
16302
+ }));
16303
+ const branchWidth = Math.max(
16304
+ "BRANCH".length,
16305
+ ...rows.map((row) => row.branch.length)
16306
+ );
16307
+ const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
16308
+ for (const row of rows) {
16309
+ lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
16310
+ }
16311
+ return lines.join("\n");
15772
16312
  }
15773
- function toExecMessage(error) {
15774
- return hasExecStderr(error) ? error.stderr.trim() : String(error);
16313
+ function sortWorktrees(worktrees) {
16314
+ return [...worktrees].sort((left, right) => {
16315
+ if (left.isCurrent && !right.isCurrent) return -1;
16316
+ if (!left.isCurrent && right.isCurrent) return 1;
16317
+ return comparePaths(left.path, right.path);
16318
+ });
15775
16319
  }
15776
16320
 
15777
16321
  // src/pr.ts
15778
- import { mkdir as mkdir6 } from "node:fs/promises";
15779
- import { basename as basename5, dirname as dirname7 } from "node:path";
15780
- import { execFile as execFile4 } from "node:child_process";
15781
- import { promisify as promisify5 } from "node:util";
15782
- var execFileAsync4 = promisify5(execFile4);
16322
+ import { mkdir as mkdir7 } from "node:fs/promises";
16323
+ import { basename as basename6, dirname as dirname8 } from "node:path";
16324
+ import { execFile as execFile5 } from "node:child_process";
16325
+ import { promisify as promisify6 } from "node:util";
16326
+ var execFileAsync5 = promisify6(execFile5);
15783
16327
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
15784
16328
  function parsePrInput(input) {
15785
16329
  if (/^\d+$/.test(input)) return input;
@@ -15860,10 +16404,10 @@ function createPrCommand(dependencies = {}) {
15860
16404
  }
15861
16405
  return 1;
15862
16406
  }
15863
- await mkdir6(dirname7(worktreePath), { recursive: true });
16407
+ await mkdir7(dirname8(worktreePath), { recursive: true });
15864
16408
  const branchAlreadyExists = await localBranchExists2(repository.repoRoot, branchName);
15865
16409
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
15866
- await execFileAsync4("git", worktreeArgs, { cwd: repository.repoRoot });
16410
+ await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
15867
16411
  const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15868
16412
  for (const pattern of syncPatterns) {
15869
16413
  try {
@@ -15878,7 +16422,7 @@ function createPrCommand(dependencies = {}) {
15878
16422
  await runHook(
15879
16423
  hooks.afterCreate,
15880
16424
  worktreePath,
15881
- { branch: branchName, path: worktreePath, repo: basename5(repository.repoRoot) },
16425
+ { branch: branchName, path: worktreePath, repo: basename6(repository.repoRoot) },
15882
16426
  options.stderr
15883
16427
  );
15884
16428
  if (options.json) {
@@ -15893,7 +16437,7 @@ function createPrCommand(dependencies = {}) {
15893
16437
  }
15894
16438
  async function localBranchExists2(repoRoot, branchName) {
15895
16439
  try {
15896
- await execFileAsync4(
16440
+ await execFileAsync5(
15897
16441
  "git",
15898
16442
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
15899
16443
  { cwd: repoRoot }
@@ -15907,7 +16451,7 @@ var runPrCommand = createPrCommand();
15907
16451
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
15908
16452
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
15909
16453
  try {
15910
- await execFileAsync4(
16454
+ await execFileAsync5(
15911
16455
  "git",
15912
16456
  ["fetch", "origin", `${sourceRef}:${remoteRef}`],
15913
16457
  { cwd: repoRoot }
@@ -15951,10 +16495,10 @@ async function writeOutput2(worktreePath, stdout) {
15951
16495
  }
15952
16496
 
15953
16497
  // src/remove.ts
15954
- import { basename as basename6 } from "node:path";
16498
+ import { basename as basename7 } from "node:path";
15955
16499
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
15956
16500
  function createRemoveCommand(dependencies = {}) {
15957
- const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
16501
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
15958
16502
  const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval2;
15959
16503
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
15960
16504
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
@@ -16016,7 +16560,7 @@ function createRemoveCommand(dependencies = {}) {
16016
16560
  await runHook(
16017
16561
  hooks.beforeRemove,
16018
16562
  worktree.path,
16019
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename6(repository.repoRoot) },
16563
+ { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename7(repository.repoRoot) },
16020
16564
  options.stderr
16021
16565
  );
16022
16566
  try {
@@ -16066,7 +16610,7 @@ function createRemoveCommand(dependencies = {}) {
16066
16610
  };
16067
16611
  }
16068
16612
  var runRemoveCommand = createRemoveCommand();
16069
- async function defaultPromptForWorktree(worktrees) {
16613
+ async function defaultPromptForWorktree2(worktrees) {
16070
16614
  const choice = await ve({
16071
16615
  message: "Choose a worktree to finish",
16072
16616
  options: worktrees.map((worktree) => ({
@@ -16355,6 +16899,7 @@ function readPackageMetadata() {
16355
16899
  }
16356
16900
  async function runCli(argv, options = {}) {
16357
16901
  await maybeNotifyForUpdates(argv);
16902
+ maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
16358
16903
  const program2 = createProgram();
16359
16904
  const cwd = options.cwd ?? process.cwd();
16360
16905
  const stdout = options.stdout ?? (() => void 0);
@@ -16398,14 +16943,18 @@ function defaultNotifyForUpdates(pkg) {
16398
16943
  const notifier = updateNotifier({ pkg });
16399
16944
  notifier.notify();
16400
16945
  }
16946
+ function maybeRegisterCurrentRepo(cwd) {
16947
+ detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
16948
+ }
16401
16949
  function registerCommands(program2) {
16402
- 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("--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"));
16950
+ program2.command("new [branch]").description("create a new branch or detached linked worktree").option("-f, --force", "remove and recreate the worktree if the target path already exists").option("--detached", "create a detached worktree without a branch").option("--open", "open the new worktree in an editor after creation").option("--editor <cli>", "editor CLI to use with --open (code, cursor, zed, \u2026)").option("--dry-run", "show what would be created without executing any git commands or writing files").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("new"));
16403
16951
  program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
16404
16952
  program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
16405
16953
  program2.command("pr <ref>").description("fetch a pull request by number, #number, or URL into a linked worktree").option("--dry-run", "show what would be created without executing any git commands or writing files").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("pr"));
16406
16954
  program2.command("back [n]").description("navigate to the previously visited worktree, optionally N steps back").option("--print", "print the resolved worktree path explicitly").action(notImplemented("back"));
16407
16955
  program2.command("history").description("show navigation history").option("--json", "print history as JSON").action(notImplemented("history"));
16408
- program2.command("go [branch]").description("print or select a worktree path").option("--print", "print the resolved worktree path explicitly").action(notImplemented("go"));
16956
+ program2.command("open [branch]").description("open the worktree in an editor").option("--editor <cli>", "editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)").option("--save", "save the chosen editor to global config").option("--workspace", "generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)").action(notImplemented("open"));
16957
+ program2.command("go [branch]").alias("jump").description("print or select a worktree path").option("--print", "print the resolved worktree path explicitly").action(notImplemented("go"));
16409
16958
  program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
16410
16959
  program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
16411
16960
  program2.command("sync").description("fetch and update one or all worktrees").option("--all", "sync every worktree in the repository").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("sync"));
@@ -16413,6 +16962,7 @@ function registerCommands(program2) {
16413
16962
  program2.command("clean").description("interactively prune linked worktrees").option("-f, --force", "bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches").option("--stale", "only target clean worktrees whose upstream is gone and branch is merged into the default branch").option("--dry-run", "show what would be deleted without removing anything").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("clean"));
16414
16963
  program2.command("remove [branch]").alias("rm").description("remove a linked worktree and delete its branch when present").option("-f, --force", "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch").option("--dry-run", "show what would be deleted without removing anything").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("remove"));
16415
16964
  program2.command("trigger-hook <hook>").description("run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree").action(notImplemented("trigger-hook"));
16965
+ program2.command("warp [branch]").description("jump to any worktree across all known repos").option("-n, --new [branch]", "create a new worktree in a registered repo").option("--print", "print the resolved worktree path without changing directory").option("--json", "emit JSON on success or error instead of human-readable output").action(notImplemented("warp"));
16416
16966
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
16417
16967
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
16418
16968
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
@@ -16420,7 +16970,7 @@ function registerCommands(program2) {
16420
16970
  }
16421
16971
  function attachCommandActions(program2, options) {
16422
16972
  program2.commands.find((command) => command.name() === "new")?.action(async (branch, commandOptions) => {
16423
- const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, force: commandOptions.force, json: commandOptions.json });
16973
+ const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, editor: commandOptions.editor, force: commandOptions.force, json: commandOptions.json, open: commandOptions.open });
16424
16974
  if (exitCode !== 0) {
16425
16975
  throw commanderExit(exitCode);
16426
16976
  }
@@ -16481,6 +17031,20 @@ function attachCommandActions(program2, options) {
16481
17031
  throw commanderExit(exitCode);
16482
17032
  }
16483
17033
  });
17034
+ program2.commands.find((command) => command.name() === "open")?.action(async (branch, commandOptions) => {
17035
+ const exitCode = await runOpenCommand({
17036
+ branch,
17037
+ cwd: options.cwd,
17038
+ editor: commandOptions.editor,
17039
+ save: commandOptions.save,
17040
+ stderr: options.stderr,
17041
+ stdout: options.stdout,
17042
+ workspace: commandOptions.workspace
17043
+ });
17044
+ if (exitCode !== 0) {
17045
+ throw commanderExit(exitCode);
17046
+ }
17047
+ });
16484
17048
  program2.commands.find((command) => command.name() === "go")?.action(async (branch, commandOptions) => {
16485
17049
  const exitCode = await runGoCommand({
16486
17050
  branch,
@@ -16575,6 +17139,22 @@ function attachCommandActions(program2, options) {
16575
17139
  throw commanderExit(exitCode);
16576
17140
  }
16577
17141
  });
17142
+ program2.commands.find((command) => command.name() === "warp")?.action(async (branch, commandOptions) => {
17143
+ const newFlag = commandOptions.new;
17144
+ const newWorktree = newFlag !== void 0 && newFlag !== false;
17145
+ const newBranch = typeof newFlag === "string" ? newFlag : void 0;
17146
+ const exitCode = await runWarpCommand({
17147
+ branch: newWorktree ? newBranch ?? branch : branch,
17148
+ cwd: options.cwd,
17149
+ json: commandOptions.json,
17150
+ newWorktree,
17151
+ stderr: options.stderr,
17152
+ stdout: options.stdout
17153
+ });
17154
+ if (exitCode !== 0) {
17155
+ throw commanderExit(exitCode);
17156
+ }
17157
+ });
16578
17158
  const configCommand = program2.commands.find((command) => command.name() === "config");
16579
17159
  configCommand?.action(async () => {
16580
17160
  const exitCode = await runConfigCommand({
@@ -16657,7 +17237,7 @@ async function main() {
16657
17237
  }
16658
17238
  async function warnIfMissingShellIntegration() {
16659
17239
  try {
16660
- const { config } = await loadGlobalConfig(homedir5());
17240
+ const { config } = await loadGlobalConfig(homedir6());
16661
17241
  if (!config.shellIntegration) {
16662
17242
  const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
16663
17243
  const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";