@solaqua/gji 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +83 -426
  2. package/dist/back.d.ts +3 -0
  3. package/dist/back.js +19 -7
  4. package/dist/bootstrap-output.d.ts +3 -0
  5. package/dist/bootstrap-output.js +27 -0
  6. package/dist/bootstrap-preview.d.ts +10 -0
  7. package/dist/bootstrap-preview.js +21 -0
  8. package/dist/clean.js +27 -3
  9. package/dist/cli.d.ts +4 -0
  10. package/dist/cli.js +140 -22
  11. package/dist/clone-failure-store.d.ts +18 -0
  12. package/dist/clone-failure-store.js +230 -0
  13. package/dist/command-runner.d.ts +6 -0
  14. package/dist/command-runner.js +38 -0
  15. package/dist/completion.js +7 -2
  16. package/dist/config-command.d.ts +1 -0
  17. package/dist/config-command.js +7 -1
  18. package/dist/config.d.ts +26 -1
  19. package/dist/config.js +110 -6
  20. package/dist/context-card.d.ts +1 -0
  21. package/dist/context-card.js +25 -0
  22. package/dist/dependency-bootstrap-prompt.d.ts +24 -0
  23. package/dist/dependency-bootstrap-prompt.js +103 -0
  24. package/dist/dependency-bootstrap.d.ts +129 -0
  25. package/dist/dependency-bootstrap.js +566 -0
  26. package/dist/dir-clone.d.ts +33 -0
  27. package/dist/dir-clone.js +505 -0
  28. package/dist/doctor.js +21 -0
  29. package/dist/done.d.ts +10 -0
  30. package/dist/done.js +210 -0
  31. package/dist/file-sync.js +71 -22
  32. package/dist/format-bytes.d.ts +1 -0
  33. package/dist/format-bytes.js +14 -0
  34. package/dist/fs-utils.d.ts +4 -0
  35. package/dist/fs-utils.js +26 -0
  36. package/dist/git.d.ts +5 -0
  37. package/dist/git.js +50 -0
  38. package/dist/gji-bundle.mjs +6189 -1313
  39. package/dist/go-resolver.d.ts +35 -0
  40. package/dist/go-resolver.js +185 -0
  41. package/dist/go.d.ts +6 -2
  42. package/dist/go.js +270 -62
  43. package/dist/hooks.d.ts +2 -1
  44. package/dist/hooks.js +20 -8
  45. package/dist/hub.d.ts +31 -0
  46. package/dist/hub.js +287 -0
  47. package/dist/init.js +30 -22
  48. package/dist/install-prompt.d.ts +1 -0
  49. package/dist/install-prompt.js +3 -25
  50. package/dist/ls.js +9 -1
  51. package/dist/navigation-output.d.ts +11 -0
  52. package/dist/navigation-output.js +6 -0
  53. package/dist/new.d.ts +14 -2
  54. package/dist/new.js +422 -38
  55. package/dist/pr-open.js +2 -10
  56. package/dist/pr.d.ts +6 -2
  57. package/dist/pr.js +135 -30
  58. package/dist/pull-requests.d.ts +1 -0
  59. package/dist/pull-requests.js +32 -3
  60. package/dist/remove.js +22 -0
  61. package/dist/repo.d.ts +4 -0
  62. package/dist/repo.js +61 -6
  63. package/dist/run-hook.js +2 -0
  64. package/dist/safe-destination.d.ts +15 -0
  65. package/dist/safe-destination.js +166 -0
  66. package/dist/shell-completion.js +52 -13
  67. package/dist/slots.d.ts +8 -0
  68. package/dist/slots.js +61 -0
  69. package/dist/status.d.ts +2 -0
  70. package/dist/status.js +9 -1
  71. package/dist/sync-directories.d.ts +33 -0
  72. package/dist/sync-directories.js +97 -0
  73. package/dist/sync-plan.d.ts +16 -0
  74. package/dist/sync-plan.js +128 -0
  75. package/dist/sync.js +7 -13
  76. package/dist/task-command.d.ts +9 -0
  77. package/dist/task-command.js +29 -0
  78. package/dist/task.d.ts +9 -0
  79. package/dist/task.js +37 -0
  80. package/dist/terminal-text.d.ts +4 -0
  81. package/dist/terminal-text.js +72 -0
  82. package/dist/undo.d.ts +36 -0
  83. package/dist/undo.js +392 -0
  84. package/dist/warp.d.ts +3 -1
  85. package/dist/warp.js +32 -125
  86. package/dist/worktree-bootstrap.d.ts +35 -0
  87. package/dist/worktree-bootstrap.js +96 -0
  88. package/dist/worktree-info.d.ts +6 -1
  89. package/dist/worktree-info.js +14 -4
  90. package/dist/worktree-management.d.ts +1 -0
  91. package/dist/worktree-management.js +23 -4
  92. package/dist/worktree-picker.d.ts +24 -11
  93. package/dist/worktree-picker.js +176 -95
  94. package/dist/worktree-prompts.js +1 -1
  95. package/dist/worktree-source.d.ts +6 -0
  96. package/dist/worktree-source.js +1 -0
  97. package/dist/worktree-sources.d.ts +4 -0
  98. package/dist/worktree-sources.js +77 -0
  99. package/man/man1/gji-back.1 +1 -1
  100. package/man/man1/gji-clean.1 +1 -1
  101. package/man/man1/gji-completion.1 +1 -1
  102. package/man/man1/gji-config.1 +1 -1
  103. package/man/man1/gji-doctor.1 +1 -1
  104. package/man/man1/gji-done.1 +19 -0
  105. package/man/man1/gji-go.1 +12 -3
  106. package/man/man1/gji-history.1 +1 -1
  107. package/man/man1/gji-init.1 +1 -1
  108. package/man/man1/gji-ls.1 +1 -1
  109. package/man/man1/gji-new.1 +21 -3
  110. package/man/man1/gji-open.1 +1 -1
  111. package/man/man1/gji-pr.1 +7 -1
  112. package/man/man1/gji-remove.1 +3 -3
  113. package/man/man1/gji-root.1 +1 -1
  114. package/man/man1/gji-run-hook.1 +1 -1
  115. package/man/man1/gji-status.1 +1 -1
  116. package/man/man1/gji-sync-files.1 +1 -1
  117. package/man/man1/gji-sync.1 +1 -1
  118. package/man/man1/gji-task.1 +16 -0
  119. package/man/man1/gji-undo.1 +16 -0
  120. package/man/man1/gji-warp.1 +3 -6
  121. package/man/man1/gji.1 +17 -5
  122. package/package.json +3 -1
package/dist/hooks.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
- export async function runHook(hookCmd, cwd, context, stderr) {
2
+ export async function runHook(hookCmd, cwd, context, stderr, stdout = (chunk) => process.stdout.write(chunk)) {
3
3
  if (!hookCmd)
4
4
  return;
5
5
  if (Array.isArray(hookCmd)) {
6
- await runArgvHook(hookCmd, cwd, context, stderr);
6
+ await runArgvHook(hookCmd, cwd, context, stderr, stdout);
7
7
  return;
8
8
  }
9
- await runShellHook(hookCmd, cwd, context, stderr);
9
+ await runShellHook(hookCmd, cwd, context, stderr, stdout);
10
10
  }
11
- async function runArgvHook(hookCmd, cwd, context, stderr) {
11
+ async function runArgvHook(hookCmd, cwd, context, stderr, stdout) {
12
12
  const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
13
13
  if (!command) {
14
14
  stderr("gji: hook argv command must include a non-empty command\n");
@@ -18,9 +18,12 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
18
18
  const child = spawn(command, args, {
19
19
  cwd,
20
20
  shell: false,
21
- stdio: ["ignore", "inherit", "pipe"],
21
+ stdio: ["ignore", "pipe", "pipe"],
22
22
  env: hookEnvironment(context),
23
23
  });
24
+ child.stdout.on("data", (chunk) => {
25
+ stdout(chunk.toString());
26
+ });
24
27
  child.stderr.on("data", (chunk) => {
25
28
  stderr(chunk.toString());
26
29
  });
@@ -36,15 +39,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
36
39
  });
37
40
  });
38
41
  }
