@solaqua/gji 0.9.0 → 0.11.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 (100) hide show
  1. package/README.md +93 -20
  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 +25 -3
  9. package/dist/cli.js +105 -19
  10. package/dist/clone-failure-store.d.ts +18 -0
  11. package/dist/clone-failure-store.js +230 -0
  12. package/dist/command-runner.d.ts +6 -0
  13. package/dist/command-runner.js +38 -0
  14. package/dist/completion.js +7 -2
  15. package/dist/config-command.d.ts +1 -0
  16. package/dist/config-command.js +7 -1
  17. package/dist/config.d.ts +26 -1
  18. package/dist/config.js +110 -6
  19. package/dist/dependency-bootstrap-prompt.d.ts +24 -0
  20. package/dist/dependency-bootstrap-prompt.js +103 -0
  21. package/dist/dependency-bootstrap.d.ts +129 -0
  22. package/dist/dependency-bootstrap.js +566 -0
  23. package/dist/dir-clone.d.ts +33 -0
  24. package/dist/dir-clone.js +505 -0
  25. package/dist/done.d.ts +10 -0
  26. package/dist/done.js +208 -0
  27. package/dist/file-sync.js +71 -22
  28. package/dist/format-bytes.d.ts +1 -0
  29. package/dist/format-bytes.js +14 -0
  30. package/dist/fs-utils.d.ts +4 -0
  31. package/dist/fs-utils.js +26 -0
  32. package/dist/git.d.ts +5 -0
  33. package/dist/git.js +50 -0
  34. package/dist/gji-bundle.mjs +5489 -1206
  35. package/dist/go-resolver.d.ts +35 -0
  36. package/dist/go-resolver.js +185 -0
  37. package/dist/go.d.ts +5 -2
  38. package/dist/go.js +262 -62
  39. package/dist/hooks.d.ts +1 -1
  40. package/dist/hooks.js +13 -7
  41. package/dist/init.js +30 -22
  42. package/dist/install-prompt.d.ts +1 -0
  43. package/dist/install-prompt.js +3 -25
  44. package/dist/navigation-output.d.ts +11 -0
  45. package/dist/navigation-output.js +6 -0
  46. package/dist/new.d.ts +14 -2
  47. package/dist/new.js +434 -36
  48. package/dist/open.d.ts +1 -0
  49. package/dist/open.js +14 -2
  50. package/dist/pr.d.ts +6 -2
  51. package/dist/pr.js +135 -30
  52. package/dist/remove.js +20 -0
  53. package/dist/repo.d.ts +3 -0
  54. package/dist/repo.js +57 -6
  55. package/dist/safe-destination.d.ts +15 -0
  56. package/dist/safe-destination.js +166 -0
  57. package/dist/shell-completion.js +47 -14
  58. package/dist/sync-directories.d.ts +33 -0
  59. package/dist/sync-directories.js +97 -0
  60. package/dist/sync-plan.d.ts +16 -0
  61. package/dist/sync-plan.js +128 -0
  62. package/dist/sync.js +7 -13
  63. package/dist/undo.d.ts +36 -0
  64. package/dist/undo.js +392 -0
  65. package/dist/warp.d.ts +3 -1
  66. package/dist/warp.js +32 -125
  67. package/dist/worktree-bootstrap.d.ts +35 -0
  68. package/dist/worktree-bootstrap.js +93 -0
  69. package/dist/worktree-management.d.ts +1 -0
  70. package/dist/worktree-management.js +23 -4
  71. package/dist/worktree-picker.d.ts +21 -8
  72. package/dist/worktree-picker.js +107 -15
  73. package/dist/worktree-prompts.js +1 -1
  74. package/dist/worktree-source.d.ts +6 -0
  75. package/dist/worktree-source.js +1 -0
  76. package/dist/worktree-sources.d.ts +3 -0
  77. package/dist/worktree-sources.js +31 -0
  78. package/man/man1/gji-back.1 +1 -1
  79. package/man/man1/gji-clean.1 +1 -1
  80. package/man/man1/gji-completion.1 +1 -1
  81. package/man/man1/gji-config.1 +1 -1
  82. package/man/man1/gji-doctor.1 +1 -1
  83. package/man/man1/gji-done.1 +19 -0
  84. package/man/man1/gji-go.1 +9 -3
  85. package/man/man1/gji-history.1 +1 -1
  86. package/man/man1/gji-init.1 +1 -1
  87. package/man/man1/gji-ls.1 +1 -1
  88. package/man/man1/gji-new.1 +21 -3
  89. package/man/man1/gji-open.1 +4 -1
  90. package/man/man1/gji-pr.1 +7 -1
  91. package/man/man1/gji-remove.1 +3 -3
  92. package/man/man1/gji-root.1 +1 -1
  93. package/man/man1/gji-run-hook.1 +1 -1
  94. package/man/man1/gji-status.1 +1 -1
  95. package/man/man1/gji-sync-files.1 +1 -1
  96. package/man/man1/gji-sync.1 +1 -1
  97. package/man/man1/gji-undo.1 +16 -0
  98. package/man/man1/gji-warp.1 +3 -6
  99. package/man/man1/gji.1 +13 -5
  100. package/package.json +3 -1
