@solaqua/gji 0.7.1 → 0.7.2
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/dist/clean.d.ts +2 -1
- package/dist/clean.js +8 -16
- package/dist/gji-bundle.mjs +466 -189
- package/dist/go.d.ts +2 -4
- package/dist/go.js +18 -47
- package/dist/history.d.ts +1 -0
- package/dist/history.js +12 -5
- package/dist/new.js +70 -8
- package/dist/open.d.ts +2 -2
- package/dist/open.js +22 -18
- package/dist/pr.js +3 -3
- package/dist/remove.d.ts +3 -2
- package/dist/remove.js +7 -12
- package/dist/repo.d.ts +0 -1
- package/dist/repo.js +0 -9
- package/dist/warp.js +24 -53
- package/dist/worktree-info.d.ts +0 -1
- package/dist/worktree-info.js +17 -11
- package/dist/worktree-picker.d.ts +14 -0
- package/dist/worktree-picker.js +228 -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-go.1 +1 -1
- 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 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- 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-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +3 -7
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorktreeEntry } from "./repo.js";
|
|
2
|
+
export interface WorktreePromptSource {
|
|
3
|
+
repoName: string;
|
|
4
|
+
worktree: WorktreeEntry;
|
|
5
|
+
}
|
|
6
|
+
export interface WorktreePromptEntry extends WorktreeEntry {
|
|
7
|
+
group: "recent" | "other";
|
|
8
|
+
label: string;
|
|
9
|
+
repoName: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildWorktreePromptEntries(sources: WorktreePromptSource[]): Promise<WorktreePromptEntry[]>;
|
|
12
|
+
export declare function resolveWorktreeQuery(sources: WorktreePromptSource[], query: string): WorktreePromptSource | null;
|
|
13
|
+
export declare function promptForSingleWorktree(message: string, worktrees: WorktreePromptEntry[]): Promise<string | null>;
|
|
14
|
+
export declare function promptForMultipleWorktrees(message: string, worktrees: WorktreePromptEntry[]): Promise<string[] | null>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { groupMultiselect, isCancel, select } from "@clack/prompts";
|
|
2
|
+
import { loadHistory } from "./history.js";
|
|
3
|
+
import { readWorktreeInfos, } from "./worktree-info.js";
|
|
4
|
+
export async function buildWorktreePromptEntries(sources) {
|
|
5
|
+
const [history, infos] = await Promise.all([
|
|
6
|
+
loadHistory(),
|
|
7
|
+
readWorktreeInfos(sources.map((source) => source.worktree)),
|
|
8
|
+
]);
|
|
9
|
+
const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
|
|
10
|
+
const entries = sources.map((source, index) => buildWorktreePromptEntry(source, infos[index], historyByPath.get(source.worktree.path)?.timestamp ?? null, Date.now()));
|
|
11
|
+
return entries
|
|
12
|
+
.sort(comparePromptEntries)
|
|
13
|
+
.map(({ lastActivityTimestamp: _lastActivityTimestamp, ...entry }) => entry);
|
|
14
|
+
}
|
|
15
|
+
export function resolveWorktreeQuery(sources, query) {
|
|
16
|
+
const normalizedQuery = normalizeQuery(query);
|
|
17
|
+
if (normalizedQuery === null)
|
|
18
|
+
return null;
|
|
19
|
+
const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
|
|
20
|
+
if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery))
|
|
21
|
+
return null;
|
|
22
|
+
return matches[0]?.source ?? null;
|
|
23
|
+
}
|
|
24
|
+
function findWorktreePromptSourceMatches(sources, normalizedQuery) {
|
|
25
|
+
return sources
|
|
26
|
+
.flatMap((source) => {
|
|
27
|
+
const matchScore = scoreWorktreeMatch({
|
|
28
|
+
...source.worktree,
|
|
29
|
+
repoName: source.repoName,
|
|
30
|
+
}, normalizedQuery);
|
|
31
|
+
return matchScore === null ? [] : [{ matchScore, source }];
|
|
32
|
+
})
|
|
33
|
+
.sort(compareQueryMatches);
|
|
34
|
+
}
|
|
35
|
+
function isAmbiguousRepoOnlyQuery(matches, query) {
|
|
36
|
+
if (matches[0]?.matchScore === 1000)
|
|
37
|
+
return false;
|
|
38
|
+
return (matches.filter((match) => match.source.repoName.toLowerCase() === query)
|
|
39
|
+
.length > 1);
|
|
40
|
+
}
|
|
41
|
+
export async function promptForSingleWorktree(message, worktrees) {
|
|
42
|
+
const choice = await select({
|
|
43
|
+
message,
|
|
44
|
+
options: worktrees.map((worktree) => ({
|
|
45
|
+
label: worktree.label,
|
|
46
|
+
value: worktree.path,
|
|
47
|
+
})),
|
|
48
|
+
maxItems: 12,
|
|
49
|
+
});
|
|
50
|
+
return isCancel(choice) ? null : choice;
|
|
51
|
+
}
|
|
52
|
+
export async function promptForMultipleWorktrees(message, worktrees) {
|
|
53
|
+
const choice = await groupMultiselect({
|
|
54
|
+
message,
|
|
55
|
+
options: groupPromptEntries(worktrees),
|
|
56
|
+
required: true,
|
|
57
|
+
selectableGroups: false,
|
|
58
|
+
});
|
|
59
|
+
return isCancel(choice) ? null : choice;
|
|
60
|
+
}
|
|
61
|
+
function compareQueryMatches(a, b) {
|
|
62
|
+
if (a.matchScore !== b.matchScore) {
|
|
63
|
+
return b.matchScore - a.matchScore;
|
|
64
|
+
}
|
|
65
|
+
if (a.source.worktree.isCurrent && !b.source.worktree.isCurrent)
|
|
66
|
+
return -1;
|
|
67
|
+
if (!a.source.worktree.isCurrent && b.source.worktree.isCurrent)
|
|
68
|
+
return 1;
|
|
69
|
+
return (a.source.repoName.localeCompare(b.source.repoName) ||
|
|
70
|
+
(a.source.worktree.branch ?? "").localeCompare(b.source.worktree.branch ?? "") ||
|
|
71
|
+
a.source.worktree.path.localeCompare(b.source.worktree.path));
|
|
72
|
+
}
|
|
73
|
+
function groupPromptEntries(worktrees) {
|
|
74
|
+
const groups = {};
|
|
75
|
+
for (const worktree of worktrees) {
|
|
76
|
+
const group = worktree.group === "recent" ? "Recent worktrees" : "Other worktrees";
|
|
77
|
+
groups[group] ??= [];
|
|
78
|
+
groups[group].push({
|
|
79
|
+
label: worktree.label,
|
|
80
|
+
value: worktree.path,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return groups;
|
|
84
|
+
}
|
|
85
|
+
function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
|
|
86
|
+
const lastWorkedTimestamp = info.lastCommitTimestamp === null ? null : info.lastCommitTimestamp * 1000;
|
|
87
|
+
const lastActivityTimestamp = lastUsedTimestamp ?? lastWorkedTimestamp;
|
|
88
|
+
const lastActivityType = lastUsedTimestamp !== null
|
|
89
|
+
? "used"
|
|
90
|
+
: lastWorkedTimestamp !== null
|
|
91
|
+
? "worked"
|
|
92
|
+
: null;
|
|
93
|
+
const branch = source.worktree.branch ?? "(detached)";
|
|
94
|
+
const badges = buildStatusBadges(info);
|
|
95
|
+
const recency = formatPromptRecency(lastActivityTimestamp, lastActivityType, now);
|
|
96
|
+
const status = badges.length > 0 ? badges.map((badge) => `[${badge}]`).join(" ") : null;
|
|
97
|
+
const path = middleEllipsize(source.worktree.path, 76);
|
|
98
|
+
const label = [
|
|
99
|
+
middleEllipsize(source.repoName, 22),
|
|
100
|
+
middleEllipsize(branch, 34),
|
|
101
|
+
status,
|
|
102
|
+
recency,
|
|
103
|
+
path,
|
|
104
|
+
]
|
|
105
|
+
.filter((part) => part !== null && part.length > 0)
|
|
106
|
+
.join(" · ");
|
|
107
|
+
return {
|
|
108
|
+
...source.worktree,
|
|
109
|
+
group: lastUsedTimestamp !== null ? "recent" : "other",
|
|
110
|
+
label,
|
|
111
|
+
lastActivityTimestamp,
|
|
112
|
+
repoName: source.repoName,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildStatusBadges(info) {
|
|
116
|
+
const badges = [];
|
|
117
|
+
if (info.isCurrent) {
|
|
118
|
+
badges.push("current");
|
|
119
|
+
}
|
|
120
|
+
if (info.branch === null) {
|
|
121
|
+
badges.push("detached");
|
|
122
|
+
}
|
|
123
|
+
if (info.status === "dirty") {
|
|
124
|
+
badges.push("dirty");
|
|
125
|
+
}
|
|
126
|
+
if (info.upstream.kind === "stale") {
|
|
127
|
+
badges.push("stale", "gone");
|
|
128
|
+
}
|
|
129
|
+
if (isUpToDate(info.upstream)) {
|
|
130
|
+
badges.push("up to date");
|
|
131
|
+
}
|
|
132
|
+
return badges;
|
|
133
|
+
}
|
|
134
|
+
function isUpToDate(upstream) {
|
|
135
|
+
return (upstream.kind === "tracked" && upstream.ahead === 0 && upstream.behind === 0);
|
|
136
|
+
}
|
|
137
|
+
function formatPromptRecency(timestamp, type, now) {
|
|
138
|
+
if (timestamp === null || type === null) {
|
|
139
|
+
return "last used: never";
|
|
140
|
+
}
|
|
141
|
+
const label = type === "used" ? "last used" : "last worked";
|
|
142
|
+
return `${label}: ${formatPickerAge(timestamp, now)}`;
|
|
143
|
+
}
|
|
144
|
+
function formatPickerAge(timestamp, now) {
|
|
145
|
+
const ageSeconds = Math.max(0, Math.floor((now - timestamp) / 1000));
|
|
146
|
+
if (ageSeconds < 60) {
|
|
147
|
+
return "now";
|
|
148
|
+
}
|
|
149
|
+
if (ageSeconds < 60 * 60) {
|
|
150
|
+
return `${Math.floor(ageSeconds / 60)}m ago`;
|
|
151
|
+
}
|
|
152
|
+
if (ageSeconds < 24 * 60 * 60) {
|
|
153
|
+
return `${Math.floor(ageSeconds / (60 * 60))}h ago`;
|
|
154
|
+
}
|
|
155
|
+
if (isYesterday(timestamp, now)) {
|
|
156
|
+
return "yesterday";
|
|
157
|
+
}
|
|
158
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
159
|
+
day: "numeric",
|
|
160
|
+
month: "short",
|
|
161
|
+
}).format(new Date(timestamp));
|
|
162
|
+
}
|
|
163
|
+
function isYesterday(timestamp, now) {
|
|
164
|
+
const date = new Date(timestamp);
|
|
165
|
+
const yesterday = new Date(now);
|
|
166
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
167
|
+
return (date.getFullYear() === yesterday.getFullYear() &&
|
|
168
|
+
date.getMonth() === yesterday.getMonth() &&
|
|
169
|
+
date.getDate() === yesterday.getDate());
|
|
170
|
+
}
|
|
171
|
+
function buildSearchText(repoName, worktree) {
|
|
172
|
+
return [
|
|
173
|
+
repoName,
|
|
174
|
+
worktree.branch ?? "detached",
|
|
175
|
+
worktree.path,
|
|
176
|
+
`${repoName}/${worktree.branch ?? "detached"}`,
|
|
177
|
+
]
|
|
178
|
+
.join(" ")
|
|
179
|
+
.toLowerCase();
|
|
180
|
+
}
|
|
181
|
+
function normalizeQuery(query) {
|
|
182
|
+
const normalized = query?.trim().toLowerCase();
|
|
183
|
+
return normalized && normalized.length > 0 ? normalized : null;
|
|
184
|
+
}
|
|
185
|
+
function comparePromptEntries(a, b) {
|
|
186
|
+
if (a.isCurrent && !b.isCurrent)
|
|
187
|
+
return -1;
|
|
188
|
+
if (!a.isCurrent && b.isCurrent)
|
|
189
|
+
return 1;
|
|
190
|
+
if (a.group !== b.group) {
|
|
191
|
+
return groupRank(a.group) - groupRank(b.group);
|
|
192
|
+
}
|
|
193
|
+
const aRecent = a.lastActivityTimestamp ?? 0;
|
|
194
|
+
const bRecent = b.lastActivityTimestamp ?? 0;
|
|
195
|
+
if (aRecent !== bRecent) {
|
|
196
|
+
return bRecent - aRecent;
|
|
197
|
+
}
|
|
198
|
+
return (a.repoName.localeCompare(b.repoName) ||
|
|
199
|
+
(a.branch ?? "").localeCompare(b.branch ?? "") ||
|
|
200
|
+
a.path.localeCompare(b.path));
|
|
201
|
+
}
|
|
202
|
+
function groupRank(group) {
|
|
203
|
+
return group === "recent" ? 0 : 1;
|
|
204
|
+
}
|
|
205
|
+
function scoreWorktreeMatch(entry, query) {
|
|
206
|
+
const branch = entry.branch ?? "detached";
|
|
207
|
+
const exactCandidates = [
|
|
208
|
+
branch,
|
|
209
|
+
entry.path,
|
|
210
|
+
`${entry.repoName}/${branch}`,
|
|
211
|
+
].map((candidate) => candidate.toLowerCase());
|
|
212
|
+
if (exactCandidates.includes(query)) {
|
|
213
|
+
return 1000;
|
|
214
|
+
}
|
|
215
|
+
return buildSearchText(entry.repoName, entry).includes(query) ? 1 : null;
|
|
216
|
+
}
|
|
217
|
+
function middleEllipsize(value, maxLength) {
|
|
218
|
+
if (value.length <= maxLength) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
if (maxLength <= 1) {
|
|
222
|
+
return "…";
|
|
223
|
+
}
|
|
224
|
+
const keep = maxLength - 1;
|
|
225
|
+
const start = Math.ceil(keep / 2);
|
|
226
|
+
const end = Math.floor(keep / 2);
|
|
227
|
+
return `${value.slice(0, start)}…${value.slice(value.length - end)}`;
|
|
228
|
+
}
|
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-go.1
CHANGED
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED
package/man/man1/gji-new.1
CHANGED
package/man/man1/gji-open.1
CHANGED
package/man/man1/gji-pr.1
CHANGED
package/man/man1/gji-remove.1
CHANGED
package/man/man1/gji-root.1
CHANGED
package/man/man1/gji-run-hook.1
CHANGED
package/man/man1/gji-status.1
CHANGED
package/man/man1/gji-sync.1
CHANGED
package/man/man1/gji-warp.1
CHANGED
package/man/man1/gji.1
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solaqua/gji",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Git worktree CLI for fast context switching.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "sjquant",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"packageManager": "pnpm@
|
|
8
|
+
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
|
9
9
|
"homepage": "https://github.com/sjquant/gji#readme",
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/sjquant/gji/issues"
|
|
@@ -66,12 +66,8 @@
|
|
|
66
66
|
"@j178/prek": "^0.3.13",
|
|
67
67
|
"@types/node": "^24.6.0",
|
|
68
68
|
"esbuild": "^0.27.0",
|
|
69
|
+
"tsx": "^4.22.4",
|
|
69
70
|
"typescript": "^5.9.3",
|
|
70
71
|
"vitest": "^3.2.4"
|
|
71
|
-
},
|
|
72
|
-
"pnpm": {
|
|
73
|
-
"onlyBuiltDependencies": [
|
|
74
|
-
"esbuild"
|
|
75
|
-
]
|
|
76
72
|
}
|
|
77
73
|
}
|