39
- async function runShellHook(hookCmd, cwd, context, stderr) {
42
+ async function runShellHook(hookCmd, cwd, context, stderr, stdout) {
40
43
  const interpolated = interpolate(hookCmd, context);
41
44
  await new Promise((resolve) => {
42
45
  const child = spawn(interpolated, {
43
46
  cwd,
44
47
  shell: true,
45
- stdio: ["ignore", "inherit", "pipe"],
48
+ stdio: ["ignore", "pipe", "pipe"],
46
49
  env: hookEnvironment(context),
47
50
  });
51
+ child.stdout.on("data", (chunk) => {
52
+ stdout(chunk.toString());
53
+ });
48
54
  child.stderr.on("data", (chunk) => {
49
55
  stderr(chunk.toString());
50
56
  });
@@ -66,6 +72,9 @@ function hookEnvironment(context) {
66
72
  GJI_BRANCH: context.branch ?? "",
67
73
  GJI_PATH: context.path,
68
74
  GJI_REPO: context.repo,
75
+ ...(context.slot === null || context.slot === undefined
76
+ ? {}
77
+ : { GJI_SLOT: String(context.slot) }),
69
78
  };
70
79
  }
71
80
  function formatArgvHook(command, args) {
@@ -75,7 +84,10 @@ export function interpolate(template, context) {
75
84
  return template
76
85
  .replace(/\{\{branch\}\}/g, context.branch ?? "")
77
86
  .replace(/\{\{path\}\}/g, context.path)
78
- .replace(/\{\{repo\}\}/g, context.repo);
87
+ .replace(/\{\{repo\}\}/g, context.repo)
88
+ .replace(/\{\{slot\}\}/g, context.slot === null || context.slot === undefined
89
+ ? ""
90
+ : String(context.slot));
79
91
  }
80
92
  export function extractHooks(config) {
81
93
  const raw = config.hooks;
package/dist/hub.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type PullRequestInfo } from "./pull-requests.js";
2
+ import { type WorktreeInfo } from "./worktree-info.js";
3
+ export interface HubCommandOptions {
4
+ cwd: string;
5
+ columns?: number;
6
+ json?: boolean;
7
+ now?: number;
8
+ stderr: (chunk: string) => void;
9
+ stdout: (chunk: string) => void;
10
+ }
11
+ export interface HubCommandDependencies {
12
+ queryRepositoryPullRequests: (repoRoot: string) => Promise<PullRequestInfo[]>;
13
+ }
14
+ export interface HubWorktree extends WorktreeInfo {
15
+ lastUsedTimestamp: number | null;
16
+ pullRequests: PullRequestInfo[];
17
+ }
18
+ export interface HubRepository {
19
+ current: boolean;
20
+ name: string;
21
+ pullRequests: PullRequestInfo[];
22
+ root: string;
23
+ worktrees: HubWorktree[];
24
+ }
25
+ export interface HubData {
26
+ currentRepository: string | null;
27
+ repositories: HubRepository[];
28
+ }
29
+ export declare function buildHubData(cwd: string, dependencies?: Partial<HubCommandDependencies>): Promise<HubData>;
30
+ export declare function runHubCommand(options: HubCommandOptions, dependencies?: Partial<HubCommandDependencies>): Promise<number>;
31
+ export declare function formatHubOutput(data: HubData, columns?: number, now?: number): string;
package/dist/hub.js ADDED
@@ -0,0 +1,287 @@
1
+ import { basename } from "node:path";
2
+ import { loadHistory } from "./history.js";
3
+ import { createPullRequestQuery, } from "./pull-requests.js";
4
+ import { detectRepository } from "./repo.js";
5
+ import { middleEllipsize, sanitizeTerminalText, terminalWidth, } from "./terminal-text.js";
6
+ import { formatRelativeAge, formatUpstreamState, readWorktreeInfos, } from "./worktree-info.js";
7
+ import { listDiscoverableWorktreeSources } from "./worktree-sources.js";
8
+ const MAX_HUB_REPOSITORY_CONCURRENCY = 4;
9
+ const defaultDependencies = {
10
+ queryRepositoryPullRequests: createPullRequestQuery().listOpenPullRequestsForRepository,
11
+ };
12
+ export async function buildHubData(cwd, dependencies = {}) {
13
+ const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
14
+ defaultDependencies.queryRepositoryPullRequests;
15
+ const currentRepository = await detectRepository(cwd).catch(() => null);
16
+ const history = await loadHistory();
17
+ const lastUsedByPath = new Map(history.map((entry) => [entry.path, entry.timestamp]));
18
+ const sources = await listDiscoverableWorktreeSources(cwd);
19
+ const groups = new Map();
20
+ for (const source of sources) {
21
+ if (!source.repoRoot)
22
+ continue;
23
+ const repoRoot = source.repoRoot;
24
+ const existing = groups.get(repoRoot);
25
+ if (existing) {
26
+ existing.sources.push(source);
27
+ }
28
+ else {
29
+ groups.set(repoRoot, {
30
+ name: source.repoName || basename(repoRoot),
31
+ sources: [source],
32
+ });
33
+ }
34
+ }
35
+ const repositories = await mapWithConcurrency([...groups.entries()], MAX_HUB_REPOSITORY_CONCURRENCY, async ([root, group]) => {
36
+ const infos = await readWorktreeInfos(group.sources.map((source) => source.worktree));
37
+ let pullRequests = [];
38
+ try {
39
+ pullRequests = await queryRepositoryPullRequests(root);
40
+ }
41
+ catch {
42
+ // PR metadata is optional. The hub remains useful when a provider is unavailable.
43
+ }
44
+ const worktrees = infos.map((info) => ({
45
+ ...info,
46
+ lastUsedTimestamp: lastUsedByPath.get(info.path) ?? null,
47
+ pullRequests: pullRequests.filter((pullRequest) => pullRequest.sourceBranch === info.branch),
48
+ }));
49
+ return {
50
+ current: currentRepository?.repoRoot === root,
51
+ name: group.name,
52
+ pullRequests,
53
+ root,
54
+ worktrees,
55
+ };
56
+ });
57
+ repositories.sort((left, right) => {
58
+ if (left.current !== right.current)
59
+ return left.current ? -1 : 1;
60
+ return left.name.localeCompare(right.name);
61
+ });
62
+ return {
63
+ currentRepository: currentRepository?.repoRoot ?? null,
64
+ repositories,
65
+ };
66
+ }
67
+ export async function runHubCommand(options, dependencies = {}) {
68
+ const data = await buildHubData(options.cwd, dependencies);
69
+ if (options.json) {
70
+ options.stdout(`${JSON.stringify(data, null, 2)}\n`);
71
+ return 0;
72
+ }
73
+ options.stdout(`${formatHubOutput(data, options.columns ?? process.stdout.columns ?? 80, options.now ?? Date.now())}\n`);
74
+ return 0;
75
+ }
76
+ export function formatHubOutput(data, columns = process.stdout.columns ?? 80, now = Date.now()) {
77
+ if (data.repositories.length === 0) {
78
+ return "No registered repositories. Run gji from a repository to add one.";
79
+ }
80
+ const lineWidth = Math.max(1, columns);
81
+ const allWorktrees = data.repositories.flatMap((repository) => repository.worktrees.map((worktree) => ({ repository, worktree })));
82
+ const attention = allWorktrees.filter(({ worktree }) => isActionable(worktree));
83
+ const current = allWorktrees.find(({ worktree }) => worktree.isCurrent) ?? null;
84
+ const next = chooseNextWorktree(allWorktrees);
85
+ const remainingAttention = attention.filter((entry) => entry !== current && entry !== next);
86
+ const quiet = allWorktrees.filter((entry) => entry !== current && entry !== next && !attention.includes(entry));
87
+ const recentOther = [...quiet]
88
+ .sort((left, right) => (right.worktree.lastCommitTimestamp ?? 0) -
89
+ (left.worktree.lastCommitTimestamp ?? 0))
90
+ .slice(0, 5);
91
+ const hiddenCount = quiet.length - recentOther.length;
92
+ const lines = [
93
+ `GJI ${data.repositories.length} repositories · ${allWorktrees.length} worktrees${attention.length > 0 ? ` · ${attention.length} need attention` : ""}`,
94
+ "",
95
+ ];
96
+ if (data.repositories.length > 1) {
97
+ lines.push("REPOSITORIES");
98
+ for (const repository of data.repositories) {
99
+ lines.push(formatRepositorySummary(repository, lineWidth));
100
+ }
101
+ lines.push("");
102
+ }
103
+ if (current !== null) {
104
+ lines.push("CURRENT");
105
+ lines.push(...formatHubWorktree(current, lineWidth, "@", now));
106
+ lines.push("");
107
+ }
108
+ if (next !== null && next !== current) {
109
+ lines.push("NEXT");
110
+ lines.push(...formatHubWorktree(next, lineWidth, "›", now));
111
+ lines.push("");
112
+ }
113
+ if (remainingAttention.length > 0) {
114
+ lines.push("ATTENTION");
115
+ for (const entry of remainingAttention) {
116
+ lines.push(...formatHubWorktree(entry, lineWidth, "!", now));
117
+ }
118
+ lines.push("");
119
+ }
120
+ if (recentOther.length > 0) {
121
+ lines.push("OTHER");
122
+ for (const entry of recentOther) {
123
+ lines.push(...formatHubWorktree(entry, lineWidth, " ", now));
124
+ }
125
+ if (hiddenCount > 0) {
126
+ lines.push(` ${hiddenCount} quiet worktree${hiddenCount === 1 ? "" : "s"} hidden`);
127
+ }
128
+ lines.push("");
129
+ }
130
+ if (next === null &&
131
+ remainingAttention.length === 0 &&
132
+ recentOther.length === 0) {
133
+ lines.push("No active worktrees.");
134
+ }
135
+ lines.push("Run `gji go <repo>/<branch>` to switch worktrees.");
136
+ return lines
137
+ .map((line) => terminalWidth(line) > lineWidth ? middleEllipsize(line, lineWidth) : line)
138
+ .join("\n")
139
+ .trimEnd();
140
+ }
141
+ function formatRepositorySummary(repository, lineWidth) {
142
+ const attentionCount = repository.worktrees.filter(isActionable).length;
143
+ const attention = attentionCount === 0 ? "healthy" : `${attentionCount} attention`;
144
+ const marker = repository.current ? "*" : " ";
145
+ return middleEllipsize(`${marker} ${repository.name} · ${repository.worktrees.length} worktree${repository.worktrees.length === 1 ? "" : "s"} · ${attention}`, lineWidth);
146
+ }
147
+ function chooseNextWorktree(entries) {
148
+ return ([...entries].sort((left, right) => {
149
+ const scoreDifference = nextWorktreeScore(right) - nextWorktreeScore(left);
150
+ if (scoreDifference !== 0)
151
+ return scoreDifference;
152
+ const lastUsedDifference = (right.worktree.lastUsedTimestamp ?? 0) -
153
+ (left.worktree.lastUsedTimestamp ?? 0);
154
+ if (lastUsedDifference !== 0)
155
+ return lastUsedDifference;
156
+ return ((right.worktree.lastCommitTimestamp ?? 0) -
157
+ (left.worktree.lastCommitTimestamp ?? 0));
158
+ })[0] ?? null);
159
+ }
160
+ function nextWorktreeScore(entry) {
161
+ const { worktree } = entry;
162
+ let score = worktree.isCurrent && (worktree.task !== null || isActionable(worktree))
163
+ ? 100
164
+ : 0;
165
+ if (worktree.task !== null)
166
+ score += 40;
167
+ if (worktree.status === "dirty")
168
+ score += 30;
169
+ if (worktree.upstream.kind === "stale")
170
+ score += 25;
171
+ if (worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0) {
172
+ score += 25;
173
+ }
174
+ if (worktree.pullRequests.length > 0)
175
+ score += 20;
176
+ if (worktree.lastUsedTimestamp !== null && worktree.lastUsedTimestamp > 0) {
177
+ score += 15;
178
+ }
179
+ return score;
180
+ }
181
+ function isActionable(worktree) {
182
+ return (worktree.status === "dirty" ||
183
+ worktree.upstream.kind === "stale" ||
184
+ (worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0) ||
185
+ worktree.pullRequests.length > 0);
186
+ }
187
+ function formatHubWorktree(entry, lineWidth, marker, now) {
188
+ const { repository, worktree } = entry;
189
+ const branch = worktree.branch ?? "(detached)";
190
+ const status = [
191
+ formatWorktreeHealth(worktree),
192
+ formatWorktreeUpstream(worktree),
193
+ worktree.pullRequests.length > 0
194
+ ? worktree.pullRequests
195
+ .map((pullRequest) => pullRequest.title
196
+ ? `#${pullRequest.number} ${formatHumanText(pullRequest.title, 60)}`
197
+ : `#${pullRequest.number}`)
198
+ .join(", ")
199
+ : null,
200
+ formatWorktreeActivity(worktree, now),
201
+ ]
202
+ .filter((value) => value !== null)
203
+ .join(" · ");
204
+ const task = worktree.task === null
205
+ ? null
206
+ : `task: ${formatHumanText(worktree.task, 80)}`;
207
+ const target = `${repository.name}/${branch}`;
208
+ const lines = [`${marker} ${middleEllipsize(target, lineWidth - 2)}`];
209
+ if (status.length > 0) {
210
+ lines.push(` ${middleEllipsize(status, lineWidth - 2)}`);
211
+ }
212
+ if (task !== null) {
213
+ lines.push(` ${middleEllipsize(task, lineWidth - 2)}`);
214
+ }
215
+ if (marker === "@" || marker === "›" || marker === "!") {
216
+ lines.push(` ${middleEllipsize(nextAction(worktree), lineWidth - 2)}`);
217
+ }
218
+ return lines;
219
+ }
220
+ function formatWorktreeHealth(worktree) {
221
+ if (worktree.status === "dirty")
222
+ return "! dirty";
223
+ if (worktree.status === "unknown")
224
+ return "? health unknown";
225
+ return null;
226
+ }
227
+ function formatWorktreeUpstream(worktree) {
228
+ if (worktree.upstream.kind === "stale")
229
+ return "! stale upstream";
230
+ if (worktree.upstream.kind === "tracked" &&
231
+ (worktree.upstream.behind > 0 || worktree.upstream.ahead > 0)) {
232
+ return formatUpstreamState(worktree.upstream);
233
+ }
234
+ return null;
235
+ }
236
+ function formatWorktreeActivity(worktree, now) {
237
+ if (worktree.isCurrent)
238
+ return null;
239
+ const timestamps = [
240
+ worktree.lastUsedTimestamp ?? 0,
241
+ (worktree.lastCommitTimestamp ?? 0) * 1000,
242
+ ].filter((timestamp) => timestamp > 0);
243
+ if (timestamps.length === 0)
244
+ return null;
245
+ const latest = Math.max(...timestamps);
246
+ const ageSeconds = Math.floor((now - latest) / 1000);
247
+ if (ageSeconds < 24 * 60 * 60)
248
+ return null;
249
+ return `inactive ${formatRelativeAge(Math.floor(latest / 1000), Math.floor(now / 1000))}`;
250
+ }
251
+ function nextAction(worktree) {
252
+ if (worktree.isCurrent && worktree.task !== null) {
253
+ return "next: continue task";
254
+ }
255
+ if (worktree.status === "dirty") {
256
+ return "next: switch → inspect changes";
257
+ }
258
+ if (worktree.upstream.kind === "stale" ||
259
+ (worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0)) {
260
+ return "next: switch → sync";
261
+ }
262
+ const pullRequest = worktree.pullRequests[0];
263
+ if (pullRequest !== undefined) {
264
+ return `next: switch → open PR '#${pullRequest.number}'`;
265
+ }
266
+ return "next: switch worktree";
267
+ }
268
+ function formatHumanText(value, maxLength) {
269
+ const sanitized = sanitizeTerminalText(value);
270
+ return sanitized.length <= maxLength
271
+ ? sanitized
272
+ : `${sanitized.slice(0, Math.max(0, maxLength - 1))}…`;
273
+ }
274
+ async function mapWithConcurrency(items, limit, mapper) {
275
+ const results = new Array(items.length);
276
+ let nextIndex = 0;
277
+ async function readNext() {
278
+ for (;;) {
279
+ const index = nextIndex++;
280
+ if (index >= items.length)
281
+ return;
282
+ results[index] = await mapper(items[index]);
283
+ }
284
+ }
285
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
286
+ return results;
287
+ }
package/dist/init.js CHANGED
@@ -12,49 +12,56 @@ import { SHELL_INTEGRATION_END_MARKER as END_MARKER, executableExists, hasShellI
12
12
  const ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
13
13
  const SHELL_WRAPPED_COMMANDS = [
14
14
  {
15
- bypassOptions: ["--help"],
15
+ bypassOptions: ["--help", "-h", "--json"],
16
16
  commandName: "new",
17
17
  envVar: "GJI_NEW_OUTPUT_FILE",
18
18
  names: ["new"],
19
19
  tempPrefix: "gji-new",
20
20
  },
21
21
  {
22
- bypassOptions: ["--help"],
22
+ bypassOptions: ["--help", "-h", "--json"],
23
23
  commandName: "pr",
24
24
  envVar: "GJI_PR_OUTPUT_FILE",
25
25
  names: ["pr"],
26
26
  tempPrefix: "gji-pr",
27
27
  },
28
28
  {
29
- bypassOptions: ["--print"],
29
+ bypassOptions: ["--print", "--help", "-h"],
30
30
  commandName: "back",
31
31
  envVar: "GJI_BACK_OUTPUT_FILE",
32
32
  names: ["back"],
33
33
  tempPrefix: "gji-back",
34
34
  },
35
35
  {
36
- bypassOptions: ["--print"],
36
+ bypassOptions: ["--print", "--json", "--help", "-h"],
37
37
  commandName: "go",
38
38
  envVar: "GJI_GO_OUTPUT_FILE",
39
39
  names: ["go", "jump"],
40
40
  tempPrefix: "gji-go",
41
41
  },
42
42
  {
43
- bypassOptions: ["--print"],
43
+ bypassOptions: ["--print", "--help", "-h"],
44
44
  commandName: "root",
45
45
  envVar: "GJI_ROOT_OUTPUT_FILE",
46
46
  names: ["root"],
47
47
  tempPrefix: "gji-root",
48
48
  },
49
49
  {
50
- bypassOptions: ["--help"],
50
+ bypassOptions: ["--help", "-h", "--json"],
51
51
  commandName: "remove",
52
52
  envVar: "GJI_REMOVE_OUTPUT_FILE",
53
53
  names: ["remove", "rm"],
54
54
  tempPrefix: "gji-remove",
55
55
  },
56
56
  {
57
- bypassOptions: ["--print", "--json"],
57
+ bypassOptions: ["--help", "-h", "--json"],
58
+ commandName: "done",
59
+ envVar: "GJI_DONE_OUTPUT_FILE",
60
+ names: ["done"],
61
+ tempPrefix: "gji-done",
62
+ },
63
+ {
64
+ bypassOptions: ["--print", "--json", "--help", "-h"],
58
65
  commandName: "warp",
59
66
  envVar: "GJI_WARP_OUTPUT_FILE",
60
67
  names: ["warp"],
@@ -405,16 +412,17 @@ function renderFishWrapper(command) {
405
412
  const nameCondition = nameTests.length === 1
406
413
  ? nameTests[0]
407
414
  : `begin; ${nameTests.join("; or ")}; end`;
408
- const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
409
- const bypassCondition = bypassTests.length === 1
410
- ? bypassTests[0]
411
- : `begin; ${bypassTests.join("; or ")}; end`;
415
+ const bypassBlock = `if test (count $argv) -gt 0
416
+ for arg in $argv
417
+ if ${command.bypassOptions.map((opt) => `test $arg = ${opt}`).join("; or ")}
418
+ command gji ${command.commandName} $argv
419
+ return $status
420
+ end
421
+ end
422
+ end`;
412
423
  return `if test (count $argv) -gt 0; and ${nameCondition}
413
424
  set -e argv[1]
414
- if test (count $argv) -gt 0; and ${bypassCondition}
415
- command gji ${command.commandName} $argv
416
- return $status
417
- end
425
+ ${bypassBlock}
418
426
 
419
427
  set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
420
428
  or return 1
@@ -434,15 +442,15 @@ function renderPosixWrapper(command) {
434
442
  const tests = command.names
435
443
  .map((name) => `[ "$1" = "${name}" ]`)
436
444
  .join(" || ");
437
- const bypassTests = command.bypassOptions
438
- .map((opt) => `[ "\${1:-}" = "${opt}" ]`)
439
- .join(" || ");
445
+ const bypassBlock = `for arg do
446
+ if ${command.bypassOptions.map((opt) => `[ "$arg" = "${opt}" ]`).join(" || ")}; then
447
+ command gji ${command.commandName} "$@"
448
+ return $?
449
+ fi
450
+ done`;
440
451
  return `if ${tests}; then
441
452
  shift
442
- if ${bypassTests}; then
443
- command gji ${command.commandName} "$@"
444
- return $?
445
- fi
453
+ ${bypassBlock}
446
454
 
447
455
  local target
448
456
  local output_file
@@ -9,3 +9,4 @@ export interface InstallPromptDependencies {
9
9
  writeGlobalRepoConfigKey?: (repoRoot: string, key: string, value: unknown) => Promise<void>;
10
10
  }
11
11
  export declare function maybeRunInstallPrompt(worktreePath: string, repoRoot: string, config: GjiConfig, stderr: (chunk: string) => void, dependencies?: InstallPromptDependencies, nonInteractive?: boolean): Promise<void>;
12
+ export declare const runInstallCommand: import("./command-runner.js").CommandRunner;
@@ -1,5 +1,5 @@
1
- import { spawn } from "node:child_process";
2
1
  import { isCancel, select } from "@clack/prompts";
2
+ import { runCommand } from "./command-runner.js";
3
3
  import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
4
4
  import { isHeadless } from "./headless.js";
5
5
  import { extractHooks } from "./hooks.js";
@@ -28,7 +28,7 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
28
28
  return;
29
29
  }
30
30
  if (choice === "yes" || choice === "always") {
31
- const runner = dependencies.runInstallCommand ?? defaultRunInstallCommand;
31
+ const runner = dependencies.runInstallCommand ?? runInstallCommand;
32
32
  try {
33
33
  await runner(pm.installCommand, worktreePath, stderr);
34
34
  }
@@ -88,29 +88,7 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
88
88
  }
89
89
  }
90
90
  }
91
- async function defaultRunInstallCommand(command, cwd, stderr) {
92
- await new Promise((resolve, reject) => {
93
- const child = spawn(command, {
94
- cwd,
95
- shell: true,
96
- stdio: ["ignore", "inherit", "pipe"],
97
- });
98
- child.stderr.on("data", (chunk) => {
99
- stderr(chunk.toString());
100
- });
101
- child.on("close", (code) => {
102
- if (code !== 0) {
103
- reject(new Error(`exited with code ${code}`));
104
- }
105
- else {
106
- resolve();
107
- }
108
- });
109
- child.on("error", (err) => {
110
- reject(err);
111
- });
112
- });
113
- }
91
+ export const runInstallCommand = runCommand;
114
92
  async function defaultWriteConfigKey(root, key, value) {
115
93
  await updateLocalConfigKey(root, key, value);
116
94
  }
package/dist/ls.js CHANGED
@@ -26,12 +26,15 @@ export function formatDetailedWorktreeTable(worktrees) {
26
26
  lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
27
27
  path: worktree.path,
28
28
  status: worktree.status,
29
+ task: worktree.task,
29
30
  upstream: formatUpstreamState(worktree.upstream),
30
31
  }));
31
32
  const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
32
33
  const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
33
34
  const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
34
35
  const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
36
+ const hasTasks = rows.some((row) => row.task !== null);
37
+ const taskWidth = Math.max("TASK".length, ...rows.map((row) => row.task?.slice(0, 40).length ?? 0));
35
38
  const lines = [
36
39
  " " +
37
40
  "BRANCH".padEnd(branchWidth, " ") +
@@ -41,6 +44,7 @@ export function formatDetailedWorktreeTable(worktrees) {
41
44
  "UPSTREAM".padEnd(upstreamWidth, " ") +
42
45
  " " +
43
46
  "LAST".padEnd(lastCommitWidth, " ") +
47
+ (hasTasks ? " " + "TASK".padEnd(taskWidth, " ") : "") +
44
48
  " PATH",
45
49
  ];
46
50
  for (const row of rows) {
@@ -48,7 +52,11 @@ export function formatDetailedWorktreeTable(worktrees) {
48
52
  `${row.branch.padEnd(branchWidth, " ")} ` +
49
53
  `${row.status.padEnd(statusWidth, " ")} ` +
50
54
  `${row.upstream.padEnd(upstreamWidth, " ")} ` +
51
- `${row.lastCommit.padEnd(lastCommitWidth, " ")} ` +
55
+ `${row.lastCommit.padEnd(lastCommitWidth, " ")}` +
56
+ (hasTasks
57
+ ? ` ${(row.task?.slice(0, 40) ?? "").padEnd(taskWidth, " ")}`
58
+ : "") +
59
+ " " +
52
60
  row.path);
53
61
  }
54
62
  return lines.join("\n");
@@ -0,0 +1,11 @@
1
+ export interface NavigationRepository {
2
+ name: string;
3
+ root: string;
4
+ }
5
+ export interface NavigationTarget {
6
+ branch: string | null;
7
+ path: string;
8
+ repository: NavigationRepository;
9
+ }
10
+ export declare function createNavigationTarget(repository: NavigationRepository, path: string, branch: string | null): NavigationTarget;
11
+ export declare function createNavigationRepository(name: string, root: string): NavigationRepository;
@@ -0,0 +1,6 @@
1
+ export function createNavigationTarget(repository, path, branch) {
2
+ return { branch, path, repository };
3
+ }
4
+ export function createNavigationRepository(name, root) {
5
+ return { name, root };
6
+ }
package/dist/new.d.ts CHANGED
@@ -1,22 +1,34 @@
1
1
  import { type PathConflictChoice } from "./conflict.js";
2
- import { type InstallPromptDependencies } from "./install-prompt.js";
2
+ import { type DependencyBootstrapPromptDependencies } from "./dependency-bootstrap-prompt.js";
3
+ import { type CloneDirectory } from "./dir-clone.js";
4
+ import type { InstallPromptDependencies } from "./install-prompt.js";
3
5
  export type { PathConflictChoice };
6
+ export type NewWorktreeMode = "create" | "checkout" | "track";
4
7
  export interface NewCommandOptions {
5
8
  branch?: string;
9
+ copy?: boolean;
6
10
  cwd: string;
7
11
  detached?: boolean;
8
12
  dryRun?: boolean;
9
13
  editor?: string;
10
14
  fromCurrent?: boolean;
11
15
  force?: boolean;
16
+ noFetch?: boolean;
12
17
  json?: boolean;
18
+ mode?: NewWorktreeMode;
13
19
  open?: boolean;
20
+ outputEnv?: string;
21
+ remote?: string;
22
+ take?: boolean;
14
23
  stderr: (chunk: string) => void;
15
24
  stdout: (chunk: string) => void;
25
+ task?: string;
16
26
  }
17
- export interface NewCommandDependencies extends InstallPromptDependencies {
27
+ export interface NewCommandDependencies extends InstallPromptDependencies, DependencyBootstrapPromptDependencies {
28
+ cloneDir: CloneDirectory;
18
29
  createBranchPlaceholder: () => string;
19
30
  promptForBranch: (placeholder: string) => Promise<string | null>;
31
+ promptForFetchFailure: (message: string) => Promise<boolean>;
20
32
  promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
21
33
  spawnEditor: (cli: string, args: string[]) => Promise<void>;
22
34
  }