@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.
- package/README.md +83 -426
- package/dist/back.d.ts +3 -0
- package/dist/back.js +19 -7
- package/dist/bootstrap-output.d.ts +3 -0
- package/dist/bootstrap-output.js +27 -0
- package/dist/bootstrap-preview.d.ts +10 -0
- package/dist/bootstrap-preview.js +21 -0
- package/dist/clean.js +27 -3
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +140 -22
- package/dist/clone-failure-store.d.ts +18 -0
- package/dist/clone-failure-store.js +230 -0
- package/dist/command-runner.d.ts +6 -0
- package/dist/command-runner.js +38 -0
- package/dist/completion.js +7 -2
- package/dist/config-command.d.ts +1 -0
- package/dist/config-command.js +7 -1
- package/dist/config.d.ts +26 -1
- package/dist/config.js +110 -6
- package/dist/context-card.d.ts +1 -0
- package/dist/context-card.js +25 -0
- package/dist/dependency-bootstrap-prompt.d.ts +24 -0
- package/dist/dependency-bootstrap-prompt.js +103 -0
- package/dist/dependency-bootstrap.d.ts +129 -0
- package/dist/dependency-bootstrap.js +566 -0
- package/dist/dir-clone.d.ts +33 -0
- package/dist/dir-clone.js +505 -0
- package/dist/doctor.js +21 -0
- package/dist/done.d.ts +10 -0
- package/dist/done.js +210 -0
- package/dist/file-sync.js +71 -22
- package/dist/format-bytes.d.ts +1 -0
- package/dist/format-bytes.js +14 -0
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +26 -0
- package/dist/git.d.ts +5 -0
- package/dist/git.js +50 -0
- package/dist/gji-bundle.mjs +6189 -1313
- package/dist/go-resolver.d.ts +35 -0
- package/dist/go-resolver.js +185 -0
- package/dist/go.d.ts +6 -2
- package/dist/go.js +270 -62
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.js +20 -8
- package/dist/hub.d.ts +31 -0
- package/dist/hub.js +287 -0
- package/dist/init.js +30 -22
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +3 -25
- package/dist/ls.js +9 -1
- package/dist/navigation-output.d.ts +11 -0
- package/dist/navigation-output.js +6 -0
- package/dist/new.d.ts +14 -2
- package/dist/new.js +422 -38
- package/dist/pr-open.js +2 -10
- package/dist/pr.d.ts +6 -2
- package/dist/pr.js +135 -30
- package/dist/pull-requests.d.ts +1 -0
- package/dist/pull-requests.js +32 -3
- package/dist/remove.js +22 -0
- package/dist/repo.d.ts +4 -0
- package/dist/repo.js +61 -6
- package/dist/run-hook.js +2 -0
- package/dist/safe-destination.d.ts +15 -0
- package/dist/safe-destination.js +166 -0
- package/dist/shell-completion.js +52 -13
- package/dist/slots.d.ts +8 -0
- package/dist/slots.js +61 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.js +9 -1
- package/dist/sync-directories.d.ts +33 -0
- package/dist/sync-directories.js +97 -0
- package/dist/sync-plan.d.ts +16 -0
- package/dist/sync-plan.js +128 -0
- package/dist/sync.js +7 -13
- package/dist/task-command.d.ts +9 -0
- package/dist/task-command.js +29 -0
- package/dist/task.d.ts +9 -0
- package/dist/task.js +37 -0
- package/dist/terminal-text.d.ts +4 -0
- package/dist/terminal-text.js +72 -0
- package/dist/undo.d.ts +36 -0
- package/dist/undo.js +392 -0
- package/dist/warp.d.ts +3 -1
- package/dist/warp.js +32 -125
- package/dist/worktree-bootstrap.d.ts +35 -0
- package/dist/worktree-bootstrap.js +96 -0
- package/dist/worktree-info.d.ts +6 -1
- package/dist/worktree-info.js +14 -4
- package/dist/worktree-management.d.ts +1 -0
- package/dist/worktree-management.js +23 -4
- package/dist/worktree-picker.d.ts +24 -11
- package/dist/worktree-picker.js +176 -95
- package/dist/worktree-prompts.js +1 -1
- package/dist/worktree-source.d.ts +6 -0
- package/dist/worktree-source.js +1 -0
- package/dist/worktree-sources.d.ts +4 -0
- package/dist/worktree-sources.js +77 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +19 -0
- package/man/man1/gji-go.1 +12 -3
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +21 -3
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +7 -1
- package/man/man1/gji-remove.1 +3 -3
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +16 -0
- package/man/man1/gji-undo.1 +16 -0
- package/man/man1/gji-warp.1 +3 -6
- package/man/man1/gji.1 +17 -5
- package/package.json +3 -1
package/dist/worktree-picker.js
CHANGED
|
@@ -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 =
|
|
11
|
-
(dependencies.
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
63
|
+
return [];
|
|
33
64
|
const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
|
|
34
65
|
if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery))
|
|
35
|
-
return
|
|
36
|
-
|
|
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: [
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
584
|
+
return this.entries;
|
|
464
585
|
}
|
|
465
|
-
return filterSearchablePromptEntries(this.
|
|
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.
|
|
732
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
969
|
+
export function formatWorktreeBranchLabel(branch, pullRequests = []) {
|
|
827
970
|
const branchLabel = branch ?? "(detached)";
|
|
828
|
-
if (
|
|
971
|
+
if (pullRequests.length === 0)
|
|
829
972
|
return branchLabel;
|
|
830
|
-
return `${branchLabel} (${
|
|
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
|
-
}
|
package/dist/worktree-prompts.js
CHANGED
|
@@ -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}
|
|
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 @@
|
|
|
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
|
+
}
|
package/man/man1/gji-back.1
CHANGED
package/man/man1/gji-clean.1
CHANGED
package/man/man1/gji-config.1
CHANGED
package/man/man1/gji-doctor.1
CHANGED
|
@@ -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.
|
|
1
|
+
.TH GJI\-GO 1 "July 2026" "gji 0.12.0" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
|
-
gji\-go \-
|
|
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
|
-
|
|
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)
|
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED