@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.
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-BACK 1 "June 2026" "gji 0.7.2" "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 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-CLEAN 1 "June 2026" "gji 0.7.2" "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 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-COMPLETION 1 "June 2026" "gji 0.7.2" "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 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-CONFIG 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-config \- manage global config defaults
4
4
  .SH SYNOPSIS
package/man/man1/gji-go.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-GO 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-GO 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-go \- print or select a worktree path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-HISTORY 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-HISTORY 1 "June 2026" "gji 0.7.2" "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 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-INIT 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-init \- print or install shell integration
4
4
  .SH SYNOPSIS
package/man/man1/gji-ls.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-LS 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-LS 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-NEW 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-NEW 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-new \- create a new branch or detached linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-OPEN 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-OPEN 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-open \- open the worktree in an editor
4
4
  .SH SYNOPSIS
package/man/man1/gji-pr.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-PR 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-PR 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-pr \- fetch a pull request by number, #number, or URL into a linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-REMOVE 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-REMOVE 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-remove \- remove a linked worktree and delete its branch when present
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-ROOT 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-ROOT 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-root \- print the main repository root path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-RUN\-HOOK 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-RUN\-HOOK 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-run\-hook \- run a named hook (after\-create, after\-enter, before\-remove) in the current worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-STATUS 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-STATUS 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-status \- summarize repository and worktree health
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-SYNC\-FILES 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-SYNC\-FILES 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-sync\-files \- manage local files copied into new worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-SYNC 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-SYNC 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-sync \- fetch and update one or all worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-WARP 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI\-WARP 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-warp \- jump to any worktree across all known repos
4
4
  .SH SYNOPSIS
package/man/man1/gji.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI 1 "June 2026" "gji 0.7.1" "User Commands"
1
+ .TH GJI 1 "June 2026" "gji 0.7.2" "User Commands"
2
2
  .SH NAME
3
3
  gji \- Context switching without the mess.
4
4
  .SH SYNOPSIS
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.7.1",
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@10.6.3",
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
  }