@@ -0,0 +1,93 @@
1
+ import { basename } from "node:path";
2
+ import { executeDependencyBootstrap, prepareDependencyBootstrap, } from "./dependency-bootstrap.js";
3
+ import { cloneDir } from "./dir-clone.js";
4
+ import { syncFiles } from "./file-sync.js";
5
+ import { extractHooks, runHook } from "./hooks.js";
6
+ import { maybeRunInstallPrompt, } from "./install-prompt.js";
7
+ import { executeSyncDirectoryPlan, } from "./sync-directories.js";
8
+ import { prepareSyncDirectoryPlan } from "./sync-plan.js";
9
+ export async function bootstrapWorktree(options) {
10
+ const dependencyMode = options.config.dependencyBootstrap ?? "off";
11
+ const dependencyPlan = await prepareDependencyBootstrap(dependencyMode, {
12
+ currentRoot: options.currentRoot,
13
+ detectionRoot: options.dependencyDetectionRoot,
14
+ repoRoot: options.repoRoot,
15
+ cargoBuildCommand: options.config.dependencyBuildCommand,
16
+ worktreePath: options.worktreePath,
17
+ });
18
+ const syncPlan = await prepareSyncDirectoryPlan(options.repoRoot, options.worktreePath, options.config.syncDirs ?? []);
19
+ const outcomes = await executeSyncDirectoryPlan(syncPlan, {
20
+ cloneDirectory: options.cloneDirectory ?? cloneDir,
21
+ repoRoot: options.repoRoot,
22
+ reporter: options.reporter,
23
+ });
24
+ const clonedDirs = outcomes.flatMap((outcome) => outcome.kind === "cloned" ? [outcome.directory] : []);
25
+ const skippedDirs = outcomes.flatMap((outcome) => outcome.kind === "skipped"
26
+ ? [{ dir: outcome.dir, reason: outcome.reason }]
27
+ : []);
28
+ const syncFileFailures = [];
29
+ for (const pattern of options.config.syncFiles ?? []) {
30
+ try {
31
+ await syncFiles(options.repoRoot, options.worktreePath, [pattern]);
32
+ }
33
+ catch (error) {
34
+ const message = `failed to sync file "${pattern}": ${toErrorMessage(error)}`;
35
+ if (!options.json)
36
+ options.reporter.write(`Warning: ${message}\n`);
37
+ syncFileFailures.push({
38
+ adapter: "syncFiles",
39
+ kind: "sync-file",
40
+ reason: "sync-file-failed",
41
+ state: "failed",
42
+ target: pattern,
43
+ message,
44
+ });
45
+ }
46
+ }
47
+ if (syncFileFailures.length > 0) {
48
+ for (const event of syncFileFailures)
49
+ options.reporter.dependency(event);
50
+ }
51
+ const dependencyBootstrap = syncFileFailures.length > 0
52
+ ? { mode: dependencyMode, ready: false, events: [] }
53
+ : await executeDependencyBootstrap(dependencyPlan, {
54
+ cloneDirectory: options.cloneDirectory,
55
+ repoRoot: options.repoRoot,
56
+ reporter: options.reporter,
57
+ stderr: options.commandStderr ?? options.reporter.write,
58
+ stdout: options.commandStdout,
59
+ seededDirectories: clonedDirs.map(({ dir }) => dir),
60
+ runCommand: options.runCommand,
61
+ });
62
+ if (!dependencyBootstrap.ready) {
63
+ return {
64
+ clonedDirs,
65
+ dependencyBootstrap,
66
+ ready: false,
67
+ syncFileFailures,
68
+ skippedDirs,
69
+ };
70
+ }
71
+ if (dependencyMode === "off" &&
72
+ ["default", "legacy"].includes(options.dependencyBootstrapPolicy?.source ?? "default")) {
73
+ await maybeRunInstallPrompt(options.worktreePath, options.repoRoot, options.config, options.reporter.write, options.installDependencies, options.nonInteractive);
74
+ }
75
+ const hooks = extractHooks(options.config);
76
+ await runHook(hooks["after-create"], options.worktreePath, {
77
+ branch: options.branch,
78
+ path: options.worktreePath,
79
+ repo: basename(options.repoRoot),
80
+ }, options.reporter.write, options.json
81
+ ? () => undefined
82
+ : (options.commandStdout ?? ((chunk) => process.stdout.write(chunk))));
83
+ return {
84
+ clonedDirs,
85
+ dependencyBootstrap,
86
+ ready: true,
87
+ syncFileFailures: [],
88
+ skippedDirs,
89
+ };
90
+ }
91
+ function toErrorMessage(error) {
92
+ return error instanceof Error ? error.message : String(error);
93
+ }
@@ -9,5 +9,6 @@ export declare function forceRemoveWorktree(repoRoot: string, worktreePath: stri
9
9
  export declare function deleteBranch(repoRoot: string, branch: string): Promise<void>;
10
10
  export declare function forceDeleteBranch(repoRoot: string, branch: string): Promise<void>;
11
11
  export declare function isWorktreeForceRemovalError(error: unknown): boolean;
12
+ export declare function isSubmoduleWorktreeRemovalError(error: unknown): boolean;
12
13
  export declare function isWorktreeDeletionError(error: unknown): boolean;
13
14
  export declare function isBranchUnmergedError(error: unknown): boolean;
@@ -21,12 +21,14 @@ export async function removeWorktree(repoRoot, worktreePath) {
21
21
  }
22
22
  export async function forceRemoveWorktree(repoRoot, worktreePath) {
23
23
  try {
24
- await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
25
- cwd: repoRoot,
26
- env: GIT_ENV,
27
- });
24
+ await removeWorktreeWithForce(repoRoot, worktreePath);
28
25
  }
