@solaqua/gji 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +26 -1
  2. package/dist/back.d.ts +1 -1
  3. package/dist/back.js +23 -17
  4. package/dist/clean.d.ts +1 -1
  5. package/dist/clean.js +44 -35
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +266 -165
  8. package/dist/completion.js +3 -3
  9. package/dist/config-command.js +5 -5
  10. package/dist/config.js +41 -35
  11. package/dist/conflict.d.ts +1 -1
  12. package/dist/conflict.js +14 -6
  13. package/dist/editor.js +29 -9
  14. package/dist/file-sync.d.ts +1 -0
  15. package/dist/file-sync.js +15 -11
  16. package/dist/git.d.ts +1 -1
  17. package/dist/git.js +21 -19
  18. package/dist/gji-bundle.mjs +1688 -865
  19. package/dist/go.d.ts +2 -2
  20. package/dist/go.js +39 -26
  21. package/dist/headless.js +1 -1
  22. package/dist/history-command.js +3 -3
  23. package/dist/history.js +12 -12
  24. package/dist/hooks.d.ts +3 -3
  25. package/dist/hooks.js +19 -19
  26. package/dist/index.js +13 -9
  27. package/dist/init.d.ts +5 -5
  28. package/dist/init.js +115 -103
  29. package/dist/install-prompt.d.ts +3 -3
  30. package/dist/install-prompt.js +57 -38
  31. package/dist/ls.d.ts +2 -2
  32. package/dist/ls.js +29 -29
  33. package/dist/new.d.ts +2 -2
  34. package/dist/new.js +97 -82
  35. package/dist/open.d.ts +2 -2
  36. package/dist/open.js +24 -21
  37. package/dist/package-manager.js +96 -45
  38. package/dist/pr.d.ts +2 -2
  39. package/dist/pr.js +47 -34
  40. package/dist/remove.d.ts +1 -1
  41. package/dist/remove.js +39 -27
  42. package/dist/repo-registry.js +14 -14
  43. package/dist/repo.js +29 -28
  44. package/dist/root.js +3 -3
  45. package/dist/run-hook.d.ts +6 -0
  46. package/dist/{trigger-hook.js → run-hook.js} +18 -8
  47. package/dist/shell-completion.d.ts +1 -1
  48. package/dist/shell-completion.js +67 -39
  49. package/dist/shell-handoff.js +2 -2
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +4 -4
  52. package/dist/status.d.ts +5 -5
  53. package/dist/status.js +23 -23
  54. package/dist/sync-files-command.d.ts +10 -0
  55. package/dist/sync-files-command.js +137 -0
  56. package/dist/sync.js +23 -15
  57. package/dist/warp.js +37 -33
  58. package/dist/worktree-info.d.ts +9 -9
  59. package/dist/worktree-info.js +31 -29
  60. package/dist/worktree-management.d.ts +1 -1
  61. package/dist/worktree-management.js +26 -11
  62. package/dist/worktree-prompts.js +5 -5
  63. package/man/man1/gji-back.1 +1 -1
  64. package/man/man1/gji-clean.1 +1 -1
  65. package/man/man1/gji-completion.1 +1 -1
  66. package/man/man1/gji-config.1 +1 -1
  67. package/man/man1/gji-go.1 +1 -1
  68. package/man/man1/gji-history.1 +1 -1
  69. package/man/man1/gji-init.1 +1 -1
  70. package/man/man1/gji-ls.1 +1 -1
  71. package/man/man1/gji-new.1 +1 -1
  72. package/man/man1/gji-open.1 +1 -1
  73. package/man/man1/gji-pr.1 +1 -1
  74. package/man/man1/gji-remove.1 +1 -1
  75. package/man/man1/gji-root.1 +1 -1
  76. package/man/man1/gji-run-hook.1 +9 -0
  77. package/man/man1/gji-status.1 +1 -1
  78. package/man/man1/gji-sync-files.1 +23 -0
  79. package/man/man1/gji-sync.1 +1 -1
  80. package/man/man1/gji-warp.1 +1 -1
  81. package/man/man1/gji.1 +10 -4
  82. package/package.json +8 -2
  83. package/dist/trigger-hook.d.ts +0 -6
  84. package/man/man1/gji-trigger-hook.1 +0 -9
