@solaqua/gji 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -14796,635 +14847,209 @@ function writeJson(stdout, value) {
14796
14847
  }
14797
14848
 
14798
14849
  // 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;
14811
- }
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;
14824
- }
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
- };
14838
- }
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
- })
14850
+ import { basename as basename5 } from "node:path";
14851
+
14852
+ // src/new.ts
14853
+ import { mkdir as mkdir4 } from "node:fs/promises";
14854
+ import { basename as basename3, dirname as dirname5 } from "node:path";
14855
+ import { execFile as execFile3 } from "node:child_process";
14856
+ import { promisify as promisify4 } from "node:util";
14857
+
14858
+ // src/editor.ts
14859
+ import { spawn as spawn3 } from "node:child_process";
14860
+ var EDITORS = [
14861
+ { cli: "cursor", name: "Cursor", newWindowFlag: "--new-window", supportsWorkspace: true },
14862
+ { cli: "code", name: "VS Code", newWindowFlag: "--new-window", supportsWorkspace: true },
14863
+ { cli: "windsurf", name: "Windsurf", newWindowFlag: "--new-window", supportsWorkspace: true },
14864
+ { cli: "zed", name: "Zed", supportsWorkspace: false },
14865
+ { cli: "subl", name: "Sublime Text", newWindowFlag: "--new-window", supportsWorkspace: false }
14866
+ ];
14867
+ async function defaultSpawnEditor(cli, args) {
14868
+ const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
14869
+ await new Promise((resolve5, reject) => {
14870
+ child.once("error", reject);
14871
+ child.once("spawn", resolve5);
14856
14872
  });
14857
- if (pD(choice)) {
14858
- return null;
14859
- }
14860
- return choice;
14861
- }
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}`;
14873
+ child.unref();
14870
14874
  }
14871
14875
 
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);
14876
+ // src/file-sync.ts
14877
+ import { copyFile, mkdir as mkdir3, stat } from "node:fs/promises";
14878
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join4, normalize } from "node:path";
14879
+ async function syncFiles(mainRoot, targetPath, patterns) {
14880
+ for (const pattern of patterns) {
14881
+ if (isAbsolute2(pattern)) {
14882
+ throw new Error(`syncFiles: pattern must be a relative path, got: ${pattern}`);
14883
+ }
14884
+ const normalized = normalize(pattern);
14885
+ if (normalized.startsWith("..")) {
14886
+ throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
14887
+ }
14888
+ const sourcePath = join4(mainRoot, normalized);
14889
+ const destPath = join4(targetPath, normalized);
14890
+ const sourceExists = await fileExists(sourcePath);
14891
+ if (!sourceExists) {
14892
+ continue;
14893
+ }
14894
+ const destExists = await fileExists(destPath);
14895
+ if (destExists) {
14896
+ continue;
14953
14897
  }
14898
+ await mkdir3(dirname4(destPath), { recursive: true });
14899
+ await copyFile(sourcePath, destPath);
14954
14900
  }
14955
- await updateGlobalConfigKey("shellIntegration", true, home);
14956
- return 0;
14957
- }
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 "$@"
14979
14901
  }
14980
- ${END_MARKER}
14981
- `;
14902
+ async function fileExists(path9) {
14903
+ try {
14904
+ await stat(path9);
14905
+ return true;
14906
+ } catch (error) {
14907
+ if (isNotFoundError(error)) {
14908
+ return false;
14909
+ }
14910
+ throw error;
14982
14911
  }
14983
14912
  }
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
- );
14995
- }
14996
- const prefix = existingConfig.trimEnd();
14997
- if (prefix.length === 0) {
14998
- return ensureTrailingNewline(trimmedScript);
14999
- }
15000
- return ensureTrailingNewline(`${prefix}
15001
-
15002
- ${trimmedScript}`);
14913
+ function isNotFoundError(error) {
14914
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
15003
14915
  }
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);
14916
+
14917
+ // src/install-prompt.ts
14918
+ import { spawn as spawn4 } from "node:child_process";
14919
+
14920
+ // src/package-manager.ts
14921
+ import { access as access2, readdir } from "node:fs/promises";
14922
+ import { join as join5 } from "node:path";
14923
+ var ENTRIES = [
14924
+ // JavaScript / TypeScript
14925
+ { name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
14926
+ { name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
14927
+ { name: "bun", signals: ["bun.lockb"], command: "bun install" },
14928
+ { name: "npm", signals: ["package-lock.json"], command: "npm install" },
14929
+ { name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
14930
+ // Python
14931
+ { name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
14932
+ { name: "uv", signals: ["uv.lock"], command: "uv sync" },
14933
+ { name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
14934
+ { name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
14935
+ { name: "conda-lock", signals: ["conda-lock.yml"], command: "conda-lock install" },
14936
+ { name: "conda", signals: ["environment.yml"], command: "conda env update --file environment.yml" },
14937
+ // R
14938
+ { name: "renv", signals: ["renv.lock"], command: "Rscript -e 'renv::restore()'" },
14939
+ // Rust
14940
+ { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
14941
+ // Go
14942
+ { name: "go", signals: ["go.sum"], command: "go mod download" },
14943
+ // Ruby
14944
+ { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
14945
+ // PHP
14946
+ { name: "composer", signals: ["composer.lock"], command: "composer install" },
14947
+ // Elixir / Erlang
14948
+ { name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
14949
+ { name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
14950
+ // Dart / Flutter
14951
+ { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
14952
+ // Java / Kotlin / Scala
14953
+ { name: "maven", signals: ["pom.xml"], command: "mvn install" },
14954
+ { name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
14955
+ { name: "gradle", signals: ["build.gradle", "build.gradle.kts"], command: "gradle build" },
14956
+ { name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
14957
+ // .NET (C# / F# / VB)
14958
+ { name: "dotnet", signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"], command: "dotnet restore", glob: true },
14959
+ // Swift
14960
+ { name: "swift", signals: ["Package.swift"], command: "swift package resolve" },
14961
+ // Haskell
14962
+ { name: "stack", signals: ["stack.yaml"], command: "stack build" },
14963
+ { name: "cabal", signals: ["cabal.project"], command: "cabal install --only-dependencies" },
14964
+ { name: "cabal", signals: ["*.cabal"], command: "cabal install --only-dependencies", glob: true },
14965
+ // Clojure
14966
+ { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
14967
+ { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
14968
+ // OCaml
14969
+ { name: "dune", signals: ["dune-project"], command: "dune build" },
14970
+ // Julia
14971
+ { name: "julia", signals: ["Manifest.toml"], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
14972
+ // Nim
14973
+ { name: "nimble", signals: ["*.nimble"], command: "nimble install", glob: true },
14974
+ // Crystal
14975
+ { name: "shards", signals: ["shard.yml"], command: "shards install" },
14976
+ // Perl
14977
+ { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
14978
+ // Zig
14979
+ { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
14980
+ // C / C++
14981
+ { name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
14982
+ { name: "conan", signals: ["conanfile.py", "conanfile.txt"], command: "conan install ." },
14983
+ // Nix
14984
+ { name: "nix", signals: ["flake.nix"], command: "nix develop" },
14985
+ { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
14986
+ // Terraform / OpenTofu
14987
+ { name: "terraform", signals: ["terraform.lock.hcl"], command: "terraform init" }
14988
+ ];
14989
+ async function detectPackageManager(repoRoot) {
14990
+ for (const entry of ENTRIES) {
14991
+ const matched = entry.glob ? await matchesGlob(repoRoot, entry.signals) : await matchesExact(repoRoot, entry.signals);
14992
+ if (matched) {
14993
+ return { name: entry.name, installCommand: entry.command };
14994
+ }
15020
14995
  }
14996
+ return null;
15021
14997
  }
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");
14998
+ async function matchesExact(repoRoot, signals) {
14999
+ for (const signal of signals) {
15000
+ try {
15001
+ await access2(join5(repoRoot, signal));
15002
+ return true;
15003
+ } catch {
15004
+ }
15030
15005
  }
15006
+ return false;
15031
15007
  }
15032
- async function readExistingConfig(path9) {
15008
+ async function matchesGlob(repoRoot, patterns) {
15009
+ let files;
15033
15010
  try {
15034
- return await readFile3(path9, "utf8");
15035
- } catch (error) {
15036
- if (isMissingFileError2(error)) {
15037
- return "";
15038
- }
15039
- throw error;
15011
+ files = await readdir(repoRoot);
15012
+ } catch {
15013
+ return false;
15040
15014
  }
15015
+ const regexes = patterns.map(patternToRegex);
15016
+ return files.some((file) => regexes.some((re) => re.test(file)));
15041
15017
  }
15042
- function ensureTrailingNewline(value) {
15043
- return value.endsWith("\n") ? value : `${value}
15044
- `;
15045
- }
15046
- function escapeForRegExp(value) {
15047
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15048
- }
15049
- function isMissingFileError2(error) {
15050
- return error instanceof Error && "code" in error && error.code === "ENOENT";
15051
- }
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`;
15018
+ function patternToRegex(pattern) {
15019
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
15020
+ return new RegExp(`^${escaped}$`);
15074
15021
  }
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
15022
 
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");
15097
- }
15098
- async function defaultPromptForSetup() {
15099
- Ie("gji setup");
15100
- const installSaveTarget = await ve({
15101
- message: "Where should preferences be saved?",
15102
- 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" }
15105
- ]
15106
- });
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;
15023
+ // src/install-prompt.ts
15024
+ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
15025
+ if (isHeadless() || nonInteractive) {
15026
+ return;
15118
15027
  }
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;
15028
+ const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15029
+ if (isConfiguredHookCommand(hooks?.afterCreate)) {
15030
+ return;
15126
15031
  }
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;
15032
+ if (config.skipInstallPrompt === true) {
15033
+ return;
15134
15034
  }
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;
15035
+ const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
15036
+ const pm = await detect(worktreePath);
15037
+ if (!pm) {
15038
+ return;
15142
15039
  }
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.");
15149
- return null;
15040
+ const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
15041
+ const choice = await prompt(pm);
15042
+ if (!choice || choice === "no") {
15043
+ return;
15150
15044
  }
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;
15173
- }
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;
15197
- }
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");
15220
- }
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}`);
15234
- }
15235
- return lines.join("\n");
15236
- }
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);
15242
- });
15243
- }
15244
-
15245
- // 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}`);
15258
- }
15259
- const normalized = normalize(pattern);
15260
- if (normalized.startsWith("..")) {
15261
- throw new Error(`syncFiles: pattern must not contain '..' segments, got: ${pattern}`);
15262
- }
15263
- const sourcePath = join5(mainRoot, normalized);
15264
- const destPath = join5(targetPath, normalized);
15265
- const sourceExists = await fileExists(sourcePath);
15266
- if (!sourceExists) {
15267
- continue;
15268
- }
15269
- const destExists = await fileExists(destPath);
15270
- if (destExists) {
15271
- continue;
15272
- }
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;
15284
- }
15285
- throw error;
15286
- }
15287
- }
15288
- function isNotFoundError(error) {
15289
- return error instanceof Error && "code" in error && error.code === "ENOENT";
15290
- }
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 };
15369
- }
15370
- }
15371
- return null;
15372
- }
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 {
15379
- }
15380
- }
15381
- return false;
15382
- }
15383
- async function matchesGlob(repoRoot, patterns) {
15384
- let files;
15385
- try {
15386
- files = await readdir(repoRoot);
15387
- } catch {
15388
- return false;
15389
- }
15390
- const regexes = patterns.map(patternToRegex);
15391
- return files.some((file) => regexes.some((re) => re.test(file)));
15392
- }
15393
- function patternToRegex(pattern) {
15394
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
15395
- return new RegExp(`^${escaped}$`);
15396
- }
15397
-
15398
- // src/install-prompt.ts
15399
- async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
15400
- if (isHeadless() || nonInteractive) {
15401
- return;
15402
- }
15403
- const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15404
- if (typeof hooks?.afterCreate === "string" && hooks.afterCreate.length > 0) {
15405
- return;
15406
- }
15407
- if (config.skipInstallPrompt === true) {
15408
- return;
15409
- }
15410
- const detect = dependencies.detectInstallPackageManager ?? detectPackageManager;
15411
- const pm = await detect(worktreePath);
15412
- if (!pm) {
15413
- return;
15414
- }
15415
- const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
15416
- const choice = await prompt(pm);
15417
- if (!choice || choice === "no") {
15418
- return;
15419
- }
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
- `);
15427
- }
15045
+ if (choice === "yes" || choice === "always") {
15046
+ const runner = dependencies.runInstallCommand ?? defaultRunInstallCommand;
15047
+ try {
15048
+ await runner(pm.installCommand, worktreePath, stderr);
15049
+ } catch (error) {
15050
+ stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}
15051
+ `);
15052
+ }
15428
15053
  }
15429
15054
  const saveGlobal = config.installSaveTarget === "global";
15430
15055
  const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
@@ -15458,8 +15083,8 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15458
15083
  }
15459
15084
  }
15460
15085
  async function defaultRunInstallCommand(command, cwd, stderr) {
15461
- await new Promise((resolve4, reject) => {
15462
- const child = spawn3(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15086
+ await new Promise((resolve5, reject) => {
15087
+ const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15463
15088
  child.stderr.on("data", (chunk) => {
15464
15089
  stderr(chunk.toString());
15465
15090
  });
@@ -15467,7 +15092,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
15467
15092
  if (code !== 0) {
15468
15093
  reject(new Error(`exited with code ${code}`));
15469
15094
  } else {
15470
- resolve4();
15095
+ resolve5();
15471
15096
  }
15472
15097
  });
15473
15098
  child.on("error", (err) => {
@@ -15505,6 +15130,10 @@ async function defaultPromptForInstallChoice(pm) {
15505
15130
  function isPlainObject2(value) {
15506
15131
  return typeof value === "object" && value !== null && !Array.isArray(value);
15507
15132
  }
15133
+ function isConfiguredHookCommand(value) {
15134
+ if (typeof value === "string") return value.length > 0;
15135
+ return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15136
+ }
15508
15137
 
15509
15138
  // src/conflict.ts
15510
15139
  import { access as access3 } from "node:fs/promises";
@@ -15538,10 +15167,14 @@ function createNewCommand(dependencies = {}) {
15538
15167
  const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
15539
15168
  const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
15540
15169
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
15170
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15541
15171
  return async function runNewCommand2(options) {
15542
15172
  const repository = await detectRepository(options.cwd);
15543
15173
  const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15544
15174
  const usesGeneratedDetachedName = options.detached && options.branch === void 0;
15175
+ if (options.editor && !options.open) {
15176
+ options.stderr("gji new: --editor has no effect without --open\n");
15177
+ }
15545
15178
  if (!options.detached && !options.branch && (options.json || isHeadless())) {
15546
15179
  const message = "branch argument is required";
15547
15180
  if (options.json) {
@@ -15635,12 +15268,14 @@ function createNewCommand(dependencies = {}) {
15635
15268
  options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath, dryRun: true }, null, 2)}
15636
15269
  `);
15637
15270
  } else {
15638
- options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName})
15271
+ const resolvedEditor = options.open ? options.editor ?? resolveConfigString(config, "editor") : void 0;
15272
+ const openNote = resolvedEditor ? `, then open in ${resolvedEditor}` : "";
15273
+ options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})
15639
15274
  `);
15640
15275
  }
15641
15276
  return 0;
15642
15277
  }
15643
- await mkdir5(dirname6(worktreePath), { recursive: true });
15278
+ await mkdir4(dirname5(worktreePath), { recursive: true });
15644
15279
  const gitArgs = options.detached ? ["worktree", "add", "--detach", worktreePath] : await localBranchExists(repository.repoRoot, worktreeName) ? ["worktree", "add", worktreePath, worktreeName] : ["worktree", "add", "-b", worktreeName, worktreePath];
15645
15280
  await execFileAsync3("git", gitArgs, { cwd: repository.repoRoot });
15646
15281
  const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
@@ -15657,7 +15292,7 @@ function createNewCommand(dependencies = {}) {
15657
15292
  await runHook(
15658
15293
  hooks.afterCreate,
15659
15294
  worktreePath,
15660
- { branch: worktreeName, path: worktreePath, repo: basename4(repository.repoRoot) },
15295
+ { branch: worktreeName, path: worktreePath, repo: basename3(repository.repoRoot) },
15661
15296
  options.stderr
15662
15297
  );
15663
15298
  if (options.json) {
@@ -15667,119 +15302,994 @@ function createNewCommand(dependencies = {}) {
15667
15302
  await appendHistory(worktreePath, worktreeName);
15668
15303
  await writeOutput(worktreePath, options.stdout);
15669
15304
  }
15305
+ if (options.open) {
15306
+ const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
15307
+ await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
15308
+ }
15670
15309
  return 0;
15671
15310
  };
15672
15311
  }
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)}`;
15312
+ var runNewCommand = createNewCommand();
15313
+ function generateBranchPlaceholder(random = Math.random) {
15314
+ const roots = [
15315
+ "socrates",
15316
+ "prometheus",
15317
+ "beethoven",
15318
+ "ada",
15319
+ "turing",
15320
+ "hypatia",
15321
+ "tesla",
15322
+ "curie",
15323
+ "diogenes",
15324
+ "plato",
15325
+ "hephaestus",
15326
+ "athena",
15327
+ "archimedes",
15328
+ "euclid",
15329
+ "heraclitus",
15330
+ "galileo",
15331
+ "newton",
15332
+ "lovelace",
15333
+ "nietzsche",
15334
+ "kafka"
15335
+ ];
15336
+ const antics = [
15337
+ "borrowed-a-bike",
15338
+ "brought-snacks",
15339
+ "missed-the-bus",
15340
+ "lost-the-keys",
15341
+ "spilled-the-coffee",
15342
+ "forgot-the-umbrella",
15343
+ "walked-the-dog",
15344
+ "missed-the-train",
15345
+ "wrote-a-poem",
15346
+ "burned-the-toast",
15347
+ "fed-the-pigeons",
15348
+ "watered-the-plants",
15349
+ "washed-the-dishes",
15350
+ "folded-the-laundry",
15351
+ "took-a-nap"
15352
+ ];
15353
+ return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
15354
+ }
15355
+ function applyConfiguredBranchPrefix(branch, branchPrefix) {
15356
+ if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
15357
+ return branch;
15358
+ }
15359
+ if (branch.startsWith(branchPrefix)) {
15360
+ return branch;
15361
+ }
15362
+ return `${branchPrefix}${branch}`;
15363
+ }
15364
+ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
15365
+ let attempt = 1;
15366
+ while (true) {
15367
+ const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
15368
+ const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
15369
+ if (!await pathExists(candidatePath)) {
15370
+ return candidatePath;
15371
+ }
15372
+ attempt += 1;
15373
+ }
15374
+ }
15375
+ async function defaultPromptForBranch(placeholder) {
15376
+ const choice = await he({
15377
+ defaultValue: placeholder,
15378
+ message: "Name the new branch",
15379
+ placeholder,
15380
+ validate: (value) => {
15381
+ const trimmed = value.trim();
15382
+ return validateBranchName(trimmed) ?? void 0;
15383
+ }
15384
+ });
15385
+ if (pD(choice)) {
15386
+ return null;
15387
+ }
15388
+ return choice.trim();
15389
+ }
15390
+ function pickRandom(values, random) {
15391
+ const index = Math.floor(random() * values.length);
15392
+ return values[Math.min(index, values.length - 1)];
15393
+ }
15394
+ async function localBranchExists(repoRoot, branchName) {
15395
+ try {
15396
+ await execFileAsync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
15397
+ return true;
15398
+ } catch {
15399
+ return false;
15400
+ }
15401
+ }
15402
+ async function writeOutput(worktreePath, stdout) {
15403
+ await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
15404
+ }
15405
+ function isNotRegisteredWorktreeError(error) {
15406
+ const stderr = hasExecStderr(error) ? error.stderr : String(error);
15407
+ return stderr.includes("is not a working tree") || stderr.includes("not a linked working tree");
15408
+ }
15409
+ function hasExecStderr(error) {
15410
+ return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
15411
+ }
15412
+ function toExecMessage(error) {
15413
+ return hasExecStderr(error) ? error.stderr.trim() : String(error);
15414
+ }
15415
+ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15416
+ if (!editorCli) {
15417
+ stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
15418
+ return;
15419
+ }
15420
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15421
+ const args = [];
15422
+ if (editorDef?.newWindowFlag) {
15423
+ args.push(editorDef.newWindowFlag);
15424
+ }
15425
+ args.push(worktreePath);
15426
+ try {
15427
+ await spawnFn(editorCli, args);
15428
+ } catch (error) {
15429
+ const message = error instanceof Error ? error.message : String(error);
15430
+ stderr(`gji new: failed to open editor: ${message}
15431
+ `);
15432
+ }
15433
+ }
15434
+
15435
+ // src/repo-registry.ts
15436
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
15437
+ import { homedir as homedir4 } from "node:os";
15438
+ import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
15439
+ var REGISTRY_FILE_NAME = "repos.json";
15440
+ var MAX_REGISTRY_ENTRIES = 100;
15441
+ function REGISTRY_FILE_PATH(home = homedir4()) {
15442
+ const configDir = process.env.GJI_CONFIG_DIR;
15443
+ if (configDir) {
15444
+ return join6(resolve4(configDir), REGISTRY_FILE_NAME);
15445
+ }
15446
+ return join6(home, GLOBAL_CONFIG_DIRECTORY, REGISTRY_FILE_NAME);
15447
+ }
15448
+ async function loadRegistry(home = homedir4()) {
15449
+ const path9 = REGISTRY_FILE_PATH(home);
15450
+ try {
15451
+ const raw = await readFile3(path9, "utf8");
15452
+ const parsed = JSON.parse(raw);
15453
+ if (!Array.isArray(parsed)) return [];
15454
+ return parsed.filter(isRegistryEntry);
15455
+ } catch {
15456
+ return [];
15457
+ }
15458
+ }
15459
+ async function registerRepo(repoPath, home = homedir4()) {
15460
+ const registryPath = REGISTRY_FILE_PATH(home);
15461
+ const existing = await loadRegistry(home);
15462
+ if (existing.length > 0 && existing[0].path === repoPath) return;
15463
+ const entry = {
15464
+ lastUsed: Date.now(),
15465
+ name: basename4(repoPath),
15466
+ path: repoPath
15467
+ };
15468
+ const filtered = existing.filter((e2) => e2.path !== repoPath);
15469
+ const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
15470
+ await mkdir5(dirname6(registryPath), { recursive: true });
15471
+ await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
15472
+ `, "utf8");
15473
+ }
15474
+ function isRegistryEntry(value) {
15475
+ 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";
15476
+ }
15477
+
15478
+ // src/warp.ts
15479
+ var WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
15480
+ async function runWarpCommand(options) {
15481
+ if (options.newWorktree) {
15482
+ const registry = await loadRegistry();
15483
+ if (registry.length === 0) {
15484
+ options.stderr(
15485
+ "gji warp: no repos registered yet.\nUse any gji command in a repository to register it automatically.\n"
15486
+ );
15487
+ return 1;
15488
+ }
15489
+ return runWarpNew(options, registry);
15490
+ }
15491
+ return runWarpNavigate(options);
15492
+ }
15493
+ async function runWarpNavigate(options) {
15494
+ if ((isHeadless() || options.json) && !options.branch) {
15495
+ const message = "branch argument is required";
15496
+ if (options.json) {
15497
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15498
+ `);
15499
+ } else {
15500
+ options.stderr(
15501
+ "gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15502
+ );
15503
+ }
15504
+ return 1;
15505
+ }
15506
+ const target = await resolveWarpTarget({ ...options, commandName: "gji warp", json: options.json });
15507
+ if (!target) return 1;
15508
+ if (options.json) {
15509
+ options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}
15510
+ `);
15511
+ return 0;
15512
+ }
15513
+ appendHistory(target.path, target.branch).catch(() => void 0);
15514
+ await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
15515
+ return 0;
15516
+ }
15517
+ async function runWarpNew(options, registry) {
15518
+ let targetRepoRoot;
15519
+ if (registry.length === 1) {
15520
+ targetRepoRoot = registry[0].path;
15521
+ } else {
15522
+ if (isHeadless()) {
15523
+ options.stderr(
15524
+ "gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15525
+ );
15526
+ return 1;
15527
+ }
15528
+ const choice = await ve({
15529
+ message: "Create worktree in which repo?",
15530
+ options: registry.map((entry) => ({
15531
+ value: entry.path,
15532
+ label: entry.name,
15533
+ hint: entry.path
15534
+ }))
15535
+ });
15536
+ if (pD(choice)) {
15537
+ options.stderr("Aborted\n");
15538
+ return 1;
15539
+ }
15540
+ targetRepoRoot = choice;
15541
+ }
15542
+ if (options.json) {
15543
+ return runNewCommand({
15544
+ branch: options.branch,
15545
+ cwd: targetRepoRoot,
15546
+ json: true,
15547
+ stderr: options.stderr,
15548
+ stdout: options.stdout
15549
+ });
15550
+ }
15551
+ let capturedPath = "";
15552
+ const captureStdout = (chunk) => {
15553
+ capturedPath = chunk.trim();
15554
+ };
15555
+ const exitCode = await runNewCommand({
15556
+ branch: options.branch,
15557
+ cwd: targetRepoRoot,
15558
+ stderr: options.stderr,
15559
+ stdout: captureStdout
15560
+ });
15561
+ if (exitCode !== 0) {
15562
+ return exitCode;
15563
+ }
15564
+ if (!capturedPath) {
15565
+ options.stderr("gji warp: could not determine new worktree path\n");
15566
+ return 1;
15567
+ }
15568
+ await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
15569
+ return 0;
15570
+ }
15571
+ function findByQuery(items, query) {
15572
+ const slashIdx = query.indexOf("/");
15573
+ if (slashIdx !== -1) {
15574
+ const repoQuery = query.slice(0, slashIdx);
15575
+ const branchQuery = query.slice(slashIdx + 1);
15576
+ const match = items.find(
15577
+ (item) => item.repoName === repoQuery && item.worktree.branch === branchQuery
15578
+ );
15579
+ if (match) return match;
15580
+ }
15581
+ return items.find((item) => item.worktree.branch === query) ?? null;
15582
+ }
15583
+ async function resolveWarpTarget(options) {
15584
+ const cmd = options.commandName ?? "gji";
15585
+ const emitError4 = (message, hint) => {
15586
+ if (options.json) {
15587
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
15588
+ `);
15589
+ } else {
15590
+ options.stderr(`${cmd}: ${message}
15591
+ `);
15592
+ if (hint) options.stderr(hint);
15593
+ }
15594
+ };
15595
+ const registry = await loadRegistry();
15596
+ if (registry.length === 0) {
15597
+ emitError4(
15598
+ "not in a git repository and no repos registered yet.",
15599
+ "Use any gji command inside a repository to register it.\n"
15600
+ );
15601
+ return null;
15602
+ }
15603
+ const results = await Promise.allSettled(
15604
+ registry.map(async (entry) => {
15605
+ const worktrees = await listWorktrees(entry.path);
15606
+ return { repoName: entry.name, worktrees };
15607
+ })
15608
+ );
15609
+ const allItems = [];
15610
+ for (const result of results) {
15611
+ if (result.status === "rejected") continue;
15612
+ const { repoName, worktrees } = result.value;
15613
+ for (const worktree of worktrees) {
15614
+ allItems.push({ repoName, worktree });
15615
+ }
15616
+ }
15617
+ if (allItems.length === 0) {
15618
+ emitError4("no accessible worktrees found in any registered repo.");
15619
+ return null;
15620
+ }
15621
+ if (options.branch) {
15622
+ const match = findByQuery(allItems, options.branch);
15623
+ if (!match) {
15624
+ emitError4(`no worktree found matching: ${options.branch}`);
15625
+ return null;
15626
+ }
15627
+ return { branch: match.worktree.branch, path: match.worktree.path };
15628
+ }
15629
+ const path9 = await promptForWarpTarget(allItems);
15630
+ if (!path9) {
15631
+ options.stderr("Aborted\n");
15632
+ return null;
15633
+ }
15634
+ const chosen = allItems.find((item) => item.worktree.path === path9);
15635
+ return { branch: chosen?.worktree.branch ?? null, path: path9 };
15636
+ }
15637
+ async function promptForWarpTarget(items) {
15638
+ const healthResults = await Promise.allSettled(
15639
+ items.map((item) => readWorktreeHealth(item.worktree.path))
15640
+ );
15641
+ const choice = await ve({
15642
+ message: "Warp to a worktree",
15643
+ options: items.map((item, i) => {
15644
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15645
+ const upstream = health ? formatHint(item.worktree.branch, health) : null;
15646
+ const label = `${item.repoName} \u203A ${item.worktree.branch ?? "(detached)"}`;
15647
+ const pathHint = item.worktree.isCurrent ? `${item.worktree.path} (current)` : item.worktree.path;
15648
+ const hint = upstream ? `${upstream} \xB7 ${pathHint}` : pathHint;
15649
+ return { hint, label, value: item.worktree.path };
15650
+ })
15651
+ });
15652
+ if (pD(choice)) {
15653
+ return null;
15654
+ }
15655
+ return choice;
15656
+ }
15657
+ function formatHint(branch, health) {
15658
+ if (branch === null) return null;
15659
+ if (!health.hasUpstream) return "no upstream";
15660
+ if (health.upstreamGone) return "upstream gone";
15661
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
15662
+ if (health.ahead === 0) return `behind ${health.behind}`;
15663
+ if (health.behind === 0) return `ahead ${health.ahead}`;
15664
+ return `ahead ${health.ahead}, behind ${health.behind}`;
15665
+ }
15666
+
15667
+ // src/go.ts
15668
+ var GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
15669
+ function createGoCommand(dependencies = {}) {
15670
+ const prompt = dependencies.promptForWorktree ?? promptForWorktree;
15671
+ return async function runGoCommand2(options) {
15672
+ let worktrees;
15673
+ let repository;
15674
+ try {
15675
+ [worktrees, repository] = await Promise.all([
15676
+ listWorktrees(options.cwd),
15677
+ detectRepository(options.cwd)
15678
+ ]);
15679
+ } catch {
15680
+ if (isHeadless() && !options.branch) {
15681
+ options.stderr(
15682
+ "gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
15683
+ );
15684
+ return 1;
15685
+ }
15686
+ const target = await resolveWarpTarget({ ...options, commandName: "gji go" });
15687
+ if (!target) return 1;
15688
+ appendHistory(target.path, target.branch).catch(() => void 0);
15689
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
15690
+ return 0;
15691
+ }
15692
+ if (!options.branch && isHeadless()) {
15693
+ options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
15694
+ return 1;
15695
+ }
15696
+ const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
15697
+ const resolvedPath = options.branch ? worktrees.find((entry) => entry.branch === options.branch)?.path : prompted ?? void 0;
15698
+ if (!resolvedPath) {
15699
+ if (options.branch) {
15700
+ options.stderr(`No worktree found for branch: ${options.branch}
15701
+ `);
15702
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
15703
+ `);
15704
+ } else {
15705
+ options.stderr("Aborted\n");
15706
+ }
15707
+ return 1;
15708
+ }
15709
+ const chosenWorktree = worktrees.find((w2) => w2.path === resolvedPath);
15710
+ const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15711
+ const hooks = extractHooks(config);
15712
+ await runHook(
15713
+ hooks.afterEnter,
15714
+ resolvedPath,
15715
+ { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename5(repository.repoRoot) },
15716
+ options.stderr
15717
+ );
15718
+ appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
15719
+ await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
15720
+ return 0;
15721
+ };
15722
+ }
15723
+ var runGoCommand = createGoCommand();
15724
+ async function promptForWorktree(worktrees) {
15725
+ const healthResults = await Promise.allSettled(
15726
+ worktrees.map((w2) => readWorktreeHealth(w2.path))
15727
+ );
15728
+ const choice = await ve({
15729
+ message: "Choose a worktree",
15730
+ options: worktrees.map((worktree, i) => {
15731
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
15732
+ const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
15733
+ const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
15734
+ return {
15735
+ value: worktree.path,
15736
+ label: worktree.branch ?? "(detached)",
15737
+ hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
15738
+ };
15739
+ })
15740
+ });
15741
+ if (pD(choice)) {
15742
+ return null;
15743
+ }
15744
+ return choice;
15745
+ }
15746
+ function formatUpstreamHint(branch, health) {
15747
+ if (branch === null) return null;
15748
+ if (!health.hasUpstream) return "no upstream";
15749
+ if (health.upstreamGone) return "upstream gone";
15750
+ if (health.ahead === 0 && health.behind === 0) return "up to date";
15751
+ if (health.ahead === 0) return `behind ${health.behind}`;
15752
+ if (health.behind === 0) return `ahead ${health.ahead}`;
15753
+ return `ahead ${health.ahead}, behind ${health.behind}`;
15754
+ }
15755
+
15756
+ // src/open.ts
15757
+ import { execFile as execFile4 } from "node:child_process";
15758
+ import { access as access4, writeFile as writeFile5 } from "node:fs/promises";
15759
+ import { join as join7 } from "node:path";
15760
+ import { promisify as promisify5 } from "node:util";
15761
+ var execFileAsync4 = promisify5(execFile4);
15762
+ function createOpenCommand(dependencies = {}) {
15763
+ const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
15764
+ const promptForEditor = dependencies.promptForEditor ?? defaultPromptForEditor;
15765
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
15766
+ const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
15767
+ return async function runOpenCommand2(options) {
15768
+ const [worktrees, repository] = await Promise.all([
15769
+ listWorktrees(options.cwd),
15770
+ detectRepository(options.cwd)
15771
+ ]);
15772
+ let targetPath;
15773
+ if (options.branch) {
15774
+ const entry = worktrees.find((w2) => w2.branch === options.branch);
15775
+ if (!entry) {
15776
+ options.stderr(`gji open: no worktree found for branch: ${options.branch}
15777
+ `);
15778
+ options.stderr(`Hint: Use 'gji ls' to see available worktrees
15779
+ `);
15780
+ return 1;
15781
+ }
15782
+ targetPath = entry.path;
15783
+ } else if (isHeadless()) {
15784
+ targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
15785
+ } else {
15786
+ const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
15787
+ if (!chosen) {
15788
+ options.stderr("Aborted\n");
15789
+ return 1;
15790
+ }
15791
+ targetPath = chosen;
15792
+ }
15793
+ const config = await loadEffectiveConfig(repository.repoRoot, void 0, options.stderr);
15794
+ const savedEditor = resolveConfigString(config, "editor");
15795
+ let editorCli;
15796
+ if (options.editor) {
15797
+ editorCli = options.editor;
15798
+ } else if (savedEditor) {
15799
+ editorCli = savedEditor;
15800
+ } else {
15801
+ const installed = await detectEditors();
15802
+ if (installed.length === 0) {
15803
+ options.stderr(
15804
+ "gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n"
15805
+ );
15806
+ return 1;
15807
+ }
15808
+ if (installed.length === 1 || isHeadless()) {
15809
+ editorCli = installed[0].cli;
15810
+ } else {
15811
+ const chosen = await promptForEditor(installed);
15812
+ if (!chosen) {
15813
+ options.stderr("Aborted\n");
15814
+ return 1;
15815
+ }
15816
+ editorCli = chosen;
15817
+ }
15818
+ }
15819
+ if (options.save && editorCli !== savedEditor) {
15820
+ await updateGlobalConfigKey("editor", editorCli);
15821
+ const displayName2 = EDITORS.find((e2) => e2.cli === editorCli)?.name ?? editorCli;
15822
+ options.stdout(`Saved editor "${displayName2}" to global config
15823
+ `);
15824
+ }
15825
+ const editorDef = EDITORS.find((e2) => e2.cli === editorCli);
15826
+ let openTarget = targetPath;
15827
+ if (options.workspace) {
15828
+ if (editorDef?.supportsWorkspace) {
15829
+ openTarget = await ensureWorkspaceFile(targetPath, repository.repoName);
15830
+ } else {
15831
+ const displayName2 = editorDef?.name ?? editorCli;
15832
+ options.stderr(`gji open: --workspace is not supported for ${displayName2}, ignoring
15833
+ `);
15834
+ }
15835
+ }
15836
+ const args = [];
15837
+ if (editorDef?.newWindowFlag) {
15838
+ args.push(editorDef.newWindowFlag);
15839
+ }
15840
+ args.push(openTarget);
15841
+ try {
15842
+ await spawnEditor(editorCli, args);
15843
+ } catch (error) {
15844
+ const message = error instanceof Error ? error.message : String(error);
15845
+ options.stderr(`gji open: failed to launch editor: ${message}
15846
+ `);
15847
+ return 1;
15848
+ }
15849
+ const displayName = editorDef?.name ?? editorCli;
15850
+ options.stdout(`Opened ${targetPath} in ${displayName}
15851
+ `);
15852
+ return 0;
15853
+ };
15854
+ }
15855
+ var runOpenCommand = createOpenCommand();
15856
+ async function detectInstalledEditors() {
15857
+ const results = await Promise.all(
15858
+ EDITORS.map(async (editor) => ({ editor, available: await isCommandAvailable(editor.cli) }))
15859
+ );
15860
+ return results.filter((r2) => r2.available).map((r2) => r2.editor);
15861
+ }
15862
+ async function isCommandAvailable(command) {
15863
+ try {
15864
+ await execFileAsync4("which", [command]);
15865
+ return true;
15866
+ } catch {
15867
+ return false;
15868
+ }
15869
+ }
15870
+ async function defaultPromptForWorktree(worktrees) {
15871
+ const choice = await ve({
15872
+ message: "Choose a worktree to open",
15873
+ options: worktrees.map((w2) => ({
15874
+ value: w2.path,
15875
+ label: w2.branch ?? "(detached)",
15876
+ hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
15877
+ }))
15878
+ });
15879
+ if (pD(choice)) return null;
15880
+ return choice;
15881
+ }
15882
+ async function defaultPromptForEditor(editors) {
15883
+ const choice = await ve({
15884
+ message: "Choose an editor",
15885
+ options: editors.map((e2) => ({ value: e2.cli, label: e2.name }))
15886
+ });
15887
+ if (pD(choice)) return null;
15888
+ return choice;
15889
+ }
15890
+ async function ensureWorkspaceFile(worktreePath, repoName) {
15891
+ const workspacePath = join7(worktreePath, `${repoName}.code-workspace`);
15892
+ try {
15893
+ await access4(workspacePath);
15894
+ return workspacePath;
15895
+ } catch {
15896
+ }
15897
+ const workspace = { folders: [{ path: "." }], settings: {} };
15898
+ await writeFile5(workspacePath, `${JSON.stringify(workspace, null, 2)}
15899
+ `, "utf8");
15900
+ return workspacePath;
15901
+ }
15902
+
15903
+ // src/init.ts
15904
+ import { mkdir as mkdir6, readFile as readFile4, writeFile as writeFile6 } from "node:fs/promises";
15905
+ import { homedir as homedir5 } from "node:os";
15906
+ import { dirname as dirname7, join as join8 } from "node:path";
15907
+ var START_MARKER = "# >>> gji init >>>";
15908
+ var END_MARKER = "# <<< gji init <<<";
15909
+ var SHELL_WRAPPED_COMMANDS = [
15910
+ {
15911
+ bypassOptions: ["--help"],
15912
+ commandName: "new",
15913
+ envVar: "GJI_NEW_OUTPUT_FILE",
15914
+ names: ["new"],
15915
+ tempPrefix: "gji-new"
15916
+ },
15917
+ {
15918
+ bypassOptions: ["--help"],
15919
+ commandName: "pr",
15920
+ envVar: "GJI_PR_OUTPUT_FILE",
15921
+ names: ["pr"],
15922
+ tempPrefix: "gji-pr"
15923
+ },
15924
+ {
15925
+ bypassOptions: ["--print"],
15926
+ commandName: "back",
15927
+ envVar: "GJI_BACK_OUTPUT_FILE",
15928
+ names: ["back"],
15929
+ tempPrefix: "gji-back"
15930
+ },
15931
+ {
15932
+ bypassOptions: ["--print"],
15933
+ commandName: "go",
15934
+ envVar: "GJI_GO_OUTPUT_FILE",
15935
+ names: ["go", "jump"],
15936
+ tempPrefix: "gji-go"
15937
+ },
15938
+ {
15939
+ bypassOptions: ["--print"],
15940
+ commandName: "root",
15941
+ envVar: "GJI_ROOT_OUTPUT_FILE",
15942
+ names: ["root"],
15943
+ tempPrefix: "gji-root"
15944
+ },
15945
+ {
15946
+ bypassOptions: ["--help"],
15947
+ commandName: "remove",
15948
+ envVar: "GJI_REMOVE_OUTPUT_FILE",
15949
+ names: ["remove", "rm"],
15950
+ tempPrefix: "gji-remove"
15951
+ },
15952
+ {
15953
+ bypassOptions: ["--print", "--json"],
15954
+ commandName: "warp",
15955
+ envVar: "GJI_WARP_OUTPUT_FILE",
15956
+ names: ["warp"],
15957
+ tempPrefix: "gji-warp"
15958
+ }
15959
+ ];
15960
+ async function runInitCommand(options) {
15961
+ const shell = resolveSupportedShell(options.shell, process.env.SHELL);
15962
+ const home = options.home ?? homedir5();
15963
+ if (!shell) {
15964
+ options.stderr?.(
15965
+ "Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n"
15966
+ );
15967
+ return 1;
15968
+ }
15969
+ const script = renderShellIntegration(shell);
15970
+ if (!options.write) {
15971
+ options.stdout(script);
15972
+ return 0;
15973
+ }
15974
+ const rcPath = resolveShellConfigPath(shell, home);
15975
+ await mkdir6(dirname7(rcPath), { recursive: true });
15976
+ const current = await readExistingConfig(rcPath);
15977
+ const next = upsertShellIntegration(current, script);
15978
+ await writeFile6(rcPath, next, "utf8");
15979
+ options.stdout(`${rcPath}
15980
+ `);
15981
+ const { config: globalConfig } = await loadGlobalConfig(home);
15982
+ const alreadyConfigured = "shellIntegration" in globalConfig || "installSaveTarget" in globalConfig;
15983
+ const hasCustomPrompt = options.promptForSetup !== void 0;
15984
+ const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
15985
+ if (!alreadyConfigured && canPrompt) {
15986
+ const prompt = options.promptForSetup ?? defaultPromptForSetup;
15987
+ const result = await prompt();
15988
+ if (result) {
15989
+ await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
15990
+ await saveWizardConfig(result, options.cwd, home);
15991
+ }
15992
+ }
15993
+ await updateGlobalConfigKey("shellIntegration", true, home);
15994
+ return 0;
15995
+ }
15996
+ function renderShellIntegration(shell) {
15997
+ const commandBlocks = SHELL_WRAPPED_COMMANDS.map(
15998
+ (command) => shell === "fish" ? renderFishWrapper(command) : renderPosixWrapper(command)
15999
+ ).join("\n\n");
16000
+ switch (shell) {
16001
+ case "fish":
16002
+ return `${START_MARKER}
16003
+ function gji --wraps gji --description 'gji shell integration'
16004
+ ${indentBlock(commandBlocks, 4)}
16005
+
16006
+ command gji $argv
16007
+ end
16008
+ ${END_MARKER}
16009
+ `;
16010
+ case "bash":
16011
+ case "zsh":
16012
+ return `${START_MARKER}
16013
+ gji() {
16014
+ ${indentBlock(commandBlocks, 2)}
16015
+
16016
+ command gji "$@"
16017
+ }
16018
+ ${END_MARKER}
16019
+ `;
16020
+ }
16021
+ }
16022
+ function upsertShellIntegration(existingConfig, script) {
16023
+ const trimmedScript = script.trimEnd();
16024
+ const blockPattern = new RegExp(
16025
+ `${escapeForRegExp(START_MARKER)}[\\s\\S]*?${escapeForRegExp(END_MARKER)}\\n?`,
16026
+ "m"
16027
+ );
16028
+ if (blockPattern.test(existingConfig)) {
16029
+ return ensureTrailingNewline(
16030
+ existingConfig.replace(blockPattern, `${trimmedScript}
16031
+ `)
16032
+ );
16033
+ }
16034
+ const prefix = existingConfig.trimEnd();
16035
+ if (prefix.length === 0) {
16036
+ return ensureTrailingNewline(trimmedScript);
16037
+ }
16038
+ return ensureTrailingNewline(`${prefix}
16039
+
16040
+ ${trimmedScript}`);
16041
+ }
16042
+ async function saveWizardConfig(result, cwd, home) {
16043
+ const values = {};
16044
+ if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
16045
+ if (result.worktreePath) values.worktreePath = result.worktreePath;
16046
+ const hooks = {};
16047
+ if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16048
+ if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16049
+ if (result.hooks?.beforeRemove) hooks.beforeRemove = result.hooks.beforeRemove;
16050
+ if (Object.keys(hooks).length > 0) values.hooks = hooks;
16051
+ if (Object.keys(values).length === 0) return;
16052
+ if (result.installSaveTarget === "local") {
16053
+ const loaded = await loadConfig(cwd);
16054
+ await saveLocalConfig(cwd, { ...loaded.config, ...values });
16055
+ } else {
16056
+ const { config: existing } = await loadGlobalConfig(home);
16057
+ await saveGlobalConfig({ ...existing, ...values }, home);
16058
+ }
16059
+ }
16060
+ function resolveShellConfigPath(shell, home) {
16061
+ switch (shell) {
16062
+ case "bash":
16063
+ return join8(home, ".bashrc");
16064
+ case "fish":
16065
+ return join8(home, ".config", "fish", "config.fish");
16066
+ case "zsh":
16067
+ return join8(home, ".zshrc");
16068
+ }
16069
+ }
16070
+ async function readExistingConfig(path9) {
16071
+ try {
16072
+ return await readFile4(path9, "utf8");
16073
+ } catch (error) {
16074
+ if (isMissingFileError2(error)) {
16075
+ return "";
16076
+ }
16077
+ throw error;
16078
+ }
16079
+ }
16080
+ function ensureTrailingNewline(value) {
16081
+ return value.endsWith("\n") ? value : `${value}
16082
+ `;
16083
+ }
16084
+ function escapeForRegExp(value) {
16085
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16086
+ }
16087
+ function isMissingFileError2(error) {
16088
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
16089
+ }
16090
+ function renderFishWrapper(command) {
16091
+ const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
16092
+ const nameCondition = nameTests.length === 1 ? nameTests[0] : `begin; ${nameTests.join("; or ")}; end`;
16093
+ const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
16094
+ const bypassCondition = bypassTests.length === 1 ? bypassTests[0] : `begin; ${bypassTests.join("; or ")}; end`;
16095
+ return `if test (count $argv) -gt 0; and ${nameCondition}
16096
+ set -e argv[1]
16097
+ if test (count $argv) -gt 0; and ${bypassCondition}
16098
+ command gji ${command.commandName} $argv
16099
+ return $status
16100
+ end
16101
+
16102
+ set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
16103
+ or return 1
16104
+ env ${command.envVar}=$output_file command gji ${command.commandName} $argv
16105
+ or begin
16106
+ set -l status_code $status
16107
+ rm -f $output_file
16108
+ return $status_code
16109
+ end
16110
+ set -l target (cat $output_file)
16111
+ rm -f $output_file
16112
+ cd $target
16113
+ return $status
16114
+ end`;
16115
+ }
16116
+ function renderPosixWrapper(command) {
16117
+ const tests = command.names.map((name) => `[ "$1" = "${name}" ]`).join(" || ");
16118
+ const bypassTests = command.bypassOptions.map((opt) => `[ "\${1:-}" = "${opt}" ]`).join(" || ");
16119
+ return `if ${tests}; then
16120
+ shift
16121
+ if ${bypassTests}; then
16122
+ command gji ${command.commandName} "$@"
16123
+ return $?
16124
+ fi
16125
+
16126
+ local target
16127
+ local output_file
16128
+ output_file="$(mktemp -t ${command.tempPrefix}.XXXXXX)" || return 1
16129
+ ${command.envVar}="$output_file" command gji ${command.commandName} "$@" || { local exit_code=$?; rm -f "$output_file"; return $exit_code; }
16130
+ target="$(cat "$output_file")"
16131
+ rm -f "$output_file"
16132
+ cd "$target" || return $?
16133
+ return 0
16134
+ fi`;
16135
+ }
16136
+ function indentBlock(value, spaces) {
16137
+ const prefix = " ".repeat(spaces);
16138
+ return value.split("\n").map((line) => line.length === 0 ? "" : `${prefix}${line}`).join("\n");
15715
16139
  }
15716
- function applyConfiguredBranchPrefix(branch, branchPrefix) {
15717
- if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
15718
- return branch;
16140
+ async function defaultPromptForSetup() {
16141
+ Ie("gji setup");
16142
+ const installSaveTarget = await ve({
16143
+ message: "Where should preferences be saved?",
16144
+ options: [
16145
+ { value: "global", label: "~/.config/gji/config.json", hint: "personal \u2014 never committed" },
16146
+ { value: "local", label: ".gji.json", hint: "repo \u2014 committed with the project" }
16147
+ ]
16148
+ });
16149
+ if (pD(installSaveTarget)) {
16150
+ Se("Setup skipped.");
16151
+ return null;
15719
16152
  }
15720
- if (branch.startsWith(branchPrefix)) {
15721
- return branch;
16153
+ const branchPrefix = await he({
16154
+ message: "Default branch prefix?",
16155
+ placeholder: "e.g. feat/ or fix/ \u2014 leave blank to skip"
16156
+ });
16157
+ if (pD(branchPrefix)) {
16158
+ Se("Setup skipped.");
16159
+ return null;
15722
16160
  }
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;
16161
+ const worktreePath = await he({
16162
+ message: "Worktree base path?",
16163
+ placeholder: "leave blank to use the default path"
16164
+ });
16165
+ if (pD(worktreePath)) {
16166
+ Se("Setup skipped.");
16167
+ return null;
15734
16168
  }
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
- }
16169
+ const afterCreate = await he({
16170
+ message: "afterCreate hook \u2014 run after creating a worktree?",
16171
+ placeholder: "e.g. pnpm install \u2014 leave blank to skip"
15745
16172
  });
15746
- if (pD(choice)) {
16173
+ if (pD(afterCreate)) {
16174
+ Se("Setup skipped.");
15747
16175
  return null;
15748
16176
  }
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)];
16177
+ const afterEnter = await he({
16178
+ message: "afterEnter hook \u2014 run after entering a worktree?",
16179
+ placeholder: "e.g. nvm use \u2014 leave blank to skip"
16180
+ });
16181
+ if (pD(afterEnter)) {
16182
+ Se("Setup skipped.");
16183
+ return null;
16184
+ }
16185
+ const beforeRemove = await he({
16186
+ message: "beforeRemove hook \u2014 run before removing a worktree?",
16187
+ placeholder: "leave blank to skip"
16188
+ });
16189
+ if (pD(beforeRemove)) {
16190
+ Se("Setup skipped.");
16191
+ return null;
16192
+ }
16193
+ Se("Setup complete!");
16194
+ const hooks = {};
16195
+ if (afterCreate) hooks.afterCreate = afterCreate;
16196
+ if (afterEnter) hooks.afterEnter = afterEnter;
16197
+ if (beforeRemove) hooks.beforeRemove = beforeRemove;
16198
+ return {
16199
+ branchPrefix: branchPrefix || void 0,
16200
+ hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
16201
+ installSaveTarget,
16202
+ worktreePath: worktreePath || void 0
16203
+ };
15754
16204
  }
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;
16205
+
16206
+ // src/paths.ts
16207
+ function comparePaths(left, right) {
16208
+ if (left < right) {
16209
+ return -1;
16210
+ }
16211
+ if (left > right) {
16212
+ return 1;
15761
16213
  }
16214
+ return 0;
15762
16215
  }
