@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.
- package/README.md +93 -20
- 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 +25 -3
- package/dist/cli.js +105 -19
- 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/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/done.d.ts +10 -0
- package/dist/done.js +208 -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 +5489 -1206
- package/dist/go-resolver.d.ts +35 -0
- package/dist/go-resolver.js +185 -0
- package/dist/go.d.ts +5 -2
- package/dist/go.js +262 -62
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +13 -7
- package/dist/init.js +30 -22
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +3 -25
- 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 +434 -36
- package/dist/open.d.ts +1 -0
- package/dist/open.js +14 -2
- package/dist/pr.d.ts +6 -2
- package/dist/pr.js +135 -30
- package/dist/remove.js +20 -0
- package/dist/repo.d.ts +3 -0
- package/dist/repo.js +57 -6
- package/dist/safe-destination.d.ts +15 -0
- package/dist/safe-destination.js +166 -0
- package/dist/shell-completion.js +47 -14
- 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/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 +93 -0
- package/dist/worktree-management.d.ts +1 -0
- package/dist/worktree-management.js +23 -4
- package/dist/worktree-picker.d.ts +21 -8
- package/dist/worktree-picker.js +107 -15
- 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 +3 -0
- package/dist/worktree-sources.js +31 -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 +9 -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 +4 -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-undo.1 +16 -0
- package/man/man1/gji-warp.1 +3 -6
- package/man/man1/gji.1 +13 -5
- 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
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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;
|
package/dist/worktree-picker.js
CHANGED
|
@@ -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 =
|
|
11
|
-
(dependencies.
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
51
|
+
return [];
|
|
33
52
|
const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
|
|
34
53
|
if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery))
|
|
35
|
-
return
|
|
36
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
552
|
+
return this.entries;
|
|
464
553
|
}
|
|
465
|
-
return filterSearchablePromptEntries(this.
|
|
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";
|
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,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
|
+
}
|
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.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.
|
|
1
|
+
.TH GJI\-GO 1 "July 2026" "gji 0.11.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 \-\-json
|
|
17
|
+
emit JSON for an existing worktree destination
|
|
12
18
|
.SH "SEE ALSO"
|
|
13
19
|
.BR gji (1)
|
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
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
.TH GJI\-NEW 1 "July 2026" "gji 0.
|
|
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
|
package/man/man1/gji-open.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH GJI\-OPEN 1 "July 2026" "gji 0.
|
|
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.
|
|
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
|