package/dist/sync.js CHANGED
@@ -1,16 +1,17 @@
1
- import { loadEffectiveConfig } from './config.js';
2
- import { isDirtyWorktree, resolveRemoteDefaultBranch, runGit } from './git.js';
3
- import { comparePaths } from './paths.js';
4
- import { detectRepository, listWorktrees } from './repo.js';
1
+ import { loadEffectiveConfig } from "./config.js";
2
+ import { isDirtyWorktree, resolveRemoteDefaultBranch, runGit } from "./git.js";
3
+ import { comparePaths } from "./paths.js";
4
+ import { detectRepository, listWorktrees } from "./repo.js";
5
5
  export async function runSyncCommand(options) {
6
6
  const repository = await detectRepository(options.cwd);
7
7
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
8
8
  const worktrees = await listWorktrees(options.cwd);
9
- const remote = resolveConfiguredString(config.syncRemote) ?? 'origin';
9
+ const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
10
10
  let defaultBranch;
11
11
  try {
12
- defaultBranch = resolveConfiguredString(config.syncDefaultBranch)
13
- ?? await resolveRemoteDefaultBranch(repository.repoRoot, remote);
12
+ defaultBranch =
13
+ resolveConfiguredString(config.syncDefaultBranch) ??
14
+ (await resolveRemoteDefaultBranch(repository.repoRoot, remote));
14
15
  }
15
16
  catch {
16
17
  emitError(options, `Unable to reach remote '${remote}'`);
@@ -20,14 +21,14 @@ export async function runSyncCommand(options) {
20
21
  return 1;
21
22
  }
22
23
  if (!defaultBranch) {
23
- emitError(options, 'Unable to determine the default branch for sync.');
24
+ emitError(options, "Unable to determine the default branch for sync.");
24
25
  if (!options.json) {
25
26
  options.stderr(`Hint: Add the remote with: git remote add ${remote} <url>\n`);
26
27
  }
27
28
  return 1;
28
29
  }
29
30
  const targetWorktrees = selectTargetWorktrees(worktrees, repository.currentRoot, options.all);
30
- if (targetWorktrees === 'detached') {
31
+ if (targetWorktrees === "detached") {
31
32
  emitError(options, `Cannot sync detached worktree: ${repository.currentRoot}`);
32
33
  return 1;
33
34
  }
@@ -38,7 +39,7 @@ export async function runSyncCommand(options) {
38
39
  }
39
40
  }
40
41
  try {
41
- await runGit(repository.repoRoot, ['fetch', '--prune', remote]);
42
+ await runGit(repository.repoRoot, ["fetch", "--prune", remote]);
42
43
  }
43
44
  catch {
44
45
  emitError(options, `Failed to fetch from remote '${remote}'`);
@@ -50,10 +51,14 @@ export async function runSyncCommand(options) {
50
51
  const updatedWorktrees = [];
51
52
  for (const worktree of targetWorktrees) {
52
53
  if (worktree.branch === defaultBranch) {
53
- await runGit(worktree.path, ['merge', '--ff-only', `${remote}/${defaultBranch}`]);
54
+ await runGit(worktree.path, [
55
+ "merge",
56
+ "--ff-only",
57
+ `${remote}/${defaultBranch}`,
58
+ ]);
54
59
  }
55
60
  else {
56
- await runGit(worktree.path, ['rebase', `${remote}/${defaultBranch}`]);
61
+ await runGit(worktree.path, ["rebase", `${remote}/${defaultBranch}`]);
57
62
  }
58
63
  updatedWorktrees.push(worktree);
59
64
  if (!options.json) {
@@ -61,7 +66,10 @@ export async function runSyncCommand(options) {
61
66
  }
62
67
  }
63
68
  if (options.json) {
64
- const updated = updatedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
69
+ const updated = updatedWorktrees.map((w) => ({
70
+ branch: w.branch,
71
+ path: w.path,
72
+ }));
65
73
  options.stdout(`${JSON.stringify({ updated }, null, 2)}\n`);
66
74
  }
67
75
  return 0;
@@ -85,10 +93,10 @@ function selectTargetWorktrees(worktrees, currentRoot, all) {
85
93
  return [];
86
94
  }
87
95
  if (!currentWorktree.branch) {
88
- return 'detached';
96
+ return "detached";
89
97
  }
90
98
  return [currentWorktree];
91
99
  }
92
100
  function resolveConfiguredString(value) {
93
- return typeof value === 'string' && value.length > 0 ? value : null;
101
+ return typeof value === "string" && value.length > 0 ? value : null;
94
102
  }
package/dist/warp.js CHANGED
@@ -1,20 +1,20 @@
1
- import { isCancel, select } from '@clack/prompts';
2
- import { realpath } from 'node:fs/promises';
3
- import { basename, resolve } from 'node:path';
4
- import { readWorktreeHealth } from './git.js';
5
- import { isHeadless } from './headless.js';
6
- import { appendHistory } from './history.js';
7
- import { runNewCommand } from './new.js';
8
- import { loadRegistry } from './repo-registry.js';
9
- import { listWorktrees } from './repo.js';
10
- import { writeShellOutput } from './shell-handoff.js';
11
- const WARP_OUTPUT_FILE_ENV = 'GJI_WARP_OUTPUT_FILE';
1
+ import { realpath } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import { isCancel, select } from "@clack/prompts";
4
+ import { readWorktreeHealth } from "./git.js";
5
+ import { isHeadless } from "./headless.js";
6
+ import { appendHistory } from "./history.js";
7
+ import { runNewCommand } from "./new.js";
8
+ import { listWorktrees } from "./repo.js";
9
+ import { loadRegistry } from "./repo-registry.js";
10
+ import { writeShellOutput } from "./shell-handoff.js";
11
+ const WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
12
12
  export async function runWarpCommand(options) {
13
13
  if (options.newWorktree) {
14
14
  const registry = await loadRegistry();
15
15
  if (registry.length === 0) {
16
- options.stderr('gji warp: no repos registered yet.\n' +
17
- 'Use any gji command in a repository to register it automatically.\n');
16
+ options.stderr("gji warp: no repos registered yet.\n" +
17
+ "Use any gji command in a repository to register it automatically.\n");
18
18
  return 1;
19
19
  }
20
20
  return runWarpNew(options, registry);
@@ -23,16 +23,20 @@ export async function runWarpCommand(options) {
23
23
  }
24
24
  async function runWarpNavigate(options) {
25
25
  if ((isHeadless() || options.json) && !options.branch) {
26
- const message = 'branch argument is required';
26
+ const message = "branch argument is required";
27
27
  if (options.json) {
28
28
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
29
29
  }
30
30
  else {
31
- options.stderr('gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
31
+ options.stderr("gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
32
32
  }
33
33
  return 1;
34
34
  }
35
- const target = await resolveWarpTarget({ ...options, commandName: 'gji warp', json: options.json });
35
+ const target = await resolveWarpTarget({
36
+ ...options,
37
+ commandName: "gji warp",
38
+ json: options.json,
39
+ });
36
40
  if (!target)
37
41
  return 1;
38
42
  if (options.json) {
@@ -52,11 +56,11 @@ async function runWarpNew(options, registry) {
52
56
  }
53
57
  else {
54
58
  if (isHeadless()) {
55
- options.stderr('gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
59
+ options.stderr("gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
56
60
  return 1;
57
61
  }
58
62
  const choice = await select({
59
- message: 'Create worktree in which repo?',
63
+ message: "Create worktree in which repo?",
60
64
  options: deduplicatedRegistry.map((entry) => ({
61
65
  value: entry.path,
62
66
  label: entry.name,
@@ -64,7 +68,7 @@ async function runWarpNew(options, registry) {
64
68
  })),
65
69
  });
66
70
  if (isCancel(choice)) {
67
- options.stderr('Aborted\n');
71
+ options.stderr("Aborted\n");
68
72
  return 1;
69
73
  }
70
74
  targetRepoRoot = choice;
@@ -81,7 +85,7 @@ async function runWarpNew(options, registry) {
81
85
  // runNewCommand writes the created path to options.stdout via writeShellOutput.
82
86
  // Since GJI_NEW_OUTPUT_FILE is not set in the warp shell context, it falls
83
87
  // through to our captured stdout, giving us the path to hand off.
84
- let capturedPath = '';
88
+ let capturedPath = "";
85
89
  const captureStdout = (chunk) => {
86
90
  capturedPath = chunk.trim();
87
91
  };
@@ -95,7 +99,7 @@ async function runWarpNew(options, registry) {
95
99
  return exitCode;
96
100
  }
97
101
  if (!capturedPath) {
98
- options.stderr('gji warp: could not determine new worktree path\n');
102
+ options.stderr("gji warp: could not determine new worktree path\n");
99
103
  return 1;
100
104
  }
101
105
  await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
@@ -127,7 +131,7 @@ async function canonicalizeRepoPath(repoPath) {
127
131
  }
128
132
  }
129
133
  function findByQuery(items, query) {
130
- const slashIdx = query.indexOf('/');
134
+ const slashIdx = query.indexOf("/");
131
135
  if (slashIdx !== -1) {
132
136
  const repoQuery = query.slice(0, slashIdx);
133
137
  const branchQuery = query.slice(slashIdx + 1);
@@ -138,7 +142,7 @@ function findByQuery(items, query) {
138
142
  return items.find((item) => item.worktree.branch === query) ?? null;
139
143
  }
140
144
  export async function resolveWarpTarget(options) {
141
- const cmd = options.commandName ?? 'gji';
145
+ const cmd = options.commandName ?? "gji";
142
146
  const emitError = (message, hint) => {
143
147
  if (options.json) {
144
148
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
@@ -151,7 +155,7 @@ export async function resolveWarpTarget(options) {
151
155
  };
152
156
  const registry = await loadRegistry();
153
157
  if (registry.length === 0) {
154
- emitError('not in a git repository and no repos registered yet.', 'Use any gji command inside a repository to register it.\n');
158
+ emitError("not in a git repository and no repos registered yet.", "Use any gji command inside a repository to register it.\n");
155
159
  return null;
156
160
  }
157
161
  const results = await Promise.allSettled(registry.map(async (entry) => {
@@ -160,7 +164,7 @@ export async function resolveWarpTarget(options) {
160
164
  }));
161
165
  const allItems = [];
162
166
  for (const result of results) {
163
- if (result.status === 'rejected')
167
+ if (result.status === "rejected")
164
168
  continue;
165
169
  const { repoName, worktrees } = result.value;
166
170
  for (const worktree of worktrees) {
@@ -168,7 +172,7 @@ export async function resolveWarpTarget(options) {
168
172
  }
169
173
  }
170
174
  if (allItems.length === 0) {
171
- emitError('no accessible worktrees found in any registered repo.');
175
+ emitError("no accessible worktrees found in any registered repo.");
172
176
  return null;
173
177
  }
174
178
  if (options.branch) {
@@ -181,7 +185,7 @@ export async function resolveWarpTarget(options) {
181
185
  }
182
186
  const path = await promptForWarpTarget(allItems);
183
187
  if (!path) {
184
- options.stderr('Aborted\n');
188
+ options.stderr("Aborted\n");
185
189
  return null;
186
190
  }
187
191
  const chosen = allItems.find((item) => item.worktree.path === path);
@@ -190,11 +194,11 @@ export async function resolveWarpTarget(options) {
190
194
  async function promptForWarpTarget(items) {
191
195
  const healthResults = await Promise.allSettled(items.map((item) => readWorktreeHealth(item.worktree.path)));
192
196
  const choice = await select({
193
- message: 'Warp to a worktree',
197
+ message: "Warp to a worktree",
194
198
  options: items.map((item, i) => {
195
- const health = healthResults[i].status === 'fulfilled' ? healthResults[i].value : null;
199
+ const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
196
200
  const upstream = health ? formatHint(item.worktree.branch, health) : null;
197
- const label = `${item.repoName} › ${item.worktree.branch ?? '(detached)'}`;
201
+ const label = `${item.repoName} › ${item.worktree.branch ?? "(detached)"}`;
198
202
  const pathHint = item.worktree.isCurrent
199
203
  ? `${item.worktree.path} (current)`
200
204
  : item.worktree.path;
@@ -211,11 +215,11 @@ function formatHint(branch, health) {
211
215
  if (branch === null)
212
216
  return null;
213
217
  if (!health.hasUpstream)
214
- return 'no upstream';
218
+ return "no upstream";
215
219
  if (health.upstreamGone)
216
- return 'upstream gone';
220
+ return "upstream gone";
217
221
  if (health.ahead === 0 && health.behind === 0)
218
- return 'up to date';
222
+ return "up to date";
219
223
  if (health.ahead === 0)
220
224
  return `behind ${health.behind}`;
221
225
  if (health.behind === 0)
@@ -1,29 +1,29 @@
1
- import { type WorktreeHealth } from './git.js';
2
- import type { WorktreeEntry } from './repo.js';
1
+ import { type WorktreeHealth } from "./git.js";
2
+ import type { WorktreeEntry } from "./repo.js";
3
3
  export interface WorktreeInfo extends WorktreeEntry {
4
4
  lastCommitTimestamp: number | null;
5
- status: WorktreeHealth['status'] | 'unknown';
5
+ status: WorktreeHealth["status"] | "unknown";
6
6
  upstream: UpstreamState;
7
7
  }
8
8
  export interface SerializedWorktreeInfo {
9
9
  branch: string | null;
10
10
  lastCommitTimestamp: number | null;
11
11
  path: string;
12
- status: WorktreeInfo['status'];
12
+ status: WorktreeInfo["status"];
13
13
  upstream: UpstreamState;
14
14
  }
15
15
  export type UpstreamState = {
16
- kind: 'detached';
16
+ kind: "detached";
17
17
  } | {
18
- kind: 'no-upstream';
18
+ kind: "no-upstream";
19
19
  } | {
20
- kind: 'stale';
20
+ kind: "stale";
21
21
  } | {
22
- kind: 'tracked';
22
+ kind: "tracked";
23
23
  ahead: number;
24
24
  behind: number;
25
25
  } | {
26
- kind: 'unknown';
26
+ kind: "unknown";
27
27
  };
28
28
  export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
29
29
  export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
@@ -1,40 +1,40 @@
1
- import { readBranchLastCommitTimestamp, readWorktreeHealth } from './git.js';
1
+ import { readBranchLastCommitTimestamp, readWorktreeHealth, } from "./git.js";
2
2
  export async function readWorktreeInfos(worktrees) {
3
3
  return Promise.all(worktrees.map((worktree) => readWorktreeInfo(worktree)));
4
4
  }
5
5
  async function readWorktreeInfo(worktree) {
6
6
  const [healthResult, lastCommitResult] = await Promise.allSettled([
7
7
  readWorktreeHealth(worktree.path),
8
- worktree.branch === null ? null : readBranchLastCommitTimestamp(worktree.path, worktree.branch),
8
+ worktree.branch === null
9
+ ? null
10
+ : readBranchLastCommitTimestamp(worktree.path, worktree.branch),
9
11
  ]);
10
- const health = healthResult.status === 'fulfilled' ? healthResult.value : null;
11
- const lastCommitTimestamp = lastCommitResult.status === 'fulfilled'
12
- ? lastCommitResult.value
13
- : null;
12
+ const health = healthResult.status === "fulfilled" ? healthResult.value : null;
13
+ const lastCommitTimestamp = lastCommitResult.status === "fulfilled" ? lastCommitResult.value : null;
14
14
  return {
15
15
  ...worktree,
16
16
  lastCommitTimestamp,
17
- status: health?.status ?? 'unknown',
17
+ status: health?.status ?? "unknown",
18
18
  upstream: buildUpstreamState(worktree.branch, health),
19
19
  };
20
20
  }
21
21
  function buildUpstreamState(branch, health) {
22
22
  if (branch === null) {
23
- return { kind: 'detached' };
23
+ return { kind: "detached" };
24
24
  }
25
25
  if (health === null) {
26
- return { kind: 'unknown' };
26
+ return { kind: "unknown" };
27
27
  }
28
28
  if (!health.hasUpstream) {
29
- return { kind: 'no-upstream' };
29
+ return { kind: "no-upstream" };
30
30
  }
31
31
  if (health.upstreamGone) {
32
- return { kind: 'stale' };
32
+ return { kind: "stale" };
33
33
  }
34
34
  return {
35
35
  ahead: health.ahead,
36
36
  behind: health.behind,
37
- kind: 'tracked',
37
+ kind: "tracked",
38
38
  };
39
39
  }
40
40
  export function serializeWorktreeInfo(info) {
@@ -54,26 +54,26 @@ export function formatWorktreeHint(info) {
54
54
  if (info.lastCommitTimestamp !== null) {
55
55
  details.push(`last: ${formatRelativeAge(info.lastCommitTimestamp)}`);
56
56
  }
57
- return `${info.path} (${details.join(', ')})`;
57
+ return `${info.path} (${details.join(", ")})`;
58
58
  }
59
59
  export function formatUpstreamState(upstream) {
60
- if (upstream.kind === 'detached') {
61
- return 'n/a';
60
+ if (upstream.kind === "detached") {
61
+ return "n/a";
62
62
  }
63
- if (upstream.kind === 'no-upstream') {
64
- return 'no-upstream';
63
+ if (upstream.kind === "no-upstream") {
64
+ return "no-upstream";
65
65
  }
66
- if (upstream.kind === 'stale') {
67
- return 'gone';
66
+ if (upstream.kind === "stale") {
67
+ return "gone";
68
68
  }
69
- if (upstream.kind === 'unknown') {
70
- return 'unknown';
69
+ if (upstream.kind === "unknown") {
70
+ return "unknown";
71
71
  }
72
72
  return formatAheadBehind(upstream.ahead, upstream.behind);
73
73
  }
74
74
  function formatAheadBehind(ahead, behind) {
75
75
  if (ahead === 0 && behind === 0) {
76
- return 'up to date';
76
+ return "up to date";
77
77
  }
78
78
  if (ahead === 0) {
79
79
  return `behind ${behind}`;
@@ -84,16 +84,18 @@ function formatAheadBehind(ahead, behind) {
84
84
  return `ahead ${ahead}, behind ${behind}`;
85
85
  }
86
86
  export function formatLastCommit(timestampSeconds) {
87
- return timestampSeconds === null ? 'n/a' : formatRelativeAge(timestampSeconds);
87
+ return timestampSeconds === null
88
+ ? "n/a"
89
+ : formatRelativeAge(timestampSeconds);
88
90
  }
89
91
  export function formatRelativeAge(timestampSeconds) {
90
92
  const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - timestampSeconds);
91
93
  const units = [
92
- { label: 'y', seconds: 365 * 24 * 60 * 60 },
93
- { label: 'mo', seconds: 30 * 24 * 60 * 60 },
94
- { label: 'd', seconds: 24 * 60 * 60 },
95
- { label: 'h', seconds: 60 * 60 },
96
- { label: 'm', seconds: 60 },
94
+ { label: "y", seconds: 365 * 24 * 60 * 60 },
95
+ { label: "mo", seconds: 30 * 24 * 60 * 60 },
96
+ { label: "d", seconds: 24 * 60 * 60 },
97
+ { label: "h", seconds: 60 * 60 },
98
+ { label: "m", seconds: 60 },
97
99
  ];
98
100
  for (const unit of units) {
99
101
  const value = Math.floor(ageSeconds / unit.seconds);
@@ -101,5 +103,5 @@ export function formatRelativeAge(timestampSeconds) {
101
103
  return `${value}${unit.label} ago`;
102
104
  }
103
105
  }
104
- return 'just now';
106
+ return "just now";
105
107
  }
@@ -1,4 +1,4 @@
1
- import { type RepositoryContext, type WorktreeEntry } from './repo.js';
1
+ import { type RepositoryContext, type WorktreeEntry } from "./repo.js";
2
2
  export interface LinkedWorktreeContext {
3
3
  linkedWorktrees: WorktreeEntry[];
4
4
  repository: RepositoryContext;
@@ -1,9 +1,9 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import { detectRepository, listWorktrees } from './repo.js';
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { detectRepository, listWorktrees, } from "./repo.js";
4
4
  const execFileAsync = promisify(execFile);
5
5
  // Force English output so error message string matching is locale-independent.
6
- const GIT_ENV = { ...process.env, LC_ALL: 'C' };
6
+ const GIT_ENV = { ...process.env, LC_ALL: "C" };
7
7
  export async function loadLinkedWorktrees(cwd) {
8
8
  const repository = await detectRepository(cwd);
9
9
  const linkedWorktrees = (await listWorktrees(cwd)).filter((worktree) => worktree.path !== repository.repoRoot);
@@ -13,23 +13,38 @@ export async function loadLinkedWorktrees(cwd) {
13
13
  };
14
14
  }
15
15
  export async function removeWorktree(repoRoot, worktreePath) {
16
- await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoRoot, env: GIT_ENV });
16
+ await execFileAsync("git", ["worktree", "remove", worktreePath], {
17
+ cwd: repoRoot,
18
+ env: GIT_ENV,
19
+ });
17
20
  }
18
21
  export async function forceRemoveWorktree(repoRoot, worktreePath) {
19
- await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot, env: GIT_ENV });
22
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
23
+ cwd: repoRoot,
24
+ env: GIT_ENV,
25
+ });
20
26
  }
21
27
  export async function deleteBranch(repoRoot, branch) {
22
- await execFileAsync('git', ['branch', '-d', branch], { cwd: repoRoot, env: GIT_ENV });
28
+ await execFileAsync("git", ["branch", "-d", branch], {
29
+ cwd: repoRoot,
30
+ env: GIT_ENV,
31
+ });
23
32
  }
24
33
  export async function forceDeleteBranch(repoRoot, branch) {
25
- await execFileAsync('git', ['branch', '-D', branch], { cwd: repoRoot, env: GIT_ENV });
34
+ await execFileAsync("git", ["branch", "-D", branch], {
35
+ cwd: repoRoot,
36
+ env: GIT_ENV,
37
+ });
26
38
  }
27
39
  export function isWorktreeDirtyError(error) {
28
- return hasStderr(error) && error.stderr.includes('contains modified or untracked files');
40
+ return (hasStderr(error) &&
41
+ error.stderr.includes("contains modified or untracked files"));
29
42
  }
30
43
  export function isBranchUnmergedError(error) {
31
- return hasStderr(error) && error.stderr.includes('is not fully merged');
44
+ return hasStderr(error) && error.stderr.includes("is not fully merged");
32
45
  }
33
46
  function hasStderr(error) {
34
- return error instanceof Error && 'stderr' in error && typeof error.stderr === 'string';
47
+ return (error instanceof Error &&
48
+ "stderr" in error &&
49
+ typeof error.stderr === "string");
35
50
  }
@@ -1,8 +1,8 @@
1
- import { confirm, isCancel } from '@clack/prompts';
1
+ import { confirm, isCancel } from "@clack/prompts";
2
2
  export async function defaultConfirmForceRemoveWorktree(worktreePath) {
3
3
  const choice = await confirm({
4
- active: 'Yes',
5
- inactive: 'No',
4
+ active: "Yes",
5
+ inactive: "No",
6
6
  initialValue: false,
7
7
  message: `Worktree at ${worktreePath} has untracked or modified files. Force remove?`,
8
8
  });
@@ -10,8 +10,8 @@ export async function defaultConfirmForceRemoveWorktree(worktreePath) {
10
10
  }
11
11
  export async function defaultConfirmForceDeleteBranch(branch) {
12
12
  const choice = await confirm({
13
- active: 'Yes',
14
- inactive: 'No',
13
+ active: "Yes",
14
+ inactive: "No",
15
15
  initialValue: false,
16
16
  message: `Branch ${branch} has unmerged commits. Force delete?`,
17
17
  });
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-BACK 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-back \- navigate to the previously visited worktree, optionally N steps back
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-CLEAN 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-CLEAN 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-clean \- interactively prune linked worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-COMPLETION 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-COMPLETION 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-completion \- print shell completion definitions
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-CONFIG 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-CONFIG 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-config \- manage global config defaults
4
4
  .SH SYNOPSIS
package/man/man1/gji-go.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-GO 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-GO 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-go \- print or select a worktree path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-HISTORY 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-HISTORY 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-history \- show navigation history
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-INIT 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-INIT 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-init \- print or install shell integration
4
4
  .SH SYNOPSIS
package/man/man1/gji-ls.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-LS 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-LS 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-NEW 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-NEW 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-new \- create a new branch or detached linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-OPEN 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-OPEN 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-open \- open the worktree in an editor
4
4
  .SH SYNOPSIS
package/man/man1/gji-pr.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-PR 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-PR 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-pr \- fetch a pull request by number, #number, or URL into a linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-REMOVE 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-REMOVE 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-remove \- remove a linked worktree and delete its branch when present
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-ROOT 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-ROOT 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-root \- print the main repository root path
4
4
  .SH SYNOPSIS
@@ -0,0 +1,9 @@
1
+ .TH GJI\-RUN\-HOOK 1 "June 2026" "gji 0.7.1" "User Commands"
2
+ .SH NAME
3
+ gji\-run\-hook \- run a named hook (after\-create, after\-enter, before\-remove) in the current worktree
4
+ .SH SYNOPSIS
5
+ .B gji run\-hook [options] <hook>
6
+ .SH DESCRIPTION
7
+ run a named hook (after\-create, after\-enter, before\-remove) in the current worktree
8
+ .SH "SEE ALSO"
9
+ .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-STATUS 1 "May 2026" "gji 0.6.2" "User Commands"
1
+ .TH GJI\-STATUS 1 "June 2026" "gji 0.7.1" "User Commands"
2
2
  .SH NAME
3
3
  gji\-status \- summarize repository and worktree health
4
4
  .SH SYNOPSIS