@solaqua/gji 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -426
- package/dist/back.d.ts +3 -0
- package/dist/back.js +19 -7
- package/dist/bootstrap-output.d.ts +3 -0
- package/dist/bootstrap-output.js +27 -0
- package/dist/bootstrap-preview.d.ts +10 -0
- package/dist/bootstrap-preview.js +21 -0
- package/dist/clean.js +27 -3
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +140 -22
- package/dist/clone-failure-store.d.ts +18 -0
- package/dist/clone-failure-store.js +230 -0
- package/dist/command-runner.d.ts +6 -0
- package/dist/command-runner.js +38 -0
- package/dist/completion.js +7 -2
- package/dist/config-command.d.ts +1 -0
- package/dist/config-command.js +7 -1
- package/dist/config.d.ts +26 -1
- package/dist/config.js +110 -6
- package/dist/context-card.d.ts +1 -0
- package/dist/context-card.js +25 -0
- package/dist/dependency-bootstrap-prompt.d.ts +24 -0
- package/dist/dependency-bootstrap-prompt.js +103 -0
- package/dist/dependency-bootstrap.d.ts +129 -0
- package/dist/dependency-bootstrap.js +566 -0
- package/dist/dir-clone.d.ts +33 -0
- package/dist/dir-clone.js +505 -0
- package/dist/doctor.js +21 -0
- package/dist/done.d.ts +10 -0
- package/dist/done.js +210 -0
- package/dist/file-sync.js +71 -22
- package/dist/format-bytes.d.ts +1 -0
- package/dist/format-bytes.js +14 -0
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +26 -0
- package/dist/git.d.ts +5 -0
- package/dist/git.js +50 -0
- package/dist/gji-bundle.mjs +6189 -1313
- package/dist/go-resolver.d.ts +35 -0
- package/dist/go-resolver.js +185 -0
- package/dist/go.d.ts +6 -2
- package/dist/go.js +270 -62
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.js +20 -8
- package/dist/hub.d.ts +31 -0
- package/dist/hub.js +287 -0
- package/dist/init.js +30 -22
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +3 -25
- package/dist/ls.js +9 -1
- package/dist/navigation-output.d.ts +11 -0
- package/dist/navigation-output.js +6 -0
- package/dist/new.d.ts +14 -2
- package/dist/new.js +422 -38
- package/dist/pr-open.js +2 -10
- package/dist/pr.d.ts +6 -2
- package/dist/pr.js +135 -30
- package/dist/pull-requests.d.ts +1 -0
- package/dist/pull-requests.js +32 -3
- package/dist/remove.js +22 -0
- package/dist/repo.d.ts +4 -0
- package/dist/repo.js +61 -6
- package/dist/run-hook.js +2 -0
- package/dist/safe-destination.d.ts +15 -0
- package/dist/safe-destination.js +166 -0
- package/dist/shell-completion.js +52 -13
- package/dist/slots.d.ts +8 -0
- package/dist/slots.js +61 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.js +9 -1
- package/dist/sync-directories.d.ts +33 -0
- package/dist/sync-directories.js +97 -0
- package/dist/sync-plan.d.ts +16 -0
- package/dist/sync-plan.js +128 -0
- package/dist/sync.js +7 -13
- package/dist/task-command.d.ts +9 -0
- package/dist/task-command.js +29 -0
- package/dist/task.d.ts +9 -0
- package/dist/task.js +37 -0
- package/dist/terminal-text.d.ts +4 -0
- package/dist/terminal-text.js +72 -0
- package/dist/undo.d.ts +36 -0
- package/dist/undo.js +392 -0
- package/dist/warp.d.ts +3 -1
- package/dist/warp.js +32 -125
- package/dist/worktree-bootstrap.d.ts +35 -0
- package/dist/worktree-bootstrap.js +96 -0
- package/dist/worktree-info.d.ts +6 -1
- package/dist/worktree-info.js +14 -4
- package/dist/worktree-management.d.ts +1 -0
- package/dist/worktree-management.js +23 -4
- package/dist/worktree-picker.d.ts +24 -11
- package/dist/worktree-picker.js +176 -95
- package/dist/worktree-prompts.js +1 -1
- package/dist/worktree-source.d.ts +6 -0
- package/dist/worktree-source.js +1 -0
- package/dist/worktree-sources.d.ts +4 -0
- package/dist/worktree-sources.js +77 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +19 -0
- package/man/man1/gji-go.1 +12 -3
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +21 -3
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +7 -1
- package/man/man1/gji-remove.1 +3 -3
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +16 -0
- package/man/man1/gji-undo.1 +16 -0
- package/man/man1/gji-warp.1 +3 -6
- package/man/man1/gji.1 +17 -5
- package/package.json +3 -1
package/dist/hooks.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
export async function runHook(hookCmd, cwd, context, stderr) {
|
|
2
|
+
export async function runHook(hookCmd, cwd, context, stderr, stdout = (chunk) => process.stdout.write(chunk)) {
|
|
3
3
|
if (!hookCmd)
|
|
4
4
|
return;
|
|
5
5
|
if (Array.isArray(hookCmd)) {
|
|
6
|
-
await runArgvHook(hookCmd, cwd, context, stderr);
|
|
6
|
+
await runArgvHook(hookCmd, cwd, context, stderr, stdout);
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
await runShellHook(hookCmd, cwd, context, stderr);
|
|
9
|
+
await runShellHook(hookCmd, cwd, context, stderr, stdout);
|
|
10
10
|
}
|
|
11
|
-
async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
11
|
+
async function runArgvHook(hookCmd, cwd, context, stderr, stdout) {
|
|
12
12
|
const [command, ...args] = hookCmd.map((arg) => interpolate(arg, context));
|
|
13
13
|
if (!command) {
|
|
14
14
|
stderr("gji: hook argv command must include a non-empty command\n");
|
|
@@ -18,9 +18,12 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
|
18
18
|
const child = spawn(command, args, {
|
|
19
19
|
cwd,
|
|
20
20
|
shell: false,
|
|
21
|
-
stdio: ["ignore", "
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
22
22
|
env: hookEnvironment(context),
|
|
23
23
|
});
|
|
24
|
+
child.stdout.on("data", (chunk) => {
|
|
25
|
+
stdout(chunk.toString());
|
|
26
|
+
});
|
|
24
27
|
child.stderr.on("data", (chunk) => {
|
|
25
28
|
stderr(chunk.toString());
|
|
26
29
|
});
|
|
@@ -36,15 +39,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
|
36
39
|
});
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
|
-
async function runShellHook(hookCmd, cwd, context, stderr) {
|
|
42
|
+
async function runShellHook(hookCmd, cwd, context, stderr, stdout) {
|
|
40
43
|
const interpolated = interpolate(hookCmd, context);
|
|
41
44
|
await new Promise((resolve) => {
|
|
42
45
|
const child = spawn(interpolated, {
|
|
43
46
|
cwd,
|
|
44
47
|
shell: true,
|
|
45
|
-
stdio: ["ignore", "
|
|
48
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
46
49
|
env: hookEnvironment(context),
|
|
47
50
|
});
|
|
51
|
+
child.stdout.on("data", (chunk) => {
|
|
52
|
+
stdout(chunk.toString());
|
|
53
|
+
});
|
|
48
54
|
child.stderr.on("data", (chunk) => {
|
|
49
55
|
stderr(chunk.toString());
|
|
50
56
|
});
|
|
@@ -66,6 +72,9 @@ function hookEnvironment(context) {
|
|
|
66
72
|
GJI_BRANCH: context.branch ?? "",
|
|
67
73
|
GJI_PATH: context.path,
|
|
68
74
|
GJI_REPO: context.repo,
|
|
75
|
+
...(context.slot === null || context.slot === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { GJI_SLOT: String(context.slot) }),
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
function formatArgvHook(command, args) {
|
|
@@ -75,7 +84,10 @@ export function interpolate(template, context) {
|
|
|
75
84
|
return template
|
|
76
85
|
.replace(/\{\{branch\}\}/g, context.branch ?? "")
|
|
77
86
|
.replace(/\{\{path\}\}/g, context.path)
|
|
78
|
-
.replace(/\{\{repo\}\}/g, context.repo)
|
|
87
|
+
.replace(/\{\{repo\}\}/g, context.repo)
|
|
88
|
+
.replace(/\{\{slot\}\}/g, context.slot === null || context.slot === undefined
|
|
89
|
+
? ""
|
|
90
|
+
: String(context.slot));
|
|
79
91
|
}
|
|
80
92
|
export function extractHooks(config) {
|
|
81
93
|
const raw = config.hooks;
|
package/dist/hub.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type PullRequestInfo } from "./pull-requests.js";
|
|
2
|
+
import { type WorktreeInfo } from "./worktree-info.js";
|
|
3
|
+
export interface HubCommandOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
columns?: number;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
now?: number;
|
|
8
|
+
stderr: (chunk: string) => void;
|
|
9
|
+
stdout: (chunk: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface HubCommandDependencies {
|
|
12
|
+
queryRepositoryPullRequests: (repoRoot: string) => Promise<PullRequestInfo[]>;
|
|
13
|
+
}
|
|
14
|
+
export interface HubWorktree extends WorktreeInfo {
|
|
15
|
+
lastUsedTimestamp: number | null;
|
|
16
|
+
pullRequests: PullRequestInfo[];
|
|
17
|
+
}
|
|
18
|
+
export interface HubRepository {
|
|
19
|
+
current: boolean;
|
|
20
|
+
name: string;
|
|
21
|
+
pullRequests: PullRequestInfo[];
|
|
22
|
+
root: string;
|
|
23
|
+
worktrees: HubWorktree[];
|
|
24
|
+
}
|
|
25
|
+
export interface HubData {
|
|
26
|
+
currentRepository: string | null;
|
|
27
|
+
repositories: HubRepository[];
|
|
28
|
+
}
|
|
29
|
+
export declare function buildHubData(cwd: string, dependencies?: Partial<HubCommandDependencies>): Promise<HubData>;
|
|
30
|
+
export declare function runHubCommand(options: HubCommandOptions, dependencies?: Partial<HubCommandDependencies>): Promise<number>;
|
|
31
|
+
export declare function formatHubOutput(data: HubData, columns?: number, now?: number): string;
|
package/dist/hub.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { loadHistory } from "./history.js";
|
|
3
|
+
import { createPullRequestQuery, } from "./pull-requests.js";
|
|
4
|
+
import { detectRepository } from "./repo.js";
|
|
5
|
+
import { middleEllipsize, sanitizeTerminalText, terminalWidth, } from "./terminal-text.js";
|
|
6
|
+
import { formatRelativeAge, formatUpstreamState, readWorktreeInfos, } from "./worktree-info.js";
|
|
7
|
+
import { listDiscoverableWorktreeSources } from "./worktree-sources.js";
|
|
8
|
+
const MAX_HUB_REPOSITORY_CONCURRENCY = 4;
|
|
9
|
+
const defaultDependencies = {
|
|
10
|
+
queryRepositoryPullRequests: createPullRequestQuery().listOpenPullRequestsForRepository,
|
|
11
|
+
};
|
|
12
|
+
export async function buildHubData(cwd, dependencies = {}) {
|
|
13
|
+
const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
|
|
14
|
+
defaultDependencies.queryRepositoryPullRequests;
|
|
15
|
+
const currentRepository = await detectRepository(cwd).catch(() => null);
|
|
16
|
+
const history = await loadHistory();
|
|
17
|
+
const lastUsedByPath = new Map(history.map((entry) => [entry.path, entry.timestamp]));
|
|
18
|
+
const sources = await listDiscoverableWorktreeSources(cwd);
|
|
19
|
+
const groups = new Map();
|
|
20
|
+
for (const source of sources) {
|
|
21
|
+
if (!source.repoRoot)
|
|
22
|
+
continue;
|
|
23
|
+
const repoRoot = source.repoRoot;
|
|
24
|
+
const existing = groups.get(repoRoot);
|
|
25
|
+
if (existing) {
|
|
26
|
+
existing.sources.push(source);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
groups.set(repoRoot, {
|
|
30
|
+
name: source.repoName || basename(repoRoot),
|
|
31
|
+
sources: [source],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const repositories = await mapWithConcurrency([...groups.entries()], MAX_HUB_REPOSITORY_CONCURRENCY, async ([root, group]) => {
|
|
36
|
+
const infos = await readWorktreeInfos(group.sources.map((source) => source.worktree));
|
|
37
|
+
let pullRequests = [];
|
|
38
|
+
try {
|
|
39
|
+
pullRequests = await queryRepositoryPullRequests(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// PR metadata is optional. The hub remains useful when a provider is unavailable.
|
|
43
|
+
}
|
|
44
|
+
const worktrees = infos.map((info) => ({
|
|
45
|
+
...info,
|
|
46
|
+
lastUsedTimestamp: lastUsedByPath.get(info.path) ?? null,
|
|
47
|
+
pullRequests: pullRequests.filter((pullRequest) => pullRequest.sourceBranch === info.branch),
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
current: currentRepository?.repoRoot === root,
|
|
51
|
+
name: group.name,
|
|
52
|
+
pullRequests,
|
|
53
|
+
root,
|
|
54
|
+
worktrees,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
repositories.sort((left, right) => {
|
|
58
|
+
if (left.current !== right.current)
|
|
59
|
+
return left.current ? -1 : 1;
|
|
60
|
+
return left.name.localeCompare(right.name);
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
currentRepository: currentRepository?.repoRoot ?? null,
|
|
64
|
+
repositories,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function runHubCommand(options, dependencies = {}) {
|
|
68
|
+
const data = await buildHubData(options.cwd, dependencies);
|
|
69
|
+
if (options.json) {
|
|
70
|
+
options.stdout(`${JSON.stringify(data, null, 2)}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
options.stdout(`${formatHubOutput(data, options.columns ?? process.stdout.columns ?? 80, options.now ?? Date.now())}\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
export function formatHubOutput(data, columns = process.stdout.columns ?? 80, now = Date.now()) {
|
|
77
|
+
if (data.repositories.length === 0) {
|
|
78
|
+
return "No registered repositories. Run gji from a repository to add one.";
|
|
79
|
+
}
|
|
80
|
+
const lineWidth = Math.max(1, columns);
|
|
81
|
+
const allWorktrees = data.repositories.flatMap((repository) => repository.worktrees.map((worktree) => ({ repository, worktree })));
|
|
82
|
+
const attention = allWorktrees.filter(({ worktree }) => isActionable(worktree));
|
|
83
|
+
const current = allWorktrees.find(({ worktree }) => worktree.isCurrent) ?? null;
|
|
84
|
+
const next = chooseNextWorktree(allWorktrees);
|
|
85
|
+
const remainingAttention = attention.filter((entry) => entry !== current && entry !== next);
|
|
86
|
+
const quiet = allWorktrees.filter((entry) => entry !== current && entry !== next && !attention.includes(entry));
|
|
87
|
+
const recentOther = [...quiet]
|
|
88
|
+
.sort((left, right) => (right.worktree.lastCommitTimestamp ?? 0) -
|
|
89
|
+
(left.worktree.lastCommitTimestamp ?? 0))
|
|
90
|
+
.slice(0, 5);
|
|
91
|
+
const hiddenCount = quiet.length - recentOther.length;
|
|
92
|
+
const lines = [
|
|
93
|
+
`GJI ${data.repositories.length} repositories · ${allWorktrees.length} worktrees${attention.length > 0 ? ` · ${attention.length} need attention` : ""}`,
|
|
94
|
+
"",
|
|
95
|
+
];
|
|
96
|
+
if (data.repositories.length > 1) {
|
|
97
|
+
lines.push("REPOSITORIES");
|
|
98
|
+
for (const repository of data.repositories) {
|
|
99
|
+
lines.push(formatRepositorySummary(repository, lineWidth));
|
|
100
|
+
}
|
|
101
|
+
lines.push("");
|
|
102
|
+
}
|
|
103
|
+
if (current !== null) {
|
|
104
|
+
lines.push("CURRENT");
|
|
105
|
+
lines.push(...formatHubWorktree(current, lineWidth, "@", now));
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
if (next !== null && next !== current) {
|
|
109
|
+
lines.push("NEXT");
|
|
110
|
+
lines.push(...formatHubWorktree(next, lineWidth, "›", now));
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
if (remainingAttention.length > 0) {
|
|
114
|
+
lines.push("ATTENTION");
|
|
115
|
+
for (const entry of remainingAttention) {
|
|
116
|
+
lines.push(...formatHubWorktree(entry, lineWidth, "!", now));
|
|
117
|
+
}
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
if (recentOther.length > 0) {
|
|
121
|
+
lines.push("OTHER");
|
|
122
|
+
for (const entry of recentOther) {
|
|
123
|
+
lines.push(...formatHubWorktree(entry, lineWidth, " ", now));
|
|
124
|
+
}
|
|
125
|
+
if (hiddenCount > 0) {
|
|
126
|
+
lines.push(` ${hiddenCount} quiet worktree${hiddenCount === 1 ? "" : "s"} hidden`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
if (next === null &&
|
|
131
|
+
remainingAttention.length === 0 &&
|
|
132
|
+
recentOther.length === 0) {
|
|
133
|
+
lines.push("No active worktrees.");
|
|
134
|
+
}
|
|
135
|
+
lines.push("Run `gji go <repo>/<branch>` to switch worktrees.");
|
|
136
|
+
return lines
|
|
137
|
+
.map((line) => terminalWidth(line) > lineWidth ? middleEllipsize(line, lineWidth) : line)
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trimEnd();
|
|
140
|
+
}
|
|
141
|
+
function formatRepositorySummary(repository, lineWidth) {
|
|
142
|
+
const attentionCount = repository.worktrees.filter(isActionable).length;
|
|
143
|
+
const attention = attentionCount === 0 ? "healthy" : `${attentionCount} attention`;
|
|
144
|
+
const marker = repository.current ? "*" : " ";
|
|
145
|
+
return middleEllipsize(`${marker} ${repository.name} · ${repository.worktrees.length} worktree${repository.worktrees.length === 1 ? "" : "s"} · ${attention}`, lineWidth);
|
|
146
|
+
}
|
|
147
|
+
function chooseNextWorktree(entries) {
|
|
148
|
+
return ([...entries].sort((left, right) => {
|
|
149
|
+
const scoreDifference = nextWorktreeScore(right) - nextWorktreeScore(left);
|
|
150
|
+
if (scoreDifference !== 0)
|
|
151
|
+
return scoreDifference;
|
|
152
|
+
const lastUsedDifference = (right.worktree.lastUsedTimestamp ?? 0) -
|
|
153
|
+
(left.worktree.lastUsedTimestamp ?? 0);
|
|
154
|
+
if (lastUsedDifference !== 0)
|
|
155
|
+
return lastUsedDifference;
|
|
156
|
+
return ((right.worktree.lastCommitTimestamp ?? 0) -
|
|
157
|
+
(left.worktree.lastCommitTimestamp ?? 0));
|
|
158
|
+
})[0] ?? null);
|
|
159
|
+
}
|
|
160
|
+
function nextWorktreeScore(entry) {
|
|
161
|
+
const { worktree } = entry;
|
|
162
|
+
let score = worktree.isCurrent && (worktree.task !== null || isActionable(worktree))
|
|
163
|
+
? 100
|
|
164
|
+
: 0;
|
|
165
|
+
if (worktree.task !== null)
|
|
166
|
+
score += 40;
|
|
167
|
+
if (worktree.status === "dirty")
|
|
168
|
+
score += 30;
|
|
169
|
+
if (worktree.upstream.kind === "stale")
|
|
170
|
+
score += 25;
|
|
171
|
+
if (worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0) {
|
|
172
|
+
score += 25;
|
|
173
|
+
}
|
|
174
|
+
if (worktree.pullRequests.length > 0)
|
|
175
|
+
score += 20;
|
|
176
|
+
if (worktree.lastUsedTimestamp !== null && worktree.lastUsedTimestamp > 0) {
|
|
177
|
+
score += 15;
|
|
178
|
+
}
|
|
179
|
+
return score;
|
|
180
|
+
}
|
|
181
|
+
function isActionable(worktree) {
|
|
182
|
+
return (worktree.status === "dirty" ||
|
|
183
|
+
worktree.upstream.kind === "stale" ||
|
|
184
|
+
(worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0) ||
|
|
185
|
+
worktree.pullRequests.length > 0);
|
|
186
|
+
}
|
|
187
|
+
function formatHubWorktree(entry, lineWidth, marker, now) {
|
|
188
|
+
const { repository, worktree } = entry;
|
|
189
|
+
const branch = worktree.branch ?? "(detached)";
|
|
190
|
+
const status = [
|
|
191
|
+
formatWorktreeHealth(worktree),
|
|
192
|
+
formatWorktreeUpstream(worktree),
|
|
193
|
+
worktree.pullRequests.length > 0
|
|
194
|
+
? worktree.pullRequests
|
|
195
|
+
.map((pullRequest) => pullRequest.title
|
|
196
|
+
? `#${pullRequest.number} ${formatHumanText(pullRequest.title, 60)}`
|
|
197
|
+
: `#${pullRequest.number}`)
|
|
198
|
+
.join(", ")
|
|
199
|
+
: null,
|
|
200
|
+
formatWorktreeActivity(worktree, now),
|
|
201
|
+
]
|
|
202
|
+
.filter((value) => value !== null)
|
|
203
|
+
.join(" · ");
|
|
204
|
+
const task = worktree.task === null
|
|
205
|
+
? null
|
|
206
|
+
: `task: ${formatHumanText(worktree.task, 80)}`;
|
|
207
|
+
const target = `${repository.name}/${branch}`;
|
|
208
|
+
const lines = [`${marker} ${middleEllipsize(target, lineWidth - 2)}`];
|
|
209
|
+
if (status.length > 0) {
|
|
210
|
+
lines.push(` ${middleEllipsize(status, lineWidth - 2)}`);
|
|
211
|
+
}
|
|
212
|
+
if (task !== null) {
|
|
213
|
+
lines.push(` ${middleEllipsize(task, lineWidth - 2)}`);
|
|
214
|
+
}
|
|
215
|
+
if (marker === "@" || marker === "›" || marker === "!") {
|
|
216
|
+
lines.push(` ${middleEllipsize(nextAction(worktree), lineWidth - 2)}`);
|
|
217
|
+
}
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
function formatWorktreeHealth(worktree) {
|
|
221
|
+
if (worktree.status === "dirty")
|
|
222
|
+
return "! dirty";
|
|
223
|
+
if (worktree.status === "unknown")
|
|
224
|
+
return "? health unknown";
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function formatWorktreeUpstream(worktree) {
|
|
228
|
+
if (worktree.upstream.kind === "stale")
|
|
229
|
+
return "! stale upstream";
|
|
230
|
+
if (worktree.upstream.kind === "tracked" &&
|
|
231
|
+
(worktree.upstream.behind > 0 || worktree.upstream.ahead > 0)) {
|
|
232
|
+
return formatUpstreamState(worktree.upstream);
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function formatWorktreeActivity(worktree, now) {
|
|
237
|
+
if (worktree.isCurrent)
|
|
238
|
+
return null;
|
|
239
|
+
const timestamps = [
|
|
240
|
+
worktree.lastUsedTimestamp ?? 0,
|
|
241
|
+
(worktree.lastCommitTimestamp ?? 0) * 1000,
|
|
242
|
+
].filter((timestamp) => timestamp > 0);
|
|
243
|
+
if (timestamps.length === 0)
|
|
244
|
+
return null;
|
|
245
|
+
const latest = Math.max(...timestamps);
|
|
246
|
+
const ageSeconds = Math.floor((now - latest) / 1000);
|
|
247
|
+
if (ageSeconds < 24 * 60 * 60)
|
|
248
|
+
return null;
|
|
249
|
+
return `inactive ${formatRelativeAge(Math.floor(latest / 1000), Math.floor(now / 1000))}`;
|
|
250
|
+
}
|
|
251
|
+
function nextAction(worktree) {
|
|
252
|
+
if (worktree.isCurrent && worktree.task !== null) {
|
|
253
|
+
return "next: continue task";
|
|
254
|
+
}
|
|
255
|
+
if (worktree.status === "dirty") {
|
|
256
|
+
return "next: switch → inspect changes";
|
|
257
|
+
}
|
|
258
|
+
if (worktree.upstream.kind === "stale" ||
|
|
259
|
+
(worktree.upstream.kind === "tracked" && worktree.upstream.behind > 0)) {
|
|
260
|
+
return "next: switch → sync";
|
|
261
|
+
}
|
|
262
|
+
const pullRequest = worktree.pullRequests[0];
|
|
263
|
+
if (pullRequest !== undefined) {
|
|
264
|
+
return `next: switch → open PR '#${pullRequest.number}'`;
|
|
265
|
+
}
|
|
266
|
+
return "next: switch worktree";
|
|
267
|
+
}
|
|
268
|
+
function formatHumanText(value, maxLength) {
|
|
269
|
+
const sanitized = sanitizeTerminalText(value);
|
|
270
|
+
return sanitized.length <= maxLength
|
|
271
|
+
? sanitized
|
|
272
|
+
: `${sanitized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
273
|
+
}
|
|
274
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
275
|
+
const results = new Array(items.length);
|
|
276
|
+
let nextIndex = 0;
|
|
277
|
+
async function readNext() {
|
|
278
|
+
for (;;) {
|
|
279
|
+
const index = nextIndex++;
|
|
280
|
+
if (index >= items.length)
|
|
281
|
+
return;
|
|
282
|
+
results[index] = await mapper(items[index]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
|
|
286
|
+
return results;
|
|
287
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -12,49 +12,56 @@ import { SHELL_INTEGRATION_END_MARKER as END_MARKER, executableExists, hasShellI
|
|
|
12
12
|
const ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
|
|
13
13
|
const SHELL_WRAPPED_COMMANDS = [
|
|
14
14
|
{
|
|
15
|
-
bypassOptions: ["--help"],
|
|
15
|
+
bypassOptions: ["--help", "-h", "--json"],
|
|
16
16
|
commandName: "new",
|
|
17
17
|
envVar: "GJI_NEW_OUTPUT_FILE",
|
|
18
18
|
names: ["new"],
|
|
19
19
|
tempPrefix: "gji-new",
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
bypassOptions: ["--help"],
|
|
22
|
+
bypassOptions: ["--help", "-h", "--json"],
|
|
23
23
|
commandName: "pr",
|
|
24
24
|
envVar: "GJI_PR_OUTPUT_FILE",
|
|
25
25
|
names: ["pr"],
|
|
26
26
|
tempPrefix: "gji-pr",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
|
-
bypassOptions: ["--print"],
|
|
29
|
+
bypassOptions: ["--print", "--help", "-h"],
|
|
30
30
|
commandName: "back",
|
|
31
31
|
envVar: "GJI_BACK_OUTPUT_FILE",
|
|
32
32
|
names: ["back"],
|
|
33
33
|
tempPrefix: "gji-back",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
bypassOptions: ["--print"],
|
|
36
|
+
bypassOptions: ["--print", "--json", "--help", "-h"],
|
|
37
37
|
commandName: "go",
|
|
38
38
|
envVar: "GJI_GO_OUTPUT_FILE",
|
|
39
39
|
names: ["go", "jump"],
|
|
40
40
|
tempPrefix: "gji-go",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
bypassOptions: ["--print"],
|
|
43
|
+
bypassOptions: ["--print", "--help", "-h"],
|
|
44
44
|
commandName: "root",
|
|
45
45
|
envVar: "GJI_ROOT_OUTPUT_FILE",
|
|
46
46
|
names: ["root"],
|
|
47
47
|
tempPrefix: "gji-root",
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
-
bypassOptions: ["--help"],
|
|
50
|
+
bypassOptions: ["--help", "-h", "--json"],
|
|
51
51
|
commandName: "remove",
|
|
52
52
|
envVar: "GJI_REMOVE_OUTPUT_FILE",
|
|
53
53
|
names: ["remove", "rm"],
|
|
54
54
|
tempPrefix: "gji-remove",
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
|
-
bypassOptions: ["--
|
|
57
|
+
bypassOptions: ["--help", "-h", "--json"],
|
|
58
|
+
commandName: "done",
|
|
59
|
+
envVar: "GJI_DONE_OUTPUT_FILE",
|
|
60
|
+
names: ["done"],
|
|
61
|
+
tempPrefix: "gji-done",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
bypassOptions: ["--print", "--json", "--help", "-h"],
|
|
58
65
|
commandName: "warp",
|
|
59
66
|
envVar: "GJI_WARP_OUTPUT_FILE",
|
|
60
67
|
names: ["warp"],
|
|
@@ -405,16 +412,17 @@ function renderFishWrapper(command) {
|
|
|
405
412
|
const nameCondition = nameTests.length === 1
|
|
406
413
|
? nameTests[0]
|
|
407
414
|
: `begin; ${nameTests.join("; or ")}; end`;
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
415
|
+
const bypassBlock = `if test (count $argv) -gt 0
|
|
416
|
+
for arg in $argv
|
|
417
|
+
if ${command.bypassOptions.map((opt) => `test $arg = ${opt}`).join("; or ")}
|
|
418
|
+
command gji ${command.commandName} $argv
|
|
419
|
+
return $status
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end`;
|
|
412
423
|
return `if test (count $argv) -gt 0; and ${nameCondition}
|
|
413
424
|
set -e argv[1]
|
|
414
|
-
|
|
415
|
-
command gji ${command.commandName} $argv
|
|
416
|
-
return $status
|
|
417
|
-
end
|
|
425
|
+
${bypassBlock}
|
|
418
426
|
|
|
419
427
|
set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
|
|
420
428
|
or return 1
|
|
@@ -434,15 +442,15 @@ function renderPosixWrapper(command) {
|
|
|
434
442
|
const tests = command.names
|
|
435
443
|
.map((name) => `[ "$1" = "${name}" ]`)
|
|
436
444
|
.join(" || ");
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
445
|
+
const bypassBlock = `for arg do
|
|
446
|
+
if ${command.bypassOptions.map((opt) => `[ "$arg" = "${opt}" ]`).join(" || ")}; then
|
|
447
|
+
command gji ${command.commandName} "$@"
|
|
448
|
+
return $?
|
|
449
|
+
fi
|
|
450
|
+
done`;
|
|
440
451
|
return `if ${tests}; then
|
|
441
452
|
shift
|
|
442
|
-
|
|
443
|
-
command gji ${command.commandName} "$@"
|
|
444
|
-
return $?
|
|
445
|
-
fi
|
|
453
|
+
${bypassBlock}
|
|
446
454
|
|
|
447
455
|
local target
|
|
448
456
|
local output_file
|
package/dist/install-prompt.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export interface InstallPromptDependencies {
|
|
|
9
9
|
writeGlobalRepoConfigKey?: (repoRoot: string, key: string, value: unknown) => Promise<void>;
|
|
10
10
|
}
|
|
11
11
|
export declare function maybeRunInstallPrompt(worktreePath: string, repoRoot: string, config: GjiConfig, stderr: (chunk: string) => void, dependencies?: InstallPromptDependencies, nonInteractive?: boolean): Promise<void>;
|
|
12
|
+
export declare const runInstallCommand: import("./command-runner.js").CommandRunner;
|
package/dist/install-prompt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { isCancel, select } from "@clack/prompts";
|
|
2
|
+
import { runCommand } from "./command-runner.js";
|
|
3
3
|
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
|
|
4
4
|
import { isHeadless } from "./headless.js";
|
|
5
5
|
import { extractHooks } from "./hooks.js";
|
|
@@ -28,7 +28,7 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
if (choice === "yes" || choice === "always") {
|
|
31
|
-
const runner = dependencies.runInstallCommand ??
|
|
31
|
+
const runner = dependencies.runInstallCommand ?? runInstallCommand;
|
|
32
32
|
try {
|
|
33
33
|
await runner(pm.installCommand, worktreePath, stderr);
|
|
34
34
|
}
|
|
@@ -88,29 +88,7 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
await new Promise((resolve, reject) => {
|
|
93
|
-
const child = spawn(command, {
|
|
94
|
-
cwd,
|
|
95
|
-
shell: true,
|
|
96
|
-
stdio: ["ignore", "inherit", "pipe"],
|
|
97
|
-
});
|
|
98
|
-
child.stderr.on("data", (chunk) => {
|
|
99
|
-
stderr(chunk.toString());
|
|
100
|
-
});
|
|
101
|
-
child.on("close", (code) => {
|
|
102
|
-
if (code !== 0) {
|
|
103
|
-
reject(new Error(`exited with code ${code}`));
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
resolve();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
child.on("error", (err) => {
|
|
110
|
-
reject(err);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
91
|
+
export const runInstallCommand = runCommand;
|
|
114
92
|
async function defaultWriteConfigKey(root, key, value) {
|
|
115
93
|
await updateLocalConfigKey(root, key, value);
|
|
116
94
|
}
|
package/dist/ls.js
CHANGED
|
@@ -26,12 +26,15 @@ export function formatDetailedWorktreeTable(worktrees) {
|
|
|
26
26
|
lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
|
|
27
27
|
path: worktree.path,
|
|
28
28
|
status: worktree.status,
|
|
29
|
+
task: worktree.task,
|
|
29
30
|
upstream: formatUpstreamState(worktree.upstream),
|
|
30
31
|
}));
|
|
31
32
|
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
32
33
|
const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
|
|
33
34
|
const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
|
|
34
35
|
const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
|
|
36
|
+
const hasTasks = rows.some((row) => row.task !== null);
|
|
37
|
+
const taskWidth = Math.max("TASK".length, ...rows.map((row) => row.task?.slice(0, 40).length ?? 0));
|
|
35
38
|
const lines = [
|
|
36
39
|
" " +
|
|
37
40
|
"BRANCH".padEnd(branchWidth, " ") +
|
|
@@ -41,6 +44,7 @@ export function formatDetailedWorktreeTable(worktrees) {
|
|
|
41
44
|
"UPSTREAM".padEnd(upstreamWidth, " ") +
|
|
42
45
|
" " +
|
|
43
46
|
"LAST".padEnd(lastCommitWidth, " ") +
|
|
47
|
+
(hasTasks ? " " + "TASK".padEnd(taskWidth, " ") : "") +
|
|
44
48
|
" PATH",
|
|
45
49
|
];
|
|
46
50
|
for (const row of rows) {
|
|
@@ -48,7 +52,11 @@ export function formatDetailedWorktreeTable(worktrees) {
|
|
|
48
52
|
`${row.branch.padEnd(branchWidth, " ")} ` +
|
|
49
53
|
`${row.status.padEnd(statusWidth, " ")} ` +
|
|
50
54
|
`${row.upstream.padEnd(upstreamWidth, " ")} ` +
|
|
51
|
-
`${row.lastCommit.padEnd(lastCommitWidth, " ")}
|
|
55
|
+
`${row.lastCommit.padEnd(lastCommitWidth, " ")}` +
|
|
56
|
+
(hasTasks
|
|
57
|
+
? ` ${(row.task?.slice(0, 40) ?? "").padEnd(taskWidth, " ")}`
|
|
58
|
+
: "") +
|
|
59
|
+
" " +
|
|
52
60
|
row.path);
|
|
53
61
|
}
|
|
54
62
|
return lines.join("\n");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface NavigationRepository {
|
|
2
|
+
name: string;
|
|
3
|
+
root: string;
|
|
4
|
+
}
|
|
5
|
+
export interface NavigationTarget {
|
|
6
|
+
branch: string | null;
|
|
7
|
+
path: string;
|
|
8
|
+
repository: NavigationRepository;
|
|
9
|
+
}
|
|
10
|
+
export declare function createNavigationTarget(repository: NavigationRepository, path: string, branch: string | null): NavigationTarget;
|
|
11
|
+
export declare function createNavigationRepository(name: string, root: string): NavigationRepository;
|
package/dist/new.d.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type DependencyBootstrapPromptDependencies } from "./dependency-bootstrap-prompt.js";
|
|
3
|
+
import { type CloneDirectory } from "./dir-clone.js";
|
|
4
|
+
import type { InstallPromptDependencies } from "./install-prompt.js";
|
|
3
5
|
export type { PathConflictChoice };
|
|
6
|
+
export type NewWorktreeMode = "create" | "checkout" | "track";
|
|
4
7
|
export interface NewCommandOptions {
|
|
5
8
|
branch?: string;
|
|
9
|
+
copy?: boolean;
|
|
6
10
|
cwd: string;
|
|
7
11
|
detached?: boolean;
|
|
8
12
|
dryRun?: boolean;
|
|
9
13
|
editor?: string;
|
|
10
14
|
fromCurrent?: boolean;
|
|
11
15
|
force?: boolean;
|
|
16
|
+
noFetch?: boolean;
|
|
12
17
|
json?: boolean;
|
|
18
|
+
mode?: NewWorktreeMode;
|
|
13
19
|
open?: boolean;
|
|
20
|
+
outputEnv?: string;
|
|
21
|
+
remote?: string;
|
|
22
|
+
take?: boolean;
|
|
14
23
|
stderr: (chunk: string) => void;
|
|
15
24
|
stdout: (chunk: string) => void;
|
|
25
|
+
task?: string;
|
|
16
26
|
}
|
|
17
|
-
export interface NewCommandDependencies extends InstallPromptDependencies {
|
|
27
|
+
export interface NewCommandDependencies extends InstallPromptDependencies, DependencyBootstrapPromptDependencies {
|
|
28
|
+
cloneDir: CloneDirectory;
|
|
18
29
|
createBranchPlaceholder: () => string;
|
|
19
30
|
promptForBranch: (placeholder: string) => Promise<string | null>;
|
|
31
|
+
promptForFetchFailure: (message: string) => Promise<boolean>;
|
|
20
32
|
promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
|
|
21
33
|
spawnEditor: (cli: string, args: string[]) => Promise<void>;
|
|
22
34
|
}
|