@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
@@ -2,38 +2,81 @@ import { env, platform, stdin, stdout } from "node:process";
2
2
  import { isCancel, Prompt } from "@clack/core";
3
3
  import { loadHistory } from "./history.js";
4
4
  import { createPullRequestQuery, } from "./pull-requests.js";
5
+ import { readTask } from "./task.js";
6
+ import { middleEllipsize, sanitizeTerminalText, startEllipsize, terminalWidth, } from "./terminal-text.js";
5
7
  import { readWorktreeInfos, } from "./worktree-info.js";
6
8
  const MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY = 4;
9
+ const MAX_TASK_READ_CONCURRENCY = 8;
7
10
  export async function buildWorktreePromptEntries(sources, dependencies = {}) {
11
+ const metadataMode = dependencies.metadata ?? "full";
12
+ const includeMetadata = metadataMode === "full";
8
13
  const pullRequestQuery = createPullRequestQuery();
9
14
  const queryPullRequests = dependencies.queryPullRequests ?? pullRequestQuery.listOpenPullRequests;
10
- const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
11
- (dependencies.queryPullRequests === undefined
12
- ? pullRequestQuery.listOpenPullRequestsForRepository
13
- : undefined);
15
+ const queryRepositoryPullRequests = includeMetadata
16
+ ? (dependencies.queryRepositoryPullRequests ??
17
+ (dependencies.queryPullRequests === undefined
18
+ ? pullRequestQuery.listOpenPullRequestsForRepository
19
+ : undefined))
20
+ : undefined;
14
21
  const repositoryPullRequests = queryRepositoryPullRequests === undefined
15
22
  ? Promise.resolve(null)
16
23
  : readRepositoryPullRequests(sources, queryRepositoryPullRequests);
17
24
  const [history, infos, pullRequestsByRepository] = await Promise.all([
18
25
  loadHistory(),
19
- readWorktreeInfos(sources.map((source) => source.worktree)),
26
+ includeMetadata
27
+ ? readWorktreeInfos(sources.map((source) => source.worktree))
28
+ : mapWithConcurrency(sources, MAX_TASK_READ_CONCURRENCY, (source) => createUnhydratedWorktreeInfo(source.worktree)),
20
29
  repositoryPullRequests,
21
30
  ]);
22
- const pullRequests = await Promise.all(sources.map((source) => readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository)));
31
+ const pullRequests = includeMetadata
32
+ ? await Promise.all(sources.map((source) => readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository)))
33
+ : sources.map(() => []);
23
34
  const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
24
35
  const entries = sources.map((source, index) => buildWorktreePromptEntry(source, infos[index], pullRequests[index], historyByPath.get(source.worktree.path)?.timestamp ?? null, Date.now()));
25
36
  return entries
26
37
  .sort(comparePromptEntries)
27
38
  .map(({ lastActivityTimestamp: _lastActivityTimestamp, ...entry }) => entry);
28
39
  }
40
+ async function createUnhydratedWorktreeInfo(worktree) {
41
+ let task = null;
42
+ try {
43
+ task = (await readTask(worktree.path))?.task ?? null;
44
+ }
45
+ catch {
46
+ // Task metadata is optional picker decoration.
47
+ }
48
+ return {
49
+ ...worktree,
50
+ lastCommitTimestamp: null,
51
+ slot: null,
52
+ status: "unknown",
53
+ task,
54
+ upstream: { kind: "unknown" },
55
+ };
56
+ }
29
57
  export function resolveWorktreeQuery(sources, query) {
58
+ return resolveWorktreeQueryMatches(sources, query)[0] ?? null;
59
+ }
60
+ export function resolveWorktreeQueryMatches(sources, query) {
30
61
  const normalizedQuery = normalizeQuery(query);
31
62
  if (normalizedQuery === null)
32
- return null;
63
+ return [];
33
64
  const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
34
65
  if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery))