29
26
  catch (error) {
27
+ if (isSubmoduleWorktreeRemovalError(error)) {
28
+ await deinitializeSubmodules(worktreePath);
29
+ await removeWorktreeWithForce(repoRoot, worktreePath);
30
+ return;
31
+ }
30
32
  const worktreeIsStillRegistered = (await listWorktrees(repoRoot)).some((worktree) => worktree.path === worktreePath);
31
33
  if (worktreeIsStillRegistered) {
32
34
  throw error;
@@ -34,6 +36,18 @@ export async function forceRemoveWorktree(repoRoot, worktreePath) {
34
36
  await rm(worktreePath, { force: true, recursive: true });
35
37
  }
36
38
  }
39
+ async function removeWorktreeWithForce(repoRoot, worktreePath) {
40
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
41
+ cwd: repoRoot,
42
+ env: GIT_ENV,
43
+ });
44
+ }
45
+ async function deinitializeSubmodules(worktreePath) {
46
+ await execFileAsync("git", ["submodule", "deinit", "--force", "--all"], {
47
+ cwd: worktreePath,
48
+ env: GIT_ENV,
49
+ });
50
+ }
37
51
  export async function deleteBranch(repoRoot, branch) {
38
52
  await execFileAsync("git", ["branch", "-d", branch], {
39
53
  cwd: repoRoot,
@@ -51,8 +65,13 @@ export function isWorktreeForceRemovalError(error) {
51
65
  return false;
52
66
  }
53
67
  return (error.stderr.includes("contains modified or untracked files") ||
68
+ isSubmoduleWorktreeRemovalError(error) ||
54
69
  isWorktreeDeletionError(error));
55
70
  }
71
+ export function isSubmoduleWorktreeRemovalError(error) {
72
+ return (hasStderr(error) &&
73
+ error.stderr.includes("working trees containing submodules cannot be moved or removed"));
74
+ }
56
75
  export function isWorktreeDeletionError(error) {
57
76
  return hasStderr(error) && error.stderr.includes("failed to delete");
58
77
  }
@@ -1,11 +1,8 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
2
  import { type PullRequestInfo } from "./pull-requests.js";
3
3
  import type { WorktreeEntry } from "./repo.js";
4
- export interface WorktreePromptSource {
5
- repoRoot?: string;
6
- repoName: string;
7
- worktree: WorktreeEntry;
8
- }
4
+ import type { WorktreeSource } from "./worktree-source.js";
5
+ export type WorktreePromptSource = WorktreeSource;
9
6
  export interface WorktreePromptEntry extends WorktreeEntry {
10
7
  group: "recent" | "other";
11
8
  label: string;
@@ -17,9 +14,11 @@ export interface WorktreePromptEntry extends WorktreeEntry {
17
14
  export type QueryWorktreePullRequests = (repoRoot: string, sourceBranch: string) => Promise<PullRequestInfo[]>;
18
15
  export type QueryRepositoryPullRequests = (repoRoot: string) => Promise<PullRequestInfo[]>;
19
16
  export interface BuildWorktreePromptEntriesDependencies {
17
+ metadata?: WorktreeMetadataMode;
20
18
  queryPullRequests?: QueryWorktreePullRequests;
21
19
  queryRepositoryPullRequests?: QueryRepositoryPullRequests;
22
20
  }
21
+ export type WorktreeMetadataMode = "full" | "fast";
23
22
  export interface WorktreePickerIO {
24
23
  input?: Readable & {
25
24
  isTTY?: boolean;
@@ -30,8 +29,22 @@ export interface WorktreePickerIO {
30
29
  rows?: number;
31
30
  };
32
31
  }
33
- export declare function buildWorktreePromptEntries(sources: WorktreePromptSource[], dependencies?: BuildWorktreePromptEntriesDependencies): Promise<WorktreePromptEntry[]>;
34
- export declare function resolveWorktreeQuery(sources: WorktreePromptSource[], query: string): WorktreePromptSource | null;
35
- export declare function promptForSingleWorktree(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string | null>;
32
+ export interface WorktreePromptScope {
33
+ label: string;
34
+ toggle: () => Promise<WorktreePromptScopeResult>;
35
+ toggleLabel: string;
36
+ }
37
+ export interface WorktreePromptScopeResult {
38
+ entries: WorktreePromptEntry[];
39
+ label: string;
40
+ toggleLabel: string;
41
+ }
42
+ export declare function buildWorktreePromptEntries(sources: WorktreeSource[], dependencies?: BuildWorktreePromptEntriesDependencies): Promise<WorktreePromptEntry[]>;
43
+ export declare function resolveWorktreeQuery(sources: WorktreeSource[], query: string): WorktreeSource | null;
44
+ export declare function resolveWorktreeQueryMatches(sources: WorktreeSource[], query: string): WorktreeSource[];
45
+ export declare function resolveExactWorktreeQueryMatches(sources: WorktreeSource[], query: string): WorktreeSource[];
46
+ export declare function promptForSingleWorktree(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO & {
47
+ scope?: WorktreePromptScope;
48
+ }): Promise<string | null>;
36
49
  export declare function promptForMultipleWorktrees(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string[] | null>;
37
50
  export declare function formatWorktreeBranchLabel(branch: string | null, pullRequestNumbers?: number[]): string;
@@ -5,35 +5,66 @@ import { createPullRequestQuery, } from "./pull-requests.js";
5
5
  import { readWorktreeInfos, } from "./worktree-info.js";
6
6
  const MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY = 4;
7
7
  export async function buildWorktreePromptEntries(sources, dependencies = {}) {
8
+ const metadataMode = dependencies.metadata ?? "full";
9
+ const includeMetadata = metadataMode === "full";
8
10
  const pullRequestQuery = createPullRequestQuery();
9
11
  const queryPullRequests = dependencies.queryPullRequests ?? pullRequestQuery.listOpenPullRequests;
10
- const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
11
- (dependencies.queryPullRequests === undefined
12
- ? pullRequestQuery.listOpenPullRequestsForRepository
13
- : undefined);
12
+ const queryRepositoryPullRequests = includeMetadata
13
+ ? (dependencies.queryRepositoryPullRequests ??
14
+ (dependencies.queryPullRequests === undefined
15
+ ? pullRequestQuery.listOpenPullRequestsForRepository
16
+ : undefined))
17
+ : undefined;
14
18
  const repositoryPullRequests = queryRepositoryPullRequests === undefined
15
19
  ? Promise.resolve(null)
16
20
  : readRepositoryPullRequests(sources, queryRepositoryPullRequests);
17
21
  const [history, infos, pullRequestsByRepository] = await Promise.all([
18
22
  loadHistory(),
19
- readWorktreeInfos(sources.map((source) => source.worktree)),
23
+ includeMetadata
24
+ ? readWorktreeInfos(sources.map((source) => source.worktree))
25
+ : Promise.resolve(sources.map((source) => createUnhydratedWorktreeInfo(source.worktree))),
20
26
  repositoryPullRequests,
21
27
  ]);
22
- const pullRequests = await Promise.all(sources.map((source) => readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository)));
28
+ const pullRequests = includeMetadata
29
+ ? await Promise.all(sources.map((source) => readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository)))
30
+ : sources.map(() => []);
23
31
  const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
24
32
  const entries = sources.map((source, index) => buildWorktreePromptEntry(source, infos[index], pullRequests[index], historyByPath.get(source.worktree.path)?.timestamp ?? null, Date.now()));
25
33
  return entries
26
34
  .sort(comparePromptEntries)
27
35
  .map(({ lastActivityTimestamp: _lastActivityTimestamp, ...entry }) => entry);
28
36
  }
37
+ function createUnhydratedWorktreeInfo(worktree) {
38
+ return {
39
+ ...worktree,
40
+ lastCommitTimestamp: null,
41
+ status: "unknown",
42
+ upstream: { kind: "unknown" },
43
+ };
44
+ }
29
45
  export function resolveWorktreeQuery(sources, query) {
46
+ return resolveWorktreeQueryMatches(sources, query)[0] ?? null;
47
+ }
48
+ export function resolveWorktreeQueryMatches(sources, query) {
30
49
  const normalizedQuery = normalizeQuery(query);
31
50
  if (normalizedQuery === null)
32
- return null;
51
+ return [];
33
52
  const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
34
53
  if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery))
35
- return null;
36
- return matches[0]?.source ?? null;
54
+ return [];
55
+ const bestScore = matches[0]?.matchScore;
56
+ return matches
57
+ .filter((match) => match.matchScore === bestScore)
58
+ .map((match) => match.source);
59
+ }
60
+ export function resolveExactWorktreeQueryMatches(sources, query) {
61
+ const normalizedQuery = normalizeQuery(query);
62
+ if (normalizedQuery === null)
63
+ return [];
64
+ return sources.filter((source) => scoreWorktreeMatch({
65
+ ...source.worktree,
66
+ repoName: source.repoName,
67
+ }, normalizedQuery) === 1000);
37
68
  }
38
69
  function findWorktreePromptSourceMatches(sources, normalizedQuery) {
39
70
  return sources
@@ -59,6 +90,7 @@ export async function promptForSingleWorktree(message, worktrees, io = {}) {
59
90
  message,
60
91
  multiple: false,
61
92
  output: io.output,
93
+ scope: io.scope,
62
94
  });
63
95
  return typeof choice === "string" ? choice : null;
64
96
  }
@@ -144,13 +176,18 @@ class SearchablePrompt {
144
176
  options;
145
177
  output;
146
178
  cursor = 0;
179
+ entries;
147
180
  query = "";
148
181
  searchActive = false;
149
182
  selected = new Set();
183
+ scope;
184
+ scopeTogglePending = false;
150
185
  prompt;
151
186
  constructor(options, input, output) {
152
187
  this.options = options;
153
188
  this.output = output;
189
+ this.entries = options.entries;
190
+ this.scope = options.scope;
154
191
  this.cursor = this.firstSelectableIndex();
155
192
  const owner = this;
156
193
  this.prompt = new WorktreeCorePrompt(this, {
@@ -183,6 +220,10 @@ class SearchablePrompt {
183
220
  this.submit(prompt);
184
221
  return;
185
222
  }
223
+ if (isScopeToggleKey(character, key)) {
224
+ void this.toggleScope(prompt);
225
+ return;
226
+ }
186
227
  if (this.searchActive && this.handleSearchKey(character, key?.name)) {
187
228
  this.syncValue();
188
229
  renderPrompt(prompt);
@@ -200,6 +241,34 @@ class SearchablePrompt {
200
241
  this.syncValue();
201
242
  renderPrompt(prompt);
202
243
  }
244
+ async toggleScope(prompt) {
245
+ const currentScope = this.scope;
246
+ if (currentScope === undefined || this.scopeTogglePending)
247
+ return;
248
+ this.scopeTogglePending = true;
249
+ renderPrompt(prompt);
250
+ try {
251
+ const nextScope = await currentScope.toggle();
252
+ this.entries = nextScope.entries.map(buildSearchableWorktreeEntry);
253
+ this.scope = {
254
+ label: nextScope.label,
255
+ toggle: currentScope.toggle,
256
+ toggleLabel: nextScope.toggleLabel,
257
+ };
258
+ this.query = "";
259
+ this.cursor = this.firstSelectableIndex();
260
+ this.syncValue();
261
+ }
262
+ catch (error) {
263
+ prompt.error =
264
+ error instanceof Error ? error.message : "Could not load worktrees";
265
+ prompt.state = "error";
266
+ }
267
+ finally {
268
+ this.scopeTogglePending = false;
269
+ renderPrompt(prompt);
270
+ }
271
+ }
203
272
  handleEscapeKey(prompt) {
204
273
  if (this.searchActive) {
205
274
  this.searchActive = false;
@@ -334,20 +403,40 @@ class SearchablePrompt {
334
403
  renderTitle(state, glyphs, colors) {
335
404
  const symbol = colors.symbol(promptSymbol(state, glyphs), state);
336
405
  const maxTitleLength = Math.max(8, this.columns() - 3);
406
+ const title = this.scope
407
+ ? `${this.options.message} · ${this.scope.label}`
408
+ : this.options.message;
337
409
  if (!this.searchActive) {
338
- return `${symbol} ${middleEllipsize(this.options.message, maxTitleLength)}`;
410
+ return `${symbol} ${middleEllipsize(title, maxTitleLength)}`;
339
411
  }
340
412
  const search = ` /${this.query}`;
341
413
  const maxSearchLength = Math.min(terminalWidth(search), Math.floor(maxTitleLength / 2));
342
- const message = middleEllipsize(this.options.message, Math.max(1, maxTitleLength - maxSearchLength));
414
+ const message = middleEllipsize(title, Math.max(1, maxTitleLength - maxSearchLength));
343
415
  const visibleSearch = middleEllipsize(search, Math.max(1, maxTitleLength - terminalWidth(message)));
344
416
  return `${symbol} ${message}${colors.search(visibleSearch)}`;
345
417
  }
346
418
  renderSearchHint(colors) {
419
+ const scopeHint = this.scope
420
+ ? `${colors.key("tab")} ${colors.hint(this.scope.toggleLabel)}`
421
+ : null;
422
+ const separator = colors.hint(" · ");
423
+ if (this.scopeTogglePending) {
424
+ return `${colors.hint("loading worktrees")} ${separator}${scopeHint}`;
425
+ }
347
426
  if (this.searchActive) {
348
- return `${colors.hint("type to filter")} ${colors.hint("·")} ${colors.key("esc")} ${colors.hint("clears")}`;
427
+ return [
428
+ `${colors.hint("type to filter")} ${separator} ${colors.key("esc")} ${colors.hint("clears")}`,
429
+ scopeHint,
430
+ ]
431
+ .filter((hint) => hint !== null)
432
+ .join(separator);
349
433
  }
350
- return `${colors.hint("press ")}${colors.key("/")}${colors.hint(" to search")}`;
434
+ return [
435
+ `${colors.hint("press ")}${colors.key("/")}${colors.hint(" to search")}`,
436
+ scopeHint,
437
+ ]
438
+ .filter((hint) => hint !== null)
439
+ .join(separator);
351
440
  }
352
441
  renderEntries(entries, visibleEntries, glyphs, colors, frame) {
353
442
  if (entries.length === 0) {
@@ -460,9 +549,9 @@ class SearchablePrompt {
460
549
  visibleEntries() {
461
550
  const query = normalizeQuery(this.query);
462
551
  if (query === null) {
463
- return this.options.entries;
552
+ return this.entries;
464
553
  }
465
- return filterSearchablePromptEntries(this.options.entries, query);
554
+ return filterSearchablePromptEntries(this.entries, query);
466
555
  }
467
556
  maxItems() {
468
557
  const rows = this.output.rows ?? 16;
@@ -505,6 +594,9 @@ function closePrompt(prompt) {
505
594
  function onWorktreeKeypress(character, key) {
506
595
  this.worktreePrompt.handleKeypress(this, character, key);
507
596
  }
597
+ function isScopeToggleKey(character, key) {
598
+ return key?.name === "tab" || character === "\t";
599
+ }
508
600
  function resolvePromptAction(character, key) {
509
601
  if ((key?.ctrl && key.name === "c") || character === "\u0003") {
510
602
  return "cancel";
@@ -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,3 @@
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[]>;
@@ -0,0 +1,31 @@
1
+ import { detectRepository, listWorktrees } from "./repo.js";
2
+ import { loadRegistry } from "./repo-registry.js";
3
+ export async function listRegisteredWorktreeSources(cwd, onSkipped) {
4
+ const registry = await loadRegistry();
5
+ const currentRoot = await detectRepository(cwd)
6
+ .then((repository) => repository.currentRoot)
7
+ .catch(() => null);
8
+ const results = await Promise.allSettled(registry.map(async (entry) => {
9
+ const worktrees = await listWorktrees(entry.path);
10
+ return { repoName: entry.name, repoRoot: entry.path, worktrees };
11
+ }));
12
+ const allItems = [];
13
+ for (const [index, result] of results.entries()) {
14
+ if (result.status === "rejected") {
15
+ onSkipped?.(registry[index]);
16
+ continue;
17
+ }
18
+ const { repoName, repoRoot, worktrees } = result.value;
19
+ for (const worktree of worktrees) {
20
+ allItems.push({
21
+ repoRoot,
22
+ repoName,
23
+ worktree: {
24
+ ...worktree,
25
+ isCurrent: currentRoot !== null && worktree.path === currentRoot,
26
+ },
27
+ });
28
+ }
29
+ }
30
+ return allItems;
31
+ }
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-BACK 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-CLEAN 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-COMPLETION 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-CONFIG 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-DOCTOR 1 "July 2026" "gji 0.11.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.11.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,19 @@
1
- .TH GJI\-GO 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-GO 1 "July 2026" "gji 0.11.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 \-\-json
17
+ emit JSON for an existing worktree destination
12
18
  .SH "SEE ALSO"
13
19
  .BR gji (1)
@@ -1,4 +1,4 @@
1
- .TH GJI\-HISTORY 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-HISTORY 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-INIT 1 "July 2026" "gji 0.11.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.9.0" "User Commands"
1
+ .TH GJI\-LS 1 "July 2026" "gji 0.11.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS
@@ -1,10 +1,16 @@
1
- .TH GJI\-NEW 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-NEW 1 "July 2026" "gji 0.11.0" "User Commands"
2
2
  .SH NAME
3
- gji\-new \- create a new branch or detached linked worktree
3
+ gji\-new \- create a new branch or detached linked worktree and CoW\-bootstrap configured directories
4
4
  .SH SYNOPSIS
5
5
  .B gji new [\fIoptions\fR] [options] [branch]
6
6
  .SH DESCRIPTION
7
- create a new branch or detached linked worktree
7
+ create a new branch or detached linked worktree and CoW\-bootstrap configured directories
8
+ .SH BOOTSTRAP_POLICY
9
+ dependencyBuildCommand overrides Cargo's default cargo check. Supported lockfiles trigger an interactive dependencyBootstrap choice when no explicit policy exists; the choice is saved according to installSaveTarget. JSON and headless modes never prompt and keep off. npm is always install\-only, and sync\-file failures stop dependency repair, install prompts, and after\-create hooks. JSON bootstrap events include structured reasons, and failed setup reports the created worktree path.
10
+
11
+ .SH BOOTSTRAP
12
+ syncDirs performs generic copy\-on\-write directory seeding before syncFiles. It never falls back to ordinary copying and never suppresses installation or repair. Set dependencyBootstrap to off, cow\-then\-repair, or install\-only to enable deterministic project\-local package\-manager or build\-cache repair. The lifecycle is CoW seed, syncFiles, repair or install, then after\-create. CoW failures fall back to repair from an empty target; partial clones are removed and existing targets are never deleted. Bundler uses vendor/bundle only; global gem stores and caches are not cloned.
13
+
8
14
  .SH OPTIONS
9
15
  .TP
10
16
  .B \-f, \-\-force
@@ -13,6 +19,18 @@ remove and recreate the worktree if the target path already exists
13
19
  .B \-\-detached
14
20
  create a detached worktree without a branch
15
21
  .TP
22
+ .B \-\-from\-current
23
+ base the new branch on the current worktree instead of the main worktree (cannot be combined with \-\-detached)
24
+ .TP
25
+ .B \-\-no\-fetch
26
+ skip refreshing the remote default branch before creating the new branch
27
+ .TP
28
+ .B \-\-take
29
+ move current uncommitted changes into the new worktree
30
+ .TP
31
+ .B \-\-copy
32
+ copy current uncommitted changes instead of moving them (requires \-\-take)
33
+ .TP
16
34
  .B \-\-open
17
35
  open the new worktree in an editor after creation
18
36
  .TP
@@ -1,4 +1,4 @@
1
- .TH GJI\-OPEN 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-OPEN 1 "July 2026" "gji 0.11.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-open \- open the worktree in an editor
4
4
  .SH SYNOPSIS
@@ -7,6 +7,9 @@ gji\-open \- open the worktree in an editor
7
7
  open the worktree in an editor
8
8
  .SH OPTIONS
9
9
  .TP
10
+ .B \-\-select
11
+ choose a worktree with the interactive selector
12
+ .TP
10
13
  .B \-\-editor <cli>
11
14
  editor CLI to use (code, cursor, zed, windsurf, subl, …)
12
15
  .TP
package/man/man1/gji-pr.1 CHANGED
@@ -1,10 +1,16 @@
1
- .TH GJI\-PR 1 "July 2026" "gji 0.9.0" "User Commands"
1
+ .TH GJI\-PR 1 "July 2026" "gji 0.11.0" "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
5
5
  .B gji pr [\fIoptions\fR] [options] <ref>
6
6
  .SH DESCRIPTION
7
7
  fetch a pull request by number, #number, or URL into a linked worktree
8
+ .SH BOOTSTRAP_POLICY
9
+ dependencyBuildCommand overrides Cargo's default cargo check. Supported lockfiles trigger an interactive dependencyBootstrap choice when no explicit policy exists; the choice is saved according to installSaveTarget. JSON and headless modes never prompt and keep off. npm is always install\-only, and sync\-file failures stop dependency repair, install prompts, and after\-create hooks. JSON bootstrap events include structured reasons, and failed setup reports the created worktree path.
10
+
11
+ .SH BOOTSTRAP
12
+ syncDirs performs generic copy\-on\-write directory seeding before syncFiles. It never falls back to ordinary copying and never suppresses installation or repair. Set dependencyBootstrap to off, cow\-then\-repair, or install\-only to enable deterministic project\-local package\-manager or build\-cache repair. The lifecycle is CoW seed, syncFiles, repair or install, then after\-create. CoW failures fall back to repair from an empty target; partial clones are removed and existing targets are never deleted. Bundler uses vendor/bundle only; global gem stores and caches are not cloned.
13
+
8
14
  .SH OPTIONS
9
15
  .TP
10
16
  .B \-\-dry\-run