15763
- async function writeOutput(worktreePath, stdout) {
15764
- await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
16216
+
16217
+ // src/ls.ts
16218
+ async function runLsCommand(options) {
16219
+ const worktrees = sortWorktrees(await listWorktrees(options.cwd));
16220
+ if (options.compact) {
16221
+ if (options.json) {
16222
+ options.stdout(`${JSON.stringify(worktrees, null, 2)}
16223
+ `);
16224
+ return 0;
16225
+ }
16226
+ options.stdout(`${formatWorktreeTable(worktrees)}
16227
+ `);
16228
+ return 0;
16229
+ }
16230
+ const infos = await readWorktreeInfos(worktrees);
16231
+ if (options.json) {
16232
+ options.stdout(`${JSON.stringify(infos, null, 2)}
16233
+ `);
16234
+ return 0;
16235
+ }
16236
+ options.stdout(`${formatDetailedWorktreeTable(infos)}
16237
+ `);
16238
+ return 0;
15765
16239
  }
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");
16240
+ function formatDetailedWorktreeTable(worktrees) {
16241
+ const rows = worktrees.map((worktree) => ({
16242
+ branch: worktree.branch ?? "(detached)",
16243
+ isCurrent: worktree.isCurrent,
16244
+ lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
16245
+ path: worktree.path,
16246
+ status: worktree.status,
16247
+ upstream: formatUpstreamState(worktree.upstream)
16248
+ }));
16249
+ const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
16250
+ const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
16251
+ const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
16252
+ const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
16253
+ const lines = [
16254
+ " " + "BRANCH".padEnd(branchWidth, " ") + " " + "STATUS".padEnd(statusWidth, " ") + " " + "UPSTREAM".padEnd(upstreamWidth, " ") + " " + "LAST".padEnd(lastCommitWidth, " ") + " PATH"
16255
+ ];
16256
+ for (const row of rows) {
16257
+ lines.push(
16258
+ `${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.status.padEnd(statusWidth, " ")} ${row.upstream.padEnd(upstreamWidth, " ")} ${row.lastCommit.padEnd(lastCommitWidth, " ")} ` + row.path
16259
+ );
16260
+ }
16261
+ return lines.join("\n");
15769
16262
  }
15770
- function hasExecStderr(error) {
15771
- return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
16263
+ function formatWorktreeTable(worktrees) {
16264
+ const rows = worktrees.map((worktree) => ({
16265
+ branch: worktree.branch ?? "(detached)",
16266
+ isCurrent: worktree.isCurrent,
16267
+ path: worktree.path
16268
+ }));
16269
+ const branchWidth = Math.max(
16270
+ "BRANCH".length,
16271
+ ...rows.map((row) => row.branch.length)
16272
+ );
16273
+ const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
16274
+ for (const row of rows) {
16275
+ lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
16276
+ }
16277
+ return lines.join("\n");
15772
16278
  }
15773
- function toExecMessage(error) {
15774
- return hasExecStderr(error) ? error.stderr.trim() : String(error);
16279
+ function sortWorktrees(worktrees) {
16280
+ return [...worktrees].sort((left, right) => {
16281
+ if (left.isCurrent && !right.isCurrent) return -1;
16282
+ if (!left.isCurrent && right.isCurrent) return 1;
16283
+ return comparePaths(left.path, right.path);
16284
+ });
15775
16285
  }
15776
16286
 
15777
16287
  // 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);
16288
+ import { mkdir as mkdir7 } from "node:fs/promises";
16289
+ import { basename as basename6, dirname as dirname8 } from "node:path";
16290
+ import { execFile as execFile5 } from "node:child_process";
16291
+ import { promisify as promisify6 } from "node:util";
16292
+ var execFileAsync5 = promisify6(execFile5);
15783
16293
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
15784
16294
  function parsePrInput(input) {
15785
16295
  if (/^\d+$/.test(input)) return input;
@@ -15860,10 +16370,10 @@ function createPrCommand(dependencies = {}) {
15860
16370
  }
15861
16371
  return 1;
15862
16372
  }
15863
- await mkdir6(dirname7(worktreePath), { recursive: true });
16373
+ await mkdir7(dirname8(worktreePath), { recursive: true });
15864
16374
  const branchAlreadyExists = await localBranchExists2(repository.repoRoot, branchName);
15865
16375
  const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
15866
- await execFileAsync4("git", worktreeArgs, { cwd: repository.repoRoot });
16376
+ await execFileAsync5("git", worktreeArgs, { cwd: repository.repoRoot });
15867
16377
  const syncPatterns = Array.isArray(config.syncFiles) ? config.syncFiles.filter((p2) => typeof p2 === "string") : [];
15868
16378
  for (const pattern of syncPatterns) {
15869
16379
  try {
@@ -15878,7 +16388,7 @@ function createPrCommand(dependencies = {}) {
15878
16388
  await runHook(
15879
16389
  hooks.afterCreate,
15880
16390
  worktreePath,
15881
- { branch: branchName, path: worktreePath, repo: basename5(repository.repoRoot) },
16391
+ { branch: branchName, path: worktreePath, repo: basename6(repository.repoRoot) },
15882
16392
  options.stderr
15883
16393
  );
15884
16394
  if (options.json) {
@@ -15893,7 +16403,7 @@ function createPrCommand(dependencies = {}) {
15893
16403
  }
15894
16404
  async function localBranchExists2(repoRoot, branchName) {
15895
16405
  try {
15896
- await execFileAsync4(
16406
+ await execFileAsync5(
15897
16407
  "git",
15898
16408
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
15899
16409
  { cwd: repoRoot }
@@ -15907,7 +16417,7 @@ var runPrCommand = createPrCommand();
15907
16417
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
15908
16418
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
15909
16419
  try {
15910
- await execFileAsync4(
16420
+ await execFileAsync5(
15911
16421
  "git",
15912
16422
  ["fetch", "origin", `${sourceRef}:${remoteRef}`],
15913
16423
  { cwd: repoRoot }
@@ -15951,10 +16461,10 @@ async function writeOutput2(worktreePath, stdout) {
15951
16461
  }
15952
16462
 
15953
16463
  // src/remove.ts
15954
- import { basename as basename6 } from "node:path";
16464
+ import { basename as basename7 } from "node:path";
15955
16465
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
15956
16466
  function createRemoveCommand(dependencies = {}) {
15957
- const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree;
16467
+ const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
15958
16468
  const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval2;
15959
16469
  const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
15960
16470
  const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
@@ -16016,7 +16526,7 @@ function createRemoveCommand(dependencies = {}) {
16016
16526
  await runHook(
16017
16527
  hooks.beforeRemove,
16018
16528
  worktree.path,
16019
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename6(repository.repoRoot) },
16529
+ { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename7(repository.repoRoot) },
16020
16530
  options.stderr
16021
16531
  );
16022
16532
  try {
@@ -16066,7 +16576,7 @@ function createRemoveCommand(dependencies = {}) {
16066
16576
  };
16067
16577
  }
16068
16578
  var runRemoveCommand = createRemoveCommand();
16069
- async function defaultPromptForWorktree(worktrees) {
16579
+ async function defaultPromptForWorktree2(worktrees) {
16070
16580
  const choice = await ve({
16071
16581
  message: "Choose a worktree to finish",
16072
16582
  options: worktrees.map((worktree) => ({
@@ -16355,6 +16865,7 @@ function readPackageMetadata() {
16355
16865
  }
16356
16866
  async function runCli(argv, options = {}) {
16357
16867
  await maybeNotifyForUpdates(argv);
16868
+ maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
16358
16869
  const program2 = createProgram();
16359
16870
  const cwd = options.cwd ?? process.cwd();
16360
16871
  const stdout = options.stdout ?? (() => void 0);
@@ -16398,14 +16909,18 @@ function defaultNotifyForUpdates(pkg) {
16398
16909
  const notifier = updateNotifier({ pkg });
16399
16910
  notifier.notify();
16400
16911
  }
16912
+ function maybeRegisterCurrentRepo(cwd) {
16913
+ detectRepository(cwd).then(({ repoRoot }) => registerRepo(repoRoot)).catch(() => void 0);
16914
+ }
16401
16915
  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"));
16916
+ 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
16917
  program2.command("init [shell]").description("print or install shell integration").option("--write", "write the integration to the shell config file").action(notImplemented("init"));
16404
16918
  program2.command("completion [shell]").description("print shell completion definitions").action(notImplemented("completion"));
16405
16919
  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
16920
  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
16921
  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"));
16922
+ 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"));
16923
+ 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
16924
  program2.command("root").description("print the main repository root path").option("--print", "print the resolved repository root path explicitly").action(notImplemented("root"));
16410
16925
  program2.command("status").description("summarize repository and worktree health").option("--json", "print repository and worktree health as JSON").action(notImplemented("status"));
16411
16926
  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 +16928,7 @@ function registerCommands(program2) {
16413
16928
  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
16929
  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
16930
  program2.command("trigger-hook <hook>").description("run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree").action(notImplemented("trigger-hook"));
16931
+ 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
16932
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
16417
16933
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
16418
16934
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
@@ -16420,7 +16936,7 @@ function registerCommands(program2) {
16420
16936
  }
16421
16937
  function attachCommandActions(program2, options) {
16422
16938
  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 });
16939
+ 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
16940
  if (exitCode !== 0) {
16425
16941
  throw commanderExit(exitCode);
16426
16942
  }
@@ -16481,6 +16997,20 @@ function attachCommandActions(program2, options) {
16481
16997
  throw commanderExit(exitCode);
16482
16998
  }
16483
16999
  });
17000
+ program2.commands.find((command) => command.name() === "open")?.action(async (branch, commandOptions) => {
17001
+ const exitCode = await runOpenCommand({
17002
+ branch,
17003
+ cwd: options.cwd,
17004
+ editor: commandOptions.editor,
17005
+ save: commandOptions.save,
17006
+ stderr: options.stderr,
17007
+ stdout: options.stdout,
17008
+ workspace: commandOptions.workspace
17009
+ });
17010
+ if (exitCode !== 0) {
17011
+ throw commanderExit(exitCode);
17012
+ }
17013
+ });
16484
17014
  program2.commands.find((command) => command.name() === "go")?.action(async (branch, commandOptions) => {
16485
17015
  const exitCode = await runGoCommand({
16486
17016
  branch,
@@ -16575,6 +17105,22 @@ function attachCommandActions(program2, options) {
16575
17105
  throw commanderExit(exitCode);
16576
17106
  }
16577
17107
  });
17108
+ program2.commands.find((command) => command.name() === "warp")?.action(async (branch, commandOptions) => {
17109
+ const newFlag = commandOptions.new;
17110
+ const newWorktree = newFlag !== void 0 && newFlag !== false;
17111
+ const newBranch = typeof newFlag === "string" ? newFlag : void 0;
17112
+ const exitCode = await runWarpCommand({
17113
+ branch: newWorktree ? newBranch ?? branch : branch,
17114
+ cwd: options.cwd,
17115
+ json: commandOptions.json,
17116
+ newWorktree,
17117
+ stderr: options.stderr,
17118
+ stdout: options.stdout
17119
+ });
17120
+ if (exitCode !== 0) {
17121
+ throw commanderExit(exitCode);
17122
+ }
17123
+ });
16578
17124
  const configCommand = program2.commands.find((command) => command.name() === "config");
16579
17125
  configCommand?.action(async () => {
16580
17126
  const exitCode = await runConfigCommand({
@@ -16657,7 +17203,7 @@ async function main() {
16657
17203
  }
16658
17204
  async function warnIfMissingShellIntegration() {
16659
17205
  try {
16660
- const { config } = await loadGlobalConfig(homedir5());
17206
+ const { config } = await loadGlobalConfig(homedir6());
16661
17207
  if (!config.shellIntegration) {
16662
17208
  const shellBin = (process.env.SHELL ?? "").split("/").at(-1);
16663
17209
  const shellArg = shellBin && ["bash", "zsh", "fish"].includes(shellBin) ? ` ${shellBin}` : "";