35
- return null;
36
- return matches[0]?.source ?? null;
66
+ return [];
67
+ const bestScore = matches[0]?.matchScore;
68
+ return matches
69
+ .filter((match) => match.matchScore === bestScore)
70
+ .map((match) => match.source);
71
+ }
72
+ export function resolveExactWorktreeQueryMatches(sources, query) {
73
+ const normalizedQuery = normalizeQuery(query);
74
+ if (normalizedQuery === null)
75
+ return [];
76
+ return sources.filter((source) => scoreWorktreeMatch({
77
+ ...source.worktree,
78
+ repoName: source.repoName,
79
+ }, normalizedQuery) === 1000);
37
80
  }
38
81
  function findWorktreePromptSourceMatches(sources, normalizedQuery) {
39
82
  return sources
@@ -59,6 +102,7 @@ export async function promptForSingleWorktree(message, worktrees, io = {}) {
59
102
  message,
60
103
  multiple: false,
61
104
  output: io.output,
105
+ scope: io.scope,
62
106
  });
63
107
  return typeof choice === "string" ? choice : null;
64
108
  }
@@ -116,17 +160,24 @@ function buildSearchableWorktreeEntry(worktree) {
116
160
  const metadata = worktree.metadata ?? null;
117
161
  const branch = worktree.branch ?? "(detached)";
118
162
  return {
119
- detail: [worktree.repoName, branch, metadata, worktree.path]
163
+ detail: [
164
+ worktree.repoName,
165
+ branch,
166
+ metadata,
167
+ worktree.task == null ? null : sanitizeTerminalText(worktree.task),
168
+ worktree.path,
169
+ ]
120
170
  .filter((part) => part !== null && part.length > 0)
121
171
  .join(" · "),
122
172
  label: worktree.label,
123
173
  searchText: buildPromptSearchText(worktree),
124
174
  value: worktree.path,
125
175
  worktree: {
126
- branch: formatWorktreeBranchLabel(worktree.branch, worktree.pullRequestNumbers),
176
+ branch: formatWorktreeBranchLabel(worktree.branch, worktree.pullRequests),
127
177
  metadata,
128
178
  path: worktree.path,
129
179
  repoName: worktree.repoName,
180
+ task: worktree.task == null ? null : sanitizeTerminalText(worktree.task),
130
181
  },
131
182
  };
132
183
  }
