@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DependencyBootstrapMode } from "./config.js";
|
|
2
|
+
import { type BootstrapTarget, type DependencyBootstrapPreview } from "./dependency-bootstrap.js";
|
|
3
|
+
export declare function createDependencyBootstrapPreview(mode: DependencyBootstrapMode, context: {
|
|
4
|
+
repoRoot: string;
|
|
5
|
+
currentRoot?: string;
|
|
6
|
+
worktreePath: string;
|
|
7
|
+
cargoBuildCommand?: string;
|
|
8
|
+
checkUvRuntime?: (target: BootstrapTarget) => Promise<boolean>;
|
|
9
|
+
}): Promise<DependencyBootstrapPreview>;
|
|
10
|
+
export declare function formatDependencyBootstrapPreview(preview: DependencyBootstrapPreview | undefined): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { prepareDependencyBootstrap, previewDependencyBootstrap, } from "./dependency-bootstrap.js";
|
|
2
|
+
export async function createDependencyBootstrapPreview(mode, context) {
|
|
3
|
+
return previewDependencyBootstrap(await prepareDependencyBootstrap(mode, context));
|
|
4
|
+
}
|
|
5
|
+
export function formatDependencyBootstrapPreview(preview) {
|
|
6
|
+
if (!preview)
|
|
7
|
+
return "";
|
|
8
|
+
return preview.targets
|
|
9
|
+
.map(({ adapter, target, strategy }) => `Would ${formatBootstrapStrategy(strategy)} ${target} with ${adapter}\n`)
|
|
10
|
+
.join("");
|
|
11
|
+
}
|
|
12
|
+
function formatBootstrapStrategy(strategy) {
|
|
13
|
+
switch (strategy) {
|
|
14
|
+
case "cow-then-repair":
|
|
15
|
+
return "seed and repair";
|
|
16
|
+
case "repair-only":
|
|
17
|
+
return "repair";
|
|
18
|
+
case "install-only":
|
|
19
|
+
return "install";
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/clean.js
CHANGED
|
@@ -2,8 +2,10 @@ import { confirm, isCancel } from "@clack/prompts";
|
|
|
2
2
|
import { loadEffectiveConfig } from "./config.js";
|
|
3
3
|
import { isBranchMergedInto, readWorktreeHealth, resolveRemoteDefaultBranch, runGit, } from "./git.js";
|
|
4
4
|
import { isHeadless } from "./headless.js";
|
|
5
|
+
import { releaseWorktreeSlot } from "./slots.js";
|
|
6
|
+
import { finalizeUndoOperation, recordUndoOperation } from "./undo.js";
|
|
5
7
|
import { formatLastCommit, formatUpstreamState, readWorktreeInfos, serializeWorktreeInfo, } from "./worktree-info.js";
|
|
6
|
-
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDeletionError, isWorktreeForceRemovalError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
|
|
8
|
+
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
|
|
7
9
|
import { buildWorktreePromptEntries, promptForMultipleWorktrees, } from "./worktree-picker.js";
|
|
8
10
|
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
|
|
9
11
|
export function createCleanCommand(dependencies = {}) {
|
|
@@ -79,6 +81,18 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
79
81
|
}
|
|
80
82
|
const removedWorktrees = [];
|
|
81
83
|
const failures = [];
|
|
84
|
+
let journal;
|
|
85
|
+
try {
|
|
86
|
+
journal = await recordUndoOperation("clean", repository.repoRoot, selectedWorktrees);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
emitError(options, `could not write undo journal; no worktrees were removed: ${toMessage(error)}`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
if (!journal) {
|
|
93
|
+
emitError(options, "could not capture undo state; no worktrees were removed");
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
82
96
|
for (const worktree of selectedWorktrees) {
|
|
83
97
|
if (options.stale &&
|
|
84
98
|
!(await isStaleCleanupCandidate(repository.repoRoot, worktree, staleBaseRef))) {
|
|
@@ -97,7 +111,9 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
97
111
|
});
|
|
98
112
|
continue;
|
|
99
113
|
}
|
|
100
|
-
if (options.stale &&
|
|
114
|
+
if (options.stale &&
|
|
115
|
+
!isWorktreeDeletionError(error) &&
|
|
116
|
+
!isSubmoduleWorktreeRemovalError(error)) {
|
|
101
117
|
options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate\n`);
|
|
102
118
|
continue;
|
|
103
119
|
}
|
|
@@ -129,7 +145,12 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
129
145
|
}
|
|
130
146
|
catch (error) {
|
|
131
147
|
if (!isBranchUnmergedError(error)) {
|
|
132
|
-
|
|
148
|
+
failures.push({
|
|
149
|
+
branch: worktree.branch,
|
|
150
|
+
message: toMessage(error),
|
|
151
|
+
path: worktree.path,
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
133
154
|
}
|
|
134
155
|
if (options.force ||
|
|
135
156
|
(await confirmForceDeleteBranch(worktree.branch))) {
|
|
@@ -146,6 +167,8 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
146
167
|
}
|
|
147
168
|
}
|
|
148
169
|
}
|
|
170
|
+
await finalizeUndoOperation(journal, removedWorktrees);
|
|
171
|
+
await Promise.all(removedWorktrees.map((worktree) => releaseWorktreeSlot(worktree.path)));
|
|
149
172
|
if (options.json) {
|
|
150
173
|
const removed = removedWorktrees.map((worktree) => {
|
|
151
174
|
const info = selectedInfoByPath.get(worktree.path);
|
|
@@ -160,6 +183,7 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
160
183
|
reportCleanFailures(failures, options.stderr);
|
|
161
184
|
}
|
|
162
185
|
else {
|
|
186
|
+
options.stderr("undo: gji undo\n");
|
|
163
187
|
options.stdout(`${repository.repoRoot}\n`);
|
|
164
188
|
}
|
|
165
189
|
return failures.length === 0 ? 0 : 1;
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type HubCommandDependencies } from "./hub.js";
|
|
2
3
|
import { type PrOpenCommandDependencies } from "./pr-open.js";
|
|
3
4
|
export interface RunCliOptions {
|
|
5
|
+
columns?: number;
|
|
4
6
|
cwd?: string;
|
|
7
|
+
now?: number;
|
|
5
8
|
prOpenDependencies?: Partial<PrOpenCommandDependencies>;
|
|
9
|
+
hubDependencies?: Partial<HubCommandDependencies>;
|
|
6
10
|
stderr?: (chunk: string) => void;
|
|
7
11
|
stdout?: (chunk: string) => void;
|
|
8
12
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,9 +6,11 @@ import { runCleanCommand } from "./clean.js";
|
|
|
6
6
|
import { runCompletionCommand } from "./completion.js";
|
|
7
7
|
import { runConfigCommand } from "./config-command.js";
|
|
8
8
|
import { runDoctorCommand } from "./doctor.js";
|
|
9
|
+
import { runDoneCommand } from "./done.js";
|
|
9
10
|
import { runGoCommand } from "./go.js";
|
|
10
11
|
import { isHeadless } from "./headless.js";
|
|
11
12
|
import { runHistoryCommand } from "./history-command.js";
|
|
13
|
+
import { runHubCommand } from "./hub.js";
|
|
12
14
|
import { runInitCommand } from "./init.js";
|
|
13
15
|
import { runLsCommand } from "./ls.js";
|
|
14
16
|
import { runNewCommand } from "./new.js";
|
|
@@ -23,6 +25,8 @@ import { runHookCommand } from "./run-hook.js";
|
|
|
23
25
|
import { runStatusCommand } from "./status.js";
|
|
24
26
|
import { runSyncCommand } from "./sync.js";
|
|
25
27
|
import { runSyncFilesCommand } from "./sync-files-command.js";
|
|
28
|
+
import { runTaskCommand } from "./task-command.js";
|
|
29
|
+
import { runUndoCommand } from "./undo.js";
|
|
26
30
|
import { runWarpCommand } from "./warp.js";
|
|
27
31
|
export function createProgram() {
|
|
28
32
|
const program = new Command();
|
|
@@ -34,6 +38,11 @@ export function createProgram() {
|
|
|
34
38
|
.showHelpAfterError()
|
|
35
39
|
.showSuggestionAfterError();
|
|
36
40
|
registerCommands(program);
|
|
41
|
+
program.addHelpText("after", "\nCommon workflows:\n" +
|
|
42
|
+
" gji new <branch> create a worktree\n" +
|
|
43
|
+
" gji go <branch> navigate to a worktree\n" +
|
|
44
|
+
" gji go choose; press Tab for all repositories\n" +
|
|
45
|
+
" gji go - return to the previous worktree\n");
|
|
37
46
|
return program;
|
|
38
47
|
}
|
|
39
48
|
function readPackageMetadata() {
|
|
@@ -46,21 +55,28 @@ function readPackageMetadata() {
|
|
|
46
55
|
}
|
|
47
56
|
export async function runCli(argv, options = {}) {
|
|
48
57
|
await maybeNotifyForUpdates(argv);
|
|
49
|
-
if (argv[0] !== "doctor") {
|
|
50
|
-
maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
|
|
51
|
-
}
|
|
52
|
-
const program = createProgram();
|
|
53
58
|
const cwd = options.cwd ?? process.cwd();
|
|
54
59
|
const stdout = options.stdout ?? (() => undefined);
|
|
55
60
|
const stderr = options.stderr ?? (() => undefined);
|
|
61
|
+
if (shouldRegisterCurrentRepo(argv) || isHubInvocation(argv)) {
|
|
62
|
+
await maybeRegisterCurrentRepo(cwd);
|
|
63
|
+
}
|
|
64
|
+
const program = createProgram();
|
|
56
65
|
program.configureOutput({
|
|
57
66
|
writeErr: stderr,
|
|
58
67
|
writeOut: stdout,
|
|
59
68
|
});
|
|
60
69
|
program.exitOverride();
|
|
61
|
-
if (argv
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
if (isHubInvocation(argv)) {
|
|
71
|
+
const exitCode = await runHubCommand({
|
|
72
|
+
columns: options.columns,
|
|
73
|
+
cwd,
|
|
74
|
+
json: argv.includes("--json"),
|
|
75
|
+
now: options.now,
|
|
76
|
+
stderr,
|
|
77
|
+
stdout,
|
|
78
|
+
}, options.hubDependencies);
|
|
79
|
+
return { exitCode };
|
|
64
80
|
}
|
|
65
81
|
try {
|
|
66
82
|
attachCommandActions(program, {
|
|
@@ -109,23 +125,62 @@ function defaultNotifyForUpdates(pkg) {
|
|
|
109
125
|
const notifier = updateNotifier({ pkg });
|
|
110
126
|
notifier.notify();
|
|
111
127
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
function shouldRegisterCurrentRepo(argv) {
|
|
129
|
+
const command = argv[0];
|
|
130
|
+
return (argv.length > 0 &&
|
|
131
|
+
![
|
|
132
|
+
"--help",
|
|
133
|
+
"-h",
|
|
134
|
+
"--version",
|
|
135
|
+
"-V",
|
|
136
|
+
"completion",
|
|
137
|
+
"config",
|
|
138
|
+
"doctor",
|
|
139
|
+
"help",
|
|
140
|
+
"init",
|
|
141
|
+
].includes(command ?? ""));
|
|
142
|
+
}
|
|
143
|
+
function isHubInvocation(argv) {
|
|
144
|
+
return argv.length === 0 || (argv.length === 1 && argv[0] === "--json");
|
|
145
|
+
}
|
|
146
|
+
async function maybeRegisterCurrentRepo(cwd) {
|
|
147
|
+
try {
|
|
148
|
+
const { repoRoot } = await detectRepository(cwd);
|
|
149
|
+
await registerRepo(repoRoot);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Registration is best effort; command behaviour should not depend on it.
|
|
153
|
+
}
|
|
116
154
|
}
|
|
117
155
|
function registerCommands(program) {
|
|
118
156
|
program
|
|
119
157
|
.command("new [branch]")
|
|
120
|
-
.description("create a new branch or detached linked worktree")
|
|
158
|
+
.description("create a new branch or detached linked worktree and CoW-bootstrap configured directories")
|
|
121
159
|
.option("-f, --force", "remove and recreate the worktree if the target path already exists")
|
|
122
160
|
.option("--detached", "create a detached worktree without a branch")
|
|
123
161
|
.option("--from-current", "base the new branch on the current worktree instead of the main worktree (cannot be combined with --detached)")
|
|
162
|
+
.option("--no-fetch", "skip refreshing the remote default branch before creating the new branch")
|
|
163
|
+
.option("--take", "move current uncommitted changes into the new worktree")
|
|
164
|
+
.option("--task <description>", "record the purpose of the new worktree")
|
|
165
|
+
.option("--copy", "copy current uncommitted changes instead of moving them (requires --take)")
|
|
124
166
|
.option("--open", "open the new worktree in an editor after creation")
|
|
125
167
|
.option("--editor <cli>", "editor CLI to use with --open (code, cursor, zed, …)")
|
|
126
168
|
.option("--dry-run", "show what would be created without executing any git commands or writing files")
|
|
127
169
|
.option("--json", "emit JSON on success or error instead of human-readable output")
|
|
128
170
|
.action(notImplemented("new"));
|
|
171
|
+
program
|
|
172
|
+
.command("done [branch]")
|
|
173
|
+
.description("finish the current linked worktree and return safely")
|
|
174
|
+
.option("--force", "remove dirty or unmerged worktrees without prompting")
|
|
175
|
+
.option("--keep-branch", "remove the worktree but preserve its branch")
|
|
176
|
+
.option("--json", "emit JSON on success or error instead of human-readable output")
|
|
177
|
+
.action(notImplemented("done"));
|
|
178
|
+
program
|
|
179
|
+
.command("undo [id]")
|
|
180
|
+
.description("restore the most recent destructive worktree operation")
|
|
181
|
+
.option("--list", "list recoverable operations")
|
|
182
|
+
.option("--json", "emit JSON on success or error instead of human-readable output")
|
|
183
|
+
.action(notImplemented("undo"));
|
|
129
184
|
program
|
|
130
185
|
.command("init [shell]")
|
|
131
186
|
.description("set up shell integration interactively or print a shell wrapper")
|
|
@@ -176,8 +231,11 @@ function registerCommands(program) {
|
|
|
176
231
|
program
|
|
177
232
|
.command("go [branch]")
|
|
178
233
|
.alias("jump")
|
|
179
|
-
.description("
|
|
234
|
+
.description("resolve and jump to a worktree path")
|
|
235
|
+
.option("--root", "navigate to the main repository root")
|
|
180
236
|
.option("--print", "print the resolved worktree path explicitly")
|
|
237
|
+
.option("--quiet", "suppress the context card")
|
|
238
|
+
.option("--json", "emit JSON for an existing worktree destination")
|
|
181
239
|
.action(notImplemented("go"));
|
|
182
240
|
program
|
|
183
241
|
.command("root")
|
|
@@ -189,6 +247,12 @@ function registerCommands(program) {
|
|
|
189
247
|
.description("summarize repository and worktree health")
|
|
190
248
|
.option("--json", "print repository and worktree health as JSON")
|
|
191
249
|
.action(notImplemented("status"));
|
|
250
|
+
program
|
|
251
|
+
.command("task [task]")
|
|
252
|
+
.description("show or update the current worktree task")
|
|
253
|
+
.option("--clear", "clear the current worktree task")
|
|
254
|
+
.option("--json", "emit task metadata as JSON")
|
|
255
|
+
.action(notImplemented("task"));
|
|
192
256
|
program
|
|
193
257
|
.command("sync")
|
|
194
258
|
.description("fetch and update one or all worktrees")
|
|
@@ -233,7 +297,7 @@ function registerCommands(program) {
|
|
|
233
297
|
program
|
|
234
298
|
.command("remove [branch]")
|
|
235
299
|
.alias("rm")
|
|
236
|
-
.description("
|
|
300
|
+
.description("deprecated: use gji done for one worktree or gji clean for bulk cleanup")
|
|
237
301
|
.option("-f, --force", "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch")
|
|
238
302
|
.option("--dry-run", "show what would be deleted without removing anything")
|
|
239
303
|
.option("--json", "emit JSON on success or error instead of human-readable output")
|
|
@@ -245,8 +309,7 @@ function registerCommands(program) {
|
|
|
245
309
|
.action(notImplemented("run-hook"));
|
|
246
310
|
program
|
|
247
311
|
.command("warp [branch]")
|
|
248
|
-
.description("
|
|
249
|
-
.option("-n, --new [branch]", "create a new worktree in a registered repo")
|
|
312
|
+
.description("deprecated: use gji go (press Tab for all known repos)")
|
|
250
313
|
// --print is the shell-wrapper bypass signal (see SHELL_WRAPPED_COMMANDS in init.ts).
|
|
251
314
|
// The shell omits GJI_WARP_OUTPUT_FILE, so writeShellOutput falls through to stdout.
|
|
252
315
|
.option("--print", "print the resolved worktree path without changing directory")
|
|
@@ -276,18 +339,51 @@ function attachCommandActions(program, options) {
|
|
|
276
339
|
const exitCode = await runNewCommand({
|
|
277
340
|
...options,
|
|
278
341
|
branch,
|
|
342
|
+
copy: commandOptions.copy,
|
|
279
343
|
detached: commandOptions.detached,
|
|
280
344
|
dryRun: commandOptions.dryRun,
|
|
281
345
|
editor: commandOptions.editor,
|
|
282
346
|
fromCurrent: commandOptions.fromCurrent,
|
|
283
347
|
force: commandOptions.force,
|
|
348
|
+
noFetch: commandOptions.fetch === false,
|
|
284
349
|
json: commandOptions.json,
|
|
285
350
|
open: commandOptions.open,
|
|
351
|
+
take: commandOptions.take,
|
|
352
|
+
task: commandOptions.task,
|
|
286
353
|
});
|
|
287
354
|
if (exitCode !== 0) {
|
|
288
355
|
throw commanderExit(exitCode);
|
|
289
356
|
}
|
|
290
357
|
});
|
|
358
|
+
program.commands
|
|
359
|
+
.find((command) => command.name() === "done")
|
|
360
|
+
?.action(async (branch, commandOptions) => {
|
|
361
|
+
const exitCode = await runDoneCommand({
|
|
362
|
+
branch,
|
|
363
|
+
cwd: options.cwd,
|
|
364
|
+
force: commandOptions.force,
|
|
365
|
+
json: commandOptions.json,
|
|
366
|
+
keepBranch: commandOptions.keepBranch,
|
|
367
|
+
stderr: options.stderr,
|
|
368
|
+
stdout: options.stdout,
|
|
369
|
+
});
|
|
370
|
+
if (exitCode !== 0)
|
|
371
|
+
throw commanderExit(exitCode);
|
|
372
|
+
});
|
|
373
|
+
program.commands
|
|
374
|
+
.find((command) => command.name() === "undo")
|
|
375
|
+
?.action(async (id, commandOptions) => {
|
|
376
|
+
const exitCode = await runUndoCommand({
|
|
377
|
+
cwd: options.cwd,
|
|
378
|
+
id,
|
|
379
|
+
json: commandOptions.json,
|
|
380
|
+
list: commandOptions.list,
|
|
381
|
+
stderr: options.stderr,
|
|
382
|
+
stdout: options.stdout,
|
|
383
|
+
});
|
|
384
|
+
if (exitCode !== 0)
|
|
385
|
+
throw commanderExit(exitCode);
|
|
386
|
+
});
|
|
291
387
|
program.commands
|
|
292
388
|
.find((command) => command.name() === "init")
|
|
293
389
|
?.action(async (shell, commandOptions) => {
|
|
@@ -415,7 +511,10 @@ function attachCommandActions(program, options) {
|
|
|
415
511
|
const exitCode = await runGoCommand({
|
|
416
512
|
branch,
|
|
417
513
|
cwd: options.cwd,
|
|
514
|
+
json: commandOptions.json,
|
|
418
515
|
print: commandOptions.print,
|
|
516
|
+
quiet: commandOptions.quiet,
|
|
517
|
+
root: commandOptions.root,
|
|
419
518
|
stderr: options.stderr,
|
|
420
519
|
stdout: options.stdout,
|
|
421
520
|
});
|
|
@@ -447,6 +546,20 @@ function attachCommandActions(program, options) {
|
|
|
447
546
|
throw commanderExit(exitCode);
|
|
448
547
|
}
|
|
449
548
|
});
|
|
549
|
+
program.commands
|
|
550
|
+
.find((command) => command.name() === "task")
|
|
551
|
+
?.action(async (task, commandOptions) => {
|
|
552
|
+
const exitCode = await runTaskCommand({
|
|
553
|
+
clear: commandOptions.clear,
|
|
554
|
+
cwd: options.cwd,
|
|
555
|
+
json: commandOptions.json,
|
|
556
|
+
stderr: options.stderr,
|
|
557
|
+
task,
|
|
558
|
+
stdout: options.stdout,
|
|
559
|
+
});
|
|
560
|
+
if (exitCode !== 0)
|
|
561
|
+
throw commanderExit(exitCode);
|
|
562
|
+
});
|
|
450
563
|
program.commands
|
|
451
564
|
.find((command) => command.name() === "sync")
|
|
452
565
|
?.action(async (commandOptions) => {
|
|
@@ -564,7 +677,12 @@ function attachCommandActions(program, options) {
|
|
|
564
677
|
};
|
|
565
678
|
program.commands
|
|
566
679
|
.find((command) => command.name() === "remove")
|
|
567
|
-
?.action(
|
|
680
|
+
?.action(async (branch, commandOptions) => {
|
|
681
|
+
if (!commandOptions.json) {
|
|
682
|
+
options.stderr("gji remove is deprecated; use 'gji done' for one worktree or 'gji clean' for bulk cleanup.\n");
|
|
683
|
+
}
|
|
684
|
+
return runRemovalCommand(branch, commandOptions);
|
|
685
|
+
});
|
|
568
686
|
program.commands
|
|
569
687
|
.find((command) => command.name() === "run-hook")
|
|
570
688
|
?.action(async (hook) => {
|
|
@@ -580,14 +698,13 @@ function attachCommandActions(program, options) {
|
|
|
580
698
|
program.commands
|
|
581
699
|
.find((command) => command.name() === "warp")
|
|
582
700
|
?.action(async (branch, commandOptions) => {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
701
|
+
if (!commandOptions.json) {
|
|
702
|
+
options.stderr("gji warp is deprecated; use 'gji go' (press Tab for all repositories).\n");
|
|
703
|
+
}
|
|
586
704
|
const exitCode = await runWarpCommand({
|
|
587
|
-
branch
|
|
705
|
+
branch,
|
|
588
706
|
cwd: options.cwd,
|
|
589
707
|
json: commandOptions.json,
|
|
590
|
-
newWorktree,
|
|
591
708
|
stderr: options.stderr,
|
|
592
709
|
stdout: options.stdout,
|
|
593
710
|
});
|
|
@@ -625,6 +742,7 @@ function attachCommandActions(program, options) {
|
|
|
625
742
|
action: "set",
|
|
626
743
|
cwd: options.cwd,
|
|
627
744
|
key,
|
|
745
|
+
stderr: options.stderr,
|
|
628
746
|
stdout: options.stdout,
|
|
629
747
|
value,
|
|
630
748
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CloneFailureStore {
|
|
2
|
+
isCached(repoRoot: string, directory: string, scope?: string): Promise<boolean>;
|
|
3
|
+
cache(repoRoot: string, directory: string, reason: string, scope?: string): Promise<void>;
|
|
4
|
+
clear(repoRoot: string, directory: string, scope?: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare class FileCloneFailureStore implements CloneFailureStore {
|
|
7
|
+
private updateQueue;
|
|
8
|
+
isCached(repoRoot: string, directory: string, scope?: string): Promise<boolean>;
|
|
9
|
+
cache(repoRoot: string, directory: string, reason: string, scope?: string): Promise<void>;
|
|
10
|
+
clear(repoRoot: string, directory: string, scope?: string): Promise<void>;
|
|
11
|
+
private update;
|
|
12
|
+
private withStateLock;
|
|
13
|
+
private readState;
|
|
14
|
+
private writeState;
|
|
15
|
+
private stateFilePath;
|
|
16
|
+
}
|
|
17
|
+
export declare const defaultCloneFailureStore: CloneFailureStore;
|
|
18
|
+
export declare function cloneFailureScope(source: string, destination: string): Promise<string>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, rename, rm, rmdir, stat, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
|
|
6
|
+
const STATE_FILE_NAME = "state.json";
|
|
7
|
+
const STATE_LOCK_SUFFIX = ".lock";
|
|
8
|
+
const CLONE_FAILURE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const STATE_LOCK_TTL_MS = 30 * 1000;
|
|
10
|
+
export class FileCloneFailureStore {
|
|
11
|
+
updateQueue = Promise.resolve();
|
|
12
|
+
async isCached(repoRoot, directory, scope) {
|
|
13
|
+
const state = await this.readState();
|
|
14
|
+
const repoState = state.syncDirs?.[repoRoot];
|
|
15
|
+
const key = failureKey(directory, scope);
|
|
16
|
+
if (!repoState || !Object.hasOwn(repoState, key))
|
|
17
|
+
return false;
|
|
18
|
+
const failure = repoState[key];
|
|
19
|
+
return (isPlainObject(failure) &&
|
|
20
|
+
typeof failure.failedAt === "number" &&
|
|
21
|
+
Date.now() - failure.failedAt < CLONE_FAILURE_TTL_MS);
|
|
22
|
+
}
|
|
23
|
+
async cache(repoRoot, directory, reason, scope) {
|
|
24
|
+
await this.update(async () => {
|
|
25
|
+
const state = await this.readState();
|
|
26
|
+
const syncDirs = state.syncDirs ?? {};
|
|
27
|
+
const repoState = syncDirs[repoRoot] ?? {};
|
|
28
|
+
const key = failureKey(directory, scope);
|
|
29
|
+
syncDirs[repoRoot] = {
|
|
30
|
+
...repoState,
|
|
31
|
+
[key]: { failedAt: Date.now(), reason },
|
|
32
|
+
};
|
|
33
|
+
await this.writeState({ ...state, syncDirs });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async clear(repoRoot, directory, scope) {
|
|
37
|
+
await this.update(async () => {
|
|
38
|
+
const state = await this.readState();
|
|
39
|
+
const repoState = state.syncDirs?.[repoRoot];
|
|
40
|
+
const key = failureKey(directory, scope);
|
|
41
|
+
if (!repoState || !Object.hasOwn(repoState, key))
|
|
42
|
+
return;
|
|
43
|
+
const nextRepoState = { ...repoState };
|
|
44
|
+
delete nextRepoState[key];
|
|
45
|
+
const syncDirs = { ...state.syncDirs };
|
|
46
|
+
if (Object.keys(nextRepoState).length === 0)
|
|
47
|
+
delete syncDirs[repoRoot];
|
|
48
|
+
else
|
|
49
|
+
syncDirs[repoRoot] = nextRepoState;
|
|
50
|
+
await this.writeState({ ...state, syncDirs });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async update(operation) {
|
|
54
|
+
const next = this.updateQueue.then(() => this.withStateLock(operation), () => this.withStateLock(operation));
|
|
55
|
+
this.updateQueue = next.then(() => undefined, () => undefined);
|
|
56
|
+
await next;
|
|
57
|
+
}
|
|
58
|
+
async withStateLock(operation) {
|
|
59
|
+
const lockPath = `${this.stateFilePath()}${STATE_LOCK_SUFFIX}`;
|
|
60
|
+
const lockToken = await acquireStateLock(lockPath);
|
|
61
|
+
if (lockToken === undefined)
|
|
62
|
+
return;
|
|
63
|
+
const stopHeartbeat = startStateLockHeartbeat(lockPath, lockToken);
|
|
64
|
+
try {
|
|
65
|
+
await operation();
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
stopHeartbeat();
|
|
69
|
+
await releaseStateLock(lockPath, lockToken);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async readState() {
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readFile(this.stateFilePath(), "utf8");
|
|
75
|
+
const parsed = JSON.parse(raw);
|
|
76
|
+
if (!isPlainObject(parsed))
|
|
77
|
+
return {};
|
|
78
|
+
return isPlainObject(parsed.syncDirs)
|
|
79
|
+
? parsed
|
|
80
|
+
: { ...parsed, syncDirs: {} };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async writeState(state) {
|
|
87
|
+
try {
|
|
88
|
+
const path = this.stateFilePath();
|
|
89
|
+
const directory = dirname(path);
|
|
90
|
+
await mkdir(directory, { recursive: true });
|
|
91
|
+
const temporaryDirectory = await mkdtemp(join(directory, `.gji-state-${randomUUID()}-`));
|
|
92
|
+
const temporaryPath = join(temporaryDirectory, STATE_FILE_NAME);
|
|
93
|
+
try {
|
|
94
|
+
await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
95
|
+
await rename(temporaryPath, path);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// The cache is advisory and must never block worktree creation.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
stateFilePath(home = homedir()) {
|
|
106
|
+
const configuredDirectory = process.env.GJI_CONFIG_DIR;
|
|
107
|
+
const directory = configuredDirectory
|
|
108
|
+
? resolve(configuredDirectory)
|
|
109
|
+
: join(home, GLOBAL_CONFIG_DIRECTORY);
|
|
110
|
+
return join(directory, STATE_FILE_NAME);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function acquireStateLock(lockPath) {
|
|
114
|
+
const lockToken = randomUUID();
|
|
115
|
+
await mkdir(dirname(lockPath), { recursive: true }).catch(() => undefined);
|
|
116
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(lockPath);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!isErrorCode(error, "EEXIST"))
|
|
122
|
+
return undefined;
|
|
123
|
+
try {
|
|
124
|
+
const freshnessPath = await lockFreshnessPath(lockPath);
|
|
125
|
+
const lockStats = await stat(freshnessPath);
|
|
126
|
+
if (Date.now() - lockStats.mtimeMs >= STATE_LOCK_TTL_MS) {
|
|
127
|
+
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
128
|
+
try {
|
|
129
|
+
await rename(lockPath, stalePath);
|
|
130
|
+
}
|
|
131
|
+
catch (renameError) {
|
|
132
|
+
if (isErrorCode(renameError, "ENOENT"))
|
|
133
|
+
continue;
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
await rm(stalePath, { force: true, recursive: true });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (lockError) {
|
|
141
|
+
if (!isErrorCode(lockError, "ENOENT"))
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await writeFile(join(lockPath, ownerFileName(lockToken)), `${lockToken}\n`, {
|
|
149
|
+
flag: "wx",
|
|
150
|
+
});
|
|
151
|
+
return lockToken;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
await rm(lockPath, { force: true, recursive: true }).catch(() => undefined);
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
async function lockFreshnessPath(lockPath) {
|
|
161
|
+
try {
|
|
162
|
+
const owner = (await readdir(lockPath)).find((entry) => entry.startsWith("owner-"));
|
|
163
|
+
return owner ? join(lockPath, owner) : lockPath;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (isErrorCode(error, "ENOENT"))
|
|
167
|
+
throw error;
|
|
168
|
+
return lockPath;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function startStateLockHeartbeat(lockPath, lockToken) {
|
|
172
|
+
const timer = setInterval(() => {
|
|
173
|
+
void refreshStateLock(lockPath, lockToken);
|
|
174
|
+
}, STATE_LOCK_TTL_MS / 3);
|
|
175
|
+
timer.unref?.();
|
|
176
|
+
return () => clearInterval(timer);
|
|
177
|
+
}
|
|
178
|
+
async function refreshStateLock(lockPath, lockToken) {
|
|
179
|
+
try {
|
|
180
|
+
const ownerPath = join(lockPath, ownerFileName(lockToken));
|
|
181
|
+
await readFile(ownerPath, "utf8");
|
|
182
|
+
const now = new Date();
|
|
183
|
+
await utimes(ownerPath, now, now);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// The cache is advisory; a failed heartbeat must not block worktree creation.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function releaseStateLock(lockPath, lockToken) {
|
|
190
|
+
try {
|
|
191
|
+
const ownerPath = join(lockPath, ownerFileName(lockToken));
|
|
192
|
+
await unlink(ownerPath);
|
|
193
|
+
await rmdir(lockPath);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (!isErrorCode(error, "ENOENT") && !isErrorCode(error, "ENOTEMPTY")) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function ownerFileName(lockToken) {
|
|
202
|
+
return `owner-${lockToken}`;
|
|
203
|
+
}
|
|
204
|
+
export const defaultCloneFailureStore = new FileCloneFailureStore();
|
|
205
|
+
export async function cloneFailureScope(source, destination) {
|
|
206
|
+
const sourcePath = resolve(source);
|
|
207
|
+
const destinationParent = resolve(dirname(destination));
|
|
208
|
+
const [sourceDevice, destinationDevice] = await Promise.all([
|
|
209
|
+
readDevice(sourcePath),
|
|
210
|
+
readDevice(destinationParent),
|
|
211
|
+
]);
|
|
212
|
+
return JSON.stringify([sourcePath, sourceDevice, destinationDevice]);
|
|
213
|
+
}
|
|
214
|
+
async function readDevice(path) {
|
|
215
|
+
try {
|
|
216
|
+
return (await stat(path)).dev;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function failureKey(directory, scope) {
|
|
223
|
+
return scope === undefined ? directory : JSON.stringify([scope, directory]);
|
|
224
|
+
}
|
|
225
|
+
function isPlainObject(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function isErrorCode(error, code) {
|
|
229
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
230
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type CommandRunner = (command: string, cwd: string, stderr: (chunk: string) => void, stdout?: (chunk: string) => void, options?: CommandRunnerOptions) => Promise<void>;
|
|
2
|
+
export interface CommandRunnerOptions {
|
|
3
|
+
env?: NodeJS.ProcessEnv;
|
|
4
|
+
shell?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const runCommand: CommandRunner;
|