@@ -144,13 +195,18 @@ class SearchablePrompt {
144
195
  options;
145
196
  output;
146
197
  cursor = 0;
198
+ entries;
147
199
  query = "";
148
200
  searchActive = false;
149
201
  selected = new Set();
202
+ scope;
203
+ scopeTogglePending = false;
150
204
  prompt;
151
205
  constructor(options, input, output) {
152
206
  this.options = options;
153
207
  this.output = output;
208
+ this.entries = options.entries;
209
+ this.scope = options.scope;
154
210
  this.cursor = this.firstSelectableIndex();
155
211
  const owner = this;
156
212
  this.prompt = new WorktreeCorePrompt(this, {
@@ -183,6 +239,10 @@ class SearchablePrompt {
183
239
  this.submit(prompt);
184
240
  return;
185
241
  }
242
+ if (isScopeToggleKey(character, key)) {
243
+ void this.toggleScope(prompt);
244
+ return;
245
+ }
186
246
  if (this.searchActive && this.handleSearchKey(character, key?.name)) {
187
247
  this.syncValue();
188
248
  renderPrompt(prompt);
@@ -200,6 +260,34 @@ class SearchablePrompt {
200
260
  this.syncValue();
201
261
  renderPrompt(prompt);
202
262
  }
263
+ async toggleScope(prompt) {
264
+ const currentScope = this.scope;
265
+ if (currentScope === undefined || this.scopeTogglePending)
266
+ return;
267
+ this.scopeTogglePending = true;
268
+ renderPrompt(prompt);
269
+ try {
270
+ const nextScope = await currentScope.toggle();
271
+ this.entries = nextScope.entries.map(buildSearchableWorktreeEntry);
272
+ this.scope = {
273
+ label: nextScope.label,
274
+ toggle: currentScope.toggle,
275
+ toggleLabel: nextScope.toggleLabel,
276
+ };
277
+ this.query = "";
278
+ this.cursor = this.firstSelectableIndex();
279
+ this.syncValue();
280
+ }
281
+ catch (error) {
282
+ prompt.error =
283
+ error instanceof Error ? error.message : "Could not load worktrees";
284
+ prompt.state = "error";
285
+ }
286
+ finally {
287
+ this.scopeTogglePending = false;
288
+ renderPrompt(prompt);
289
+ }
290
+ }
203
291
  handleEscapeKey(prompt) {
204
292
  if (this.searchActive) {
205
293
  this.searchActive = false;
@@ -334,20 +422,40 @@ class SearchablePrompt {
334
422
  renderTitle(state, glyphs, colors) {
335
423
  const symbol = colors.symbol(promptSymbol(state, glyphs), state);
336
424
  const maxTitleLength = Math.max(8, this.columns() - 3);
425
+ const title = this.scope
426
+ ? `${this.options.message} · ${this.scope.label}`
427
+ : this.options.message;
337
428
  if (!this.searchActive) {
338
- return `${symbol} ${middleEllipsize(this.options.message, maxTitleLength)}`;
429
+ return `${symbol} ${middleEllipsize(title, maxTitleLength)}`;
339
430
  }
340
431
  const search = ` /${this.query}`;
341
432
  const maxSearchLength = Math.min(terminalWidth(search), Math.floor(maxTitleLength / 2));
342
- const message = middleEllipsize(this.options.message, Math.max(1, maxTitleLength - maxSearchLength));
433
+ const message = middleEllipsize(title, Math.max(1, maxTitleLength - maxSearchLength));
343
434
  const visibleSearch = middleEllipsize(search, Math.max(1, maxTitleLength - terminalWidth(message)));
344
435
  return `${symbol} ${message}${colors.search(visibleSearch)}`;
345
436
  }
346
437
  renderSearchHint(colors) {
438
+ const scopeHint = this.scope
439
+ ? `${colors.key("tab")} ${colors.hint(this.scope.toggleLabel)}`
440
+ : null;
441
+ const separator = colors.hint(" · ");
442
+ if (this.scopeTogglePending) {
443
+ return `${colors.hint("loading worktrees")} ${separator}${scopeHint}`;
444
+ }
347
445
  if (this.searchActive) {
348
- return `${colors.hint("type to filter")} ${colors.hint("·")} ${colors.key("esc")} ${colors.hint("clears")}`;
446
+ return [
447
+ `${colors.hint("type to filter")} ${separator} ${colors.key("esc")} ${colors.hint("clears")}`,
448
+ scopeHint,
449
+ ]
450
+ .filter((hint) => hint !== null)
451
+ .join(separator);
349
452
  }
350
- return `${colors.hint("press ")}${colors.key("/")}${colors.hint(" to search")}`;
453
+ return [
454
+ `${colors.hint("press ")}${colors.key("/")}${colors.hint(" to search")}`,
455
+ scopeHint,
456
+ ]
457
+ .filter((hint) => hint !== null)
458
+ .join(separator);
351
459
  }
352
460
  renderEntries(entries, visibleEntries, glyphs, colors, frame) {
353
461
  if (entries.length === 0) {
@@ -399,6 +507,7 @@ class SearchablePrompt {
399
507
  ellipsize: middleEllipsize,
400
508
  max: 18,
401
509
  min: 6,
510
+ removablePriority: entry.worktree.task === null ? undefined : 1,
402
511
  value: entry.worktree.repoName,
403
512
  },
404
513
  {
@@ -414,13 +523,25 @@ class SearchablePrompt {
414
523
  ellipsize: middleEllipsize,
415
524
  max: 30,
416
525
  min: 10,
526
+ removablePriority: entry.worktree.task === null ? undefined : 0,
417
527
  value: entry.worktree.metadata,
418
528
  },
419
529
  ]),
530
+ ...(entry.worktree.task === null
531
+ ? []
532
+ : [
533
+ {
534
+ ellipsize: middleEllipsize,
535
+ max: 36,
536
+ min: 10,
537
+ value: entry.worktree.task,
538
+ },
539
+ ]),
420
540
  {
421
541
  ellipsize: startEllipsize,
422
542
  max: 36,
423
543
  min: 12,
544
+ removablePriority: entry.worktree.task === null ? undefined : 2,
424
545
  value: entry.worktree.path,
425
546
  },
426
547
  ], width);
@@ -460,9 +581,9 @@ class SearchablePrompt {
460
581
  visibleEntries() {
461
582
  const query = normalizeQuery(this.query);
462
583
  if (query === null) {
463
- return this.options.entries;
584
+ return this.entries;
464
585
  }
465
- return filterSearchablePromptEntries(this.options.entries, query);
586
+ return filterSearchablePromptEntries(this.entries, query);
466
587
  }
467
588
  maxItems() {
468
589
  const rows = this.output.rows ?? 16;
@@ -505,6 +626,9 @@ function closePrompt(prompt) {
505
626
  function onWorktreeKeypress(character, key) {
506
627
  this.worktreePrompt.handleKeypress(this, character, key);
507
628
  }
629
+ function isScopeToggleKey(character, key) {
630
+ return key?.name === "tab" || character === "\t";
631
+ }
508
632
  function resolvePromptAction(character, key) {
509
633
  if ((key?.ctrl && key.name === "c") || character === "\u0003") {
510
634
  return "cancel";
@@ -653,13 +777,25 @@ function windowPromptEntries(entries, cursor, maxItems) {
653
777
  }
654
778
  return window;
655
779
  }
780
+ function findPromptPieceToRemove(pieces, fallbackIndex) {
781
+ let candidate = fallbackIndex;
782
+ let priority = Infinity;
783
+ for (let index = 0; index < pieces.length; index += 1) {
784
+ const piecePriority = pieces[index].removablePriority;
785
+ if (piecePriority !== undefined && piecePriority < priority) {
786
+ candidate = index;
787
+ priority = piecePriority;
788
+ }
789
+ }
790
+ return candidate;
791
+ }
656
792
  function fitPromptPieces(pieces, width) {
657
793
  const separator = " · ";
658
794
  let visiblePieces = pieces.filter((piece) => piece.value.length > 0);
659
795
  let available = width - terminalWidth(separator) * Math.max(0, visiblePieces.length - 1);
660
796
  while (visiblePieces.length > 1 &&
661
797
  available < minimumPieceLength(visiblePieces)) {
662
- const removableIndex = visiblePieces.length === 4 ? 2 : visiblePieces.length - 2;
798
+ const removableIndex = findPromptPieceToRemove(visiblePieces, visiblePieces.length === 4 ? 2 : visiblePieces.length - 2);
663
799
  visiblePieces = visiblePieces.filter((_, index) => index !== removableIndex);
664
800
  available =
665
801
  width - terminalWidth(separator) * Math.max(0, visiblePieces.length - 1);
@@ -727,9 +863,13 @@ function buildPromptSearchText(worktree) {
727
863
  worktree.branch ?? "detached",
728
864
  worktree.path,
729
865
  worktree.label,
866
+ worktree.task ?? "",
730
867
  `${worktree.repoName}/${worktree.branch ?? "detached"}`,
731
- ...(worktree.pullRequestNumbers ?? []).map((number) => `#${number}`),
732
- ...(worktree.pullRequestUrls ?? []),
868
+ ...(worktree.pullRequests ?? []).flatMap((pullRequest) => [
869
+ `#${pullRequest.number}`,
870
+ pullRequest.title ?? "",
871
+ pullRequest.url,
872
+ ]),
733
873
  ]
734
874
  .join(" ")
735
875
  .toLowerCase();
@@ -742,7 +882,7 @@ function buildWorktreePromptEntry(source, info, pullRequests, lastUsedTimestamp,
742
882
  : lastWorkedTimestamp !== null
743
883
  ? "worked"
744
884
  : null;
745
- const branch = formatWorktreeBranchLabel(source.worktree.branch, pullRequests.map((pullRequest) => pullRequest.number));
885
+ const branch = formatWorktreeBranchLabel(source.worktree.branch, pullRequests);
746
886
  const badges = buildStatusBadges(info);
747
887
  const recency = formatPromptRecency(lastActivityTimestamp, lastActivityType, now);
748
888
  const status = badges.length > 0 ? badges.map((badge) => `[${badge}]`).join(" ") : null;
@@ -753,6 +893,9 @@ function buildWorktreePromptEntry(source, info, pullRequests, lastUsedTimestamp,
753
893
  middleEllipsize(source.repoName, 22),
754
894
  middleEllipsize(branch, 34),
755
895
  metadata,
896
+ info.task === null
897
+ ? null
898
+ : middleEllipsize(sanitizeTerminalText(info.task), 36),
756
899
  path,
757
900
  ]
758
901
  .filter((part) => part !== null && part.length > 0)
@@ -763,9 +906,9 @@ function buildWorktreePromptEntry(source, info, pullRequests, lastUsedTimestamp,
763
906
  label,
764
907
  lastActivityTimestamp,
765
908
  metadata,
766
- pullRequestNumbers: pullRequests.map((pullRequest) => pullRequest.number),
767
- pullRequestUrls: pullRequests.map((pullRequest) => pullRequest.url),
909
+ pullRequests,
768
910
  repoName: source.repoName,
911
+ task: info.task,
769
912
  };
770
913
  }
771
914
  async function readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository) {
@@ -823,11 +966,18 @@ async function mapWithConcurrency(items, limit, mapper) {
823
966
  function sortSourcePullRequests(pullRequests) {
824
967
  return [...(pullRequests ?? [])].sort((a, b) => a.number - b.number);
825
968
  }
826
- export function formatWorktreeBranchLabel(branch, pullRequestNumbers = []) {
969
+ export function formatWorktreeBranchLabel(branch, pullRequests = []) {
827
970
  const branchLabel = branch ?? "(detached)";
828
- if (pullRequestNumbers.length === 0)
971
+ if (pullRequests.length === 0)
829
972
  return branchLabel;
830
- return `${branchLabel} (${pullRequestNumbers.map((number) => `#${number}`).join(", ")})`;
973
+ return `${branchLabel} (${pullRequests
974
+ .map((pullRequest) => {
975
+ const title = pullRequest.title
976
+ ? ` ${middleEllipsize(sanitizeTerminalText(pullRequest.title), 48)}`
977
+ : "";
978
+ return `#${pullRequest.number}${title}`;
979
+ })
980
+ .join(", ")})`;
831
981
  }
832
982
  function buildStatusBadges(info) {
833
983
  const badges = [];
@@ -931,72 +1081,3 @@ function scoreWorktreeMatch(entry, query) {
931
1081
  }
932
1082
  return buildSearchText(entry.repoName, entry).includes(query) ? 1 : null;
933
1083
  }
934
- function middleEllipsize(value, maxLength) {
935
- if (terminalWidth(value) <= maxLength) {
936
- return value;
937
- }
938
- if (maxLength <= 1) {
939
- return "…";
940
- }
941
- const keep = maxLength - 1;
942
- const start = takeTerminalColumns(value, Math.ceil(keep / 2), "start");
943
- const end = takeTerminalColumns(value, Math.floor(keep / 2), "end");
944
- return `${start}…${end}`;
945
- }
946
- function startEllipsize(value, maxLength) {
947
- if (terminalWidth(value) <= maxLength) {
948
- return value;
949
- }
950
- if (maxLength <= 1) {
951
- return "…";
952
- }
953
- return `…${takeTerminalColumns(value, maxLength - 1, "end")}`;
954
- }
955
- function takeTerminalColumns(value, maxWidth, direction) {
956
- const characters = direction === "start" ? Array.from(value) : Array.from(value).reverse();
957
- const kept = [];
958
- let width = 0;
959
- for (const character of characters) {
960
- const characterWidth = terminalWidth(character);
961
- if (width + characterWidth > maxWidth)
962
- break;
963
- kept.push(character);
964
- width += characterWidth;
965
- }
966
- return direction === "start" ? kept.join("") : kept.reverse().join("");
967
- }
968
- function terminalWidth(value) {
969
- let width = 0;
970
- for (const character of Array.from(value)) {
971
- width += characterTerminalWidth(character);
972
- }
973
- return width;
974
- }
975
- function characterTerminalWidth(character) {
976
- const codePoint = character.codePointAt(0);
977
- if (codePoint === undefined)
978
- return 0;
979
- if (isZeroWidthCodePoint(codePoint) || /\p{Mark}/u.test(character)) {
980
- return 0;
981
- }
982
- return isWideCodePoint(codePoint) ? 2 : 1;
983
- }
984
- function isZeroWidthCodePoint(codePoint) {
985
- return (codePoint === 0 ||
986
- codePoint === 0x200d ||
987
- (codePoint >= 0xfe00 && codePoint <= 0xfe0f));
988
- }
989
- function isWideCodePoint(codePoint) {
990
- return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
991
- codePoint === 0x2329 ||
992
- codePoint === 0x232a ||
993
- (codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
994
- (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
995
- (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
996
- (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
997
- (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
998
- (codePoint >= 0xff00 && codePoint <= 0xff60) ||
999
- (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
1000
- (codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
1001
- (codePoint >= 0x20000 && codePoint <= 0x3fffd));
1002
- }
@@ -4,7 +4,7 @@ export async function defaultConfirmForceRemoveWorktree(worktreePath) {
4
4
  active: "Yes",
5
5
  inactive: "No",
6
6
  initialValue: false,
7
- message: `Worktree at ${worktreePath} has untracked or modified files. Force remove?`,
7
+ message: `Worktree at ${worktreePath} contains files or initialized submodules that cannot be undone. Force remove?`,
8
8
  });
9
9
  return !isCancel(choice) && choice;
10
10
  }
@@ -0,0 +1,6 @@
1
+ import type { WorktreeEntry } from "./repo.js";
2
+ export interface WorktreeSource {
3
+ repoRoot?: string;
4
+ repoName: string;
5
+ worktree: WorktreeEntry;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { type RepoRegistryEntry } from "./repo-registry.js";
2
+ import type { WorktreeSource } from "./worktree-source.js";
3
+ export declare function listRegisteredWorktreeSources(cwd: string, onSkipped?: (entry: RepoRegistryEntry) => void): Promise<WorktreeSource[]>;
4
+ export declare function listDiscoverableWorktreeSources(cwd: string, onSkipped?: (entry: RepoRegistryEntry) => void): Promise<WorktreeSource[]>;
@@ -0,0 +1,77 @@
1
+ import { detectRepository, listWorktrees } from "./repo.js";
2
+ import { loadRegistry } from "./repo-registry.js";
3
+ const MAX_REPOSITORY_DISCOVERY_CONCURRENCY = 4;
4
+ export async function listRegisteredWorktreeSources(cwd, onSkipped) {
5
+ const registry = await loadRegistry();
6
+ const currentRoot = await detectRepository(cwd)
7
+ .then((repository) => repository.currentRoot)
8
+ .catch(() => null);
9
+ const results = await mapWithConcurrency(registry, MAX_REPOSITORY_DISCOVERY_CONCURRENCY, async (entry) => {
10
+ try {
11
+ const worktrees = await listWorktrees(entry.path);
12
+ return { entry, worktrees };
13
+ }
14
+ catch {
15
+ onSkipped?.(entry);
16
+ return null;
17
+ }
18
+ });
19
+ const allItems = [];
20
+ for (const result of results) {
21
+ if (result === null)
22
+ continue;
23
+ const { entry, worktrees } = result;
24
+ for (const worktree of worktrees) {
25
+ allItems.push({
26
+ repoRoot: entry.path,
27
+ repoName: entry.name,
28
+ worktree: {
29
+ ...worktree,
30
+ isCurrent: currentRoot !== null && worktree.path === currentRoot,
31
+ },
32
+ });
33
+ }
34
+ }
35
+ return allItems;
36
+ }
37
+ export async function listDiscoverableWorktreeSources(cwd, onSkipped) {
38
+ const currentRepository = await detectRepository(cwd).catch(() => null);
39
+ const registeredSources = await listRegisteredWorktreeSources(cwd, onSkipped);
40
+ if (currentRepository === null)
41
+ return dedupeSources(registeredSources);
42
+ let currentSources = [];
43
+ try {
44
+ currentSources = (await listWorktrees(cwd)).map((worktree) => ({
45
+ repoName: currentRepository.repoName,
46
+ repoRoot: currentRepository.repoRoot,
47
+ worktree,
48
+ }));
49
+ }
50
+ catch {
51
+ // Registered repositories remain discoverable when the current checkout is transiently unavailable.
52
+ }
53
+ return dedupeSources([...currentSources, ...registeredSources]);
54
+ }
55
+ async function mapWithConcurrency(items, limit, mapper) {
56
+ const results = new Array(items.length);
57
+ let nextIndex = 0;
58
+ async function readNext() {
59
+ for (;;) {
60
+ const index = nextIndex++;
61
+ if (index >= items.length)
62
+ return;
63
+ results[index] = await mapper(items[index]);
64
+ }
65
+ }
66
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
67
+ return results;
68
+ }
69
+ function dedupeSources(sources) {
70
+ const seen = new Set();
71
+ return sources.filter((source) => {
72
+ if (seen.has(source.worktree.path))
73
+ return false;
74
+ seen.add(source.worktree.path);
75
+ return true;
76
+ });
77
+ }
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-BACK 1 "July 2026" "gji 0.12.0" "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 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-CLEAN 1 "July 2026" "gji 0.12.0" "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 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-COMPLETION 1 "July 2026" "gji 0.12.0" "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 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-CONFIG 1 "July 2026" "gji 0.12.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-config \- manage global config defaults
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-DOCTOR 1 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-DOCTOR 1 "July 2026" "gji 0.12.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-doctor \- check gji installation and configuration health
4
4
  .SH SYNOPSIS
@@ -0,0 +1,19 @@
1
+ .TH GJI\-DONE 1 "July 2026" "gji 0.12.0" "User Commands"
2
+ .SH NAME
3
+ gji\-done \- finish the current linked worktree and return safely
4
+ .SH SYNOPSIS
5
+ .B gji done [\fIoptions\fR] [options] [branch]
6
+ .SH DESCRIPTION
7
+ finish the current linked worktree and return safely
8
+ .SH OPTIONS
9
+ .TP
10
+ .B \-\-force
11
+ remove dirty or unmerged worktrees without prompting
12
+ .TP
13
+ .B \-\-keep\-branch
14
+ remove the worktree but preserve its branch
15
+ .TP
16
+ .B \-\-json
17
+ emit JSON on success or error instead of human\-readable output
18
+ .SH "SEE ALSO"
19
+ .BR gji (1)
package/man/man1/gji-go.1 CHANGED
@@ -1,13 +1,22 @@
1
- .TH GJI\-GO 1 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-GO 1 "July 2026" "gji 0.12.0" "User Commands"
2
2
  .SH NAME
3
- gji\-go \- print or select a worktree path
3
+ gji\-go \- resolve and jump to a worktree path
4
4
  .SH SYNOPSIS
5
5
  .B gji go [\fIoptions\fR] [options] [branch]
6
6
  .SH DESCRIPTION
7
- print or select a worktree path
7
+ resolve and jump to a worktree path
8
8
  .SH OPTIONS
9
9
  .TP
10
+ .B \-\-root
11
+ navigate to the main repository root
12
+ .TP
10
13
  .B \-\-print
11
14
  print the resolved worktree path explicitly
15
+ .TP
16
+ .B \-\-quiet
17
+ suppress the context card
18
+ .TP
19
+ .B \-\-json
20
+ emit JSON for an existing worktree destination
12
21
  .SH "SEE ALSO"
13
22
  .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-HISTORY 1 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-HISTORY 1 "July 2026" "gji 0.12.0" "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 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-INIT 1 "July 2026" "gji 0.12.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-init \- set up shell integration interactively or print a shell wrapper
4
4
  .SH SYNOPSIS
package/man/man1/gji-ls.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-LS 1 "July 2026" "gji 0.10.0" "User Commands"
1
+ .TH GJI\-LS 1 "July 2026" "gji 0.12.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS