@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,166 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { isAlreadyExistsError, isNotDirectoryError, isNotFoundError, } from "./fs-utils.js";
|
|
5
|
+
export async function openDestinationDirectory(root, path) {
|
|
6
|
+
const segments = destinationSegments(root, path);
|
|
7
|
+
if (process.platform !== "linux") {
|
|
8
|
+
await ensureDestinationDirectory(root, path);
|
|
9
|
+
return { path: resolve(path), close: async () => undefined };
|
|
10
|
+
}
|
|
11
|
+
let handle = await open(root, destinationDirectoryFlags());
|
|
12
|
+
try {
|
|
13
|
+
for (const segment of segments) {
|
|
14
|
+
const childPath = join(fileDescriptorPath(handle.fd), segment);
|
|
15
|
+
try {
|
|
16
|
+
await mkdir(childPath);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!isAlreadyExistsError(error))
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const childHandle = await open(childPath, destinationDirectoryFlags());
|
|
23
|
+
await handle.close();
|
|
24
|
+
handle = childHandle;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
path: fileDescriptorPath(handle.fd),
|
|
28
|
+
close: async () => handle.close(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await handle.close().catch(() => undefined);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function inspectDestination(root, path) {
|
|
37
|
+
const resolvedPath = resolve(path);
|
|
38
|
+
const resolvedRoot = resolve(root);
|
|
39
|
+
const distance = relative(resolvedRoot, resolvedPath);
|
|
40
|
+
if (isAbsolute(distance) ||
|
|
41
|
+
distance === ".." ||
|
|
42
|
+
distance.startsWith(`..${sep}`)) {
|
|
43
|
+
return { kind: "unsafe", reason: "destination escapes the worktree" };
|
|
44
|
+
}
|
|
45
|
+
let current = resolvedRoot;
|
|
46
|
+
try {
|
|
47
|
+
const rootStats = await lstat(current);
|
|
48
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
49
|
+
return { kind: "unsafe", reason: "worktree root is not a directory" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isNotFoundError(error))
|
|
54
|
+
return { kind: "missing" };
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const segments = distance.split(sep).filter(Boolean);
|
|
58
|
+
for (const [index, segment] of segments.entries()) {
|
|
59
|
+
current = resolve(current, segment);
|
|
60
|
+
try {
|
|
61
|
+
const stats = await lstat(current);
|
|
62
|
+
if (stats.isSymbolicLink()) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "unsafe",
|
|
65
|
+
reason: `destination has a symbolic-link component: ${current}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (index < segments.length - 1 && !stats.isDirectory()) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "unsafe",
|
|
71
|
+
reason: `destination has a non-directory ancestor: ${current}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (index === segments.length - 1) {
|
|
75
|
+
return stats.isDirectory()
|
|
76
|
+
? { kind: "exists" }
|
|
77
|
+
: {
|
|
78
|
+
kind: "unsafe",
|
|
79
|
+
reason: `destination is not a directory: ${current}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (isNotFoundError(error))
|
|
85
|
+
return { kind: "missing" };
|
|
86
|
+
if (isNotDirectoryError(error)) {
|
|
87
|
+
return {
|
|
88
|
+
kind: "unsafe",
|
|
89
|
+
reason: `destination has a non-directory ancestor: ${current}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const stats = await lstat(current);
|
|
97
|
+
return stats.isDirectory()
|
|
98
|
+
? { kind: "exists" }
|
|
99
|
+
: { kind: "unsafe", reason: "destination is not a directory" };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isNotFoundError(error))
|
|
103
|
+
return { kind: "missing" };
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function destinationSegments(root, path) {
|
|
108
|
+
const resolvedRoot = resolve(root);
|
|
109
|
+
const resolvedPath = resolve(path);
|
|
110
|
+
const distance = relative(resolvedRoot, resolvedPath);
|
|
111
|
+
if (isAbsolute(distance) ||
|
|
112
|
+
distance === ".." ||
|
|
113
|
+
distance.startsWith(`..${sep}`)) {
|
|
114
|
+
throw new Error("destination escapes the worktree");
|
|
115
|
+
}
|
|
116
|
+
return distance.split(sep).filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
function destinationDirectoryFlags() {
|
|
119
|
+
return constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
|
|
120
|
+
}
|
|
121
|
+
function fileDescriptorPath(fd) {
|
|
122
|
+
if (process.platform === "linux")
|
|
123
|
+
return `/proc/self/fd/${fd}`;
|
|
124
|
+
if (process.platform === "darwin")
|
|
125
|
+
return `/dev/fd/${fd}`;
|
|
126
|
+
throw new Error(`safe destination handles are unsupported on ${process.platform}`);
|
|
127
|
+
}
|
|
128
|
+
export async function ensureDestinationDirectory(root, path) {
|
|
129
|
+
const resolvedRoot = resolve(root);
|
|
130
|
+
const resolvedPath = resolve(path);
|
|
131
|
+
const distance = relative(resolvedRoot, resolvedPath);
|
|
132
|
+
if (isAbsolute(distance) ||
|
|
133
|
+
distance === ".." ||
|
|
134
|
+
distance.startsWith(`..${sep}`)) {
|
|
135
|
+
throw new Error("destination escapes the worktree");
|
|
136
|
+
}
|
|
137
|
+
let current = resolvedRoot;
|
|
138
|
+
const rootStats = await lstat(current);
|
|
139
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
140
|
+
throw new Error("worktree root is not a directory");
|
|
141
|
+
}
|
|
142
|
+
for (const segment of distance.split(sep).filter(Boolean)) {
|
|
143
|
+
current = resolve(current, segment);
|
|
144
|
+
try {
|
|
145
|
+
const stats = await lstat(current);
|
|
146
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
147
|
+
throw new Error(`destination has an unsafe component: ${current}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!isNotFoundError(error))
|
|
152
|
+
throw error;
|
|
153
|
+
try {
|
|
154
|
+
await mkdir(current);
|
|
155
|
+
}
|
|
156
|
+
catch (mkdirError) {
|
|
157
|
+
if (!isAlreadyExistsError(mkdirError))
|
|
158
|
+
throw mkdirError;
|
|
159
|
+
}
|
|
160
|
+
const stats = await lstat(current);
|
|
161
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
162
|
+
throw new Error(`destination has an unsafe component: ${current}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/shell-completion.js
CHANGED
|
@@ -2,7 +2,15 @@ import { KNOWN_GLOBAL_CONFIG_KEYS } from "./config.js";
|
|
|
2
2
|
const TOP_LEVEL_COMMANDS = [
|
|
3
3
|
{
|
|
4
4
|
name: "new",
|
|
5
|
-
description: "create a new branch or detached linked worktree",
|
|
5
|
+
description: "create a new branch or detached linked worktree and CoW-bootstrap configured directories",
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: "done",
|
|
9
|
+
description: "finish the current linked worktree and return safely",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "undo",
|
|
13
|
+
description: "restore the most recent destructive worktree operation",
|
|
6
14
|
},
|
|
7
15
|
{
|
|
8
16
|
name: "init",
|
|
@@ -17,10 +25,11 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
17
25
|
{ name: "back", description: "navigate to the previously visited worktree" },
|
|
18
26
|
{ name: "history", description: "show navigation history" },
|
|
19
27
|
{ name: "open", description: "open the worktree in an editor" },
|
|
20
|
-
{ name: "go", description: "
|
|
28
|
+
{ name: "go", description: "resolve and jump to a worktree path" },
|
|
21
29
|
{ name: "jump", description: "alias of go" },
|
|
22
30
|
{ name: "root", description: "print the main repository root path" },
|
|
23
31
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
32
|
+
{ name: "task", description: "show or update the current worktree task" },
|
|
24
33
|
{ name: "sync", description: "fetch and update one or all worktrees" },
|
|
25
34
|
{
|
|
26
35
|
name: "sync-files",
|
|
@@ -30,14 +39,17 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
30
39
|
{ name: "clean", description: "interactively prune linked worktrees" },
|
|
31
40
|
{
|
|
32
41
|
name: "remove",
|
|
33
|
-
description: "
|
|
42
|
+
description: "deprecated: use done for one worktree or clean for bulk cleanup",
|
|
34
43
|
},
|
|
35
|
-
{ name: "rm", description: "alias of remove" },
|
|
44
|
+
{ name: "rm", description: "deprecated alias of remove" },
|
|
36
45
|
{
|
|
37
46
|
name: "run-hook",
|
|
38
47
|
description: "run a named hook in the current worktree",
|
|
39
48
|
},
|
|
40
|
-
{
|
|
49
|
+
{
|
|
50
|
+
name: "warp",
|
|
51
|
+
description: "deprecated: use go (press Tab for all known repos)",
|
|
52
|
+
},
|
|
41
53
|
{ name: "config", description: "manage global config defaults" },
|
|
42
54
|
];
|
|
43
55
|
const SHELL_NAMES = ["bash", "fish", "zsh"];
|
|
@@ -129,7 +141,7 @@ _gji_completion() {
|
|
|
129
141
|
cur="\${COMP_WORDS[COMP_CWORD]:-}"
|
|
130
142
|
|
|
131
143
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
132
|
-
|
|
144
|
+
COMPREPLY=( $(compgen -W "${topLevelCommands} --json --help" -- "$cur") )
|
|
133
145
|
return 0
|
|
134
146
|
fi
|
|
135
147
|
|
|
@@ -137,7 +149,13 @@ _gji_completion() {
|
|
|
137
149
|
|
|
138
150
|
case "$command_name" in
|
|
139
151
|
new)
|
|
140
|
-
|
|
152
|
+
COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
153
|
+
;;
|
|
154
|
+
done)
|
|
155
|
+
COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
|
|
156
|
+
;;
|
|
157
|
+
undo)
|
|
158
|
+
COMPREPLY=( $(compgen -W "--list --json --help" -- "$cur") )
|
|
141
159
|
;;
|
|
142
160
|
init)
|
|
143
161
|
COMPREPLY=( $(compgen -W "${shells} --write --json --help" -- "$cur") )
|
|
@@ -182,7 +200,7 @@ _gji_completion() {
|
|
|
182
200
|
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --select --save --workspace --help" -- "$cur") )
|
|
183
201
|
;;
|
|
184
202
|
go|jump)
|
|
185
|
-
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
|
|
203
|
+
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --root --print --json --help" -- "$cur") )
|
|
186
204
|
;;
|
|
187
205
|
root)
|
|
188
206
|
COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
|
|
@@ -212,7 +230,7 @@ _gji_completion() {
|
|
|
212
230
|
COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
|
|
213
231
|
;;
|
|
214
232
|
warp)
|
|
215
|
-
COMPREPLY=( $(compgen -W "
|
|
233
|
+
COMPREPLY=( $(compgen -W "--print --json --help" -- "$cur") )
|
|
216
234
|
;;
|
|
217
235
|
config)
|
|
218
236
|
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
@@ -361,10 +379,18 @@ function __gji_should_complete_config_key
|
|
|
361
379
|
end
|
|
362
380
|
|
|
363
381
|
complete -c gji -f
|
|
382
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l force -d 'remove dirty or unmerged worktrees without prompting'
|
|
383
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l keep-branch -d 'remove the worktree but preserve its branch'
|
|
384
|
+
complete -c gji -n '__fish_seen_subcommand_from done' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
385
|
+
complete -c gji -n '__fish_seen_subcommand_from undo' -l list -d 'list recoverable operations'
|
|
386
|
+
complete -c gji -n '__fish_seen_subcommand_from undo' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
364
387
|
${commandLines}
|
|
365
388
|
|
|
366
389
|
complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
|
|
367
390
|
complete -c gji -n '__fish_seen_subcommand_from new' -l from-current -d 'base the new branch on the current worktree instead of the main worktree'
|
|
391
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l take -d 'move current uncommitted changes into the new worktree'
|
|
392
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l copy -d 'copy current uncommitted changes instead of moving them (requires --take)'
|
|
393
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l task -r -d 'record the purpose of the new worktree'
|
|
368
394
|
complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
|
|
369
395
|
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
370
396
|
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
|
|
@@ -400,11 +426,16 @@ complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate
|
|
|
400
426
|
complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
401
427
|
|
|
402
428
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
|
|
429
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l root -d 'navigate to the main repository root'
|
|
430
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l json -d 'emit JSON for an existing worktree destination'
|
|
431
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l quiet -d 'suppress the context card'
|
|
403
432
|
complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
404
433
|
|
|
405
434
|
complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
|
|
406
435
|
|
|
407
436
|
complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print repository and worktree health as JSON'
|
|
437
|
+
complete -c gji -n '__fish_seen_subcommand_from task' -l clear -d 'clear the current worktree task'
|
|
438
|
+
complete -c gji -n '__fish_seen_subcommand_from task' -l json -d 'emit task metadata as JSON'
|
|
408
439
|
|
|
409
440
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
|
|
410
441
|
complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
@@ -430,7 +461,8 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
|
|
|
430
461
|
|
|
431
462
|
${hookLines}
|
|
432
463
|
|
|
433
|
-
complete -c gji -n '
|
|
464
|
+
complete -c gji -n '__fish_use_subcommand' -l json -d 'emit the repository hub as JSON'
|
|
465
|
+
|
|
434
466
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
|
|
435
467
|
complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
436
468
|
|
|
@@ -509,6 +541,7 @@ local -a command_entries worktree_branches
|
|
|
509
541
|
|
|
510
542
|
command_entries=(
|
|
511
543
|
${commandLines}
|
|
544
|
+
'--json:emit the repository hub as JSON'
|
|
512
545
|
)
|
|
513
546
|
|
|
514
547
|
if (( CURRENT == 2 )); then
|
|
@@ -518,7 +551,13 @@ fi
|
|
|
518
551
|
|
|
519
552
|
case "\${words[2]}" in
|
|
520
553
|
new)
|
|
521
|
-
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
554
|
+
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
555
|
+
;;
|
|
556
|
+
done)
|
|
557
|
+
_arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
558
|
+
;;
|
|
559
|
+
undo)
|
|
560
|
+
_arguments '--list[list recoverable operations]' '--json[emit JSON on success or error instead of human-readable output]' '2:id: '
|
|
522
561
|
;;
|
|
523
562
|
init)
|
|
524
563
|
_arguments '--write[write the integration to the shell config file]' '--json[emit a JSON error in non-interactive mode]' '2:shell:(${shells})'
|
|
@@ -552,7 +591,7 @@ case "\${words[2]}" in
|
|
|
552
591
|
_arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--select[choose a worktree with the interactive selector]' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
|
|
553
592
|
;;
|
|
554
593
|
go|jump)
|
|
555
|
-
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
594
|
+
_arguments '--root[navigate to the main repository root]' '--print[print the resolved worktree path explicitly]' '--json[emit JSON for an existing worktree destination]' '2:branch:->worktrees'
|
|
556
595
|
;;
|
|
557
596
|
root)
|
|
558
597
|
_arguments '--print[print the resolved repository root path explicitly]'
|
|
@@ -579,7 +618,7 @@ case "\${words[2]}" in
|
|
|
579
618
|
_arguments "2:hook:(${hooks})"
|
|
580
619
|
;;
|
|
581
620
|
warp)
|
|
582
|
-
_arguments '
|
|
621
|
+
_arguments '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
583
622
|
;;
|
|
584
623
|
config)
|
|
585
624
|
if (( CURRENT == 3 )); then
|
package/dist/slots.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SlotStore {
|
|
2
|
+
[worktreePath: string]: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function SLOTS_FILE_PATH(home?: string): string;
|
|
5
|
+
export declare function loadSlots(home?: string): Promise<SlotStore>;
|
|
6
|
+
export declare function assignWorktreeSlot(repoRoot: string, worktreePath: string, home?: string): Promise<number>;
|
|
7
|
+
export declare function getWorktreeSlot(worktreePath: string, home?: string): Promise<number | null>;
|
|
8
|
+
export declare function releaseWorktreeSlot(worktreePath: string, home?: string): Promise<void>;
|
package/dist/slots.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
|
|
5
|
+
export function SLOTS_FILE_PATH(home = homedir()) {
|
|
6
|
+
const configDir = process.env.GJI_CONFIG_DIR;
|
|
7
|
+
return configDir
|
|
8
|
+
? join(resolve(configDir), "slots.json")
|
|
9
|
+
: join(home, GLOBAL_CONFIG_DIRECTORY, "slots.json");
|
|
10
|
+
}
|
|
11
|
+
export async function loadSlots(home = homedir()) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(SLOTS_FILE_PATH(home), "utf8"));
|
|
14
|
+
if (!isSlotStore(parsed))
|
|
15
|
+
return {};
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function assignWorktreeSlot(repoRoot, worktreePath, home = homedir()) {
|
|
23
|
+
const slots = await loadSlots(home);
|
|
24
|
+
const existing = slots[worktreePath];
|
|
25
|
+
if (existing !== undefined)
|
|
26
|
+
return existing;
|
|
27
|
+
if (worktreePath !== repoRoot && slots[repoRoot] === undefined)
|
|
28
|
+
slots[repoRoot] = 0;
|
|
29
|
+
const used = new Set(Object.values(slots));
|
|
30
|
+
const slot = worktreePath === repoRoot ? 0 : nextAvailableSlot(used);
|
|
31
|
+
slots[worktreePath] = slot;
|
|
32
|
+
await saveSlots(slots, home);
|
|
33
|
+
return slot;
|
|
34
|
+
}
|
|
35
|
+
export async function getWorktreeSlot(worktreePath, home = homedir()) {
|
|
36
|
+
const slot = (await loadSlots(home))[worktreePath];
|
|
37
|
+
return slot === undefined ? null : slot;
|
|
38
|
+
}
|
|
39
|
+
export async function releaseWorktreeSlot(worktreePath, home = homedir()) {
|
|
40
|
+
const slots = await loadSlots(home);
|
|
41
|
+
if (!(worktreePath in slots))
|
|
42
|
+
return;
|
|
43
|
+
delete slots[worktreePath];
|
|
44
|
+
await saveSlots(slots, home);
|
|
45
|
+
}
|
|
46
|
+
async function saveSlots(slots, home) {
|
|
47
|
+
const path = SLOTS_FILE_PATH(home);
|
|
48
|
+
await mkdir(dirname(path), { recursive: true });
|
|
49
|
+
await writeFile(path, `${JSON.stringify(slots, null, 2)}\n`, "utf8");
|
|
50
|
+
}
|
|
51
|
+
function nextAvailableSlot(used) {
|
|
52
|
+
let slot = 1;
|
|
53
|
+
while (used.has(slot))
|
|
54
|
+
slot += 1;
|
|
55
|
+
return slot;
|
|
56
|
+
}
|
|
57
|
+
function isSlotStore(value) {
|
|
58
|
+
return (typeof value === "object" &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
Object.values(value).every((slot) => Number.isInteger(slot) && slot >= 0));
|
|
61
|
+
}
|
package/dist/status.d.ts
CHANGED
package/dist/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readWorktreeHealth } from "./git.js";
|
|
2
2
|
import { comparePaths } from "./paths.js";
|
|
3
3
|
import { detectRepository, listWorktrees } from "./repo.js";
|
|
4
|
+
import { getWorktreeSlot } from "./slots.js";
|
|
5
|
+
import { readTask } from "./task.js";
|
|
4
6
|
export async function runStatusCommand(options) {
|
|
5
7
|
const repository = await detectRepository(options.cwd);
|
|
6
8
|
const worktrees = sortWorktreesByPath(await listWorktrees(options.cwd));
|
|
@@ -36,12 +38,18 @@ export function formatStatusJson(repoRoot, currentRoot, rows) {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
async function buildStatusRow(worktree) {
|
|
39
|
-
const health = await
|
|
41
|
+
const [health, slot, task] = await Promise.all([
|
|
42
|
+
readWorktreeHealth(worktree.path),
|
|
43
|
+
getWorktreeSlot(worktree.path),
|
|
44
|
+
readTask(worktree.path),
|
|
45
|
+
]);
|
|
40
46
|
return {
|
|
41
47
|
branch: worktree.branch,
|
|
42
48
|
current: worktree.isCurrent,
|
|
43
49
|
path: worktree.path,
|
|
50
|
+
slot,
|
|
44
51
|
status: health.status,
|
|
52
|
+
task: task?.task ?? null,
|
|
45
53
|
upstream: buildUpstreamState(worktree.branch, health),
|
|
46
54
|
};
|
|
47
55
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CloneFailureStore } from "./clone-failure-store.js";
|
|
2
|
+
import type { CloneDirectory } from "./dir-clone.js";
|
|
3
|
+
import type { SyncDirectoryPlan } from "./sync-plan.js";
|
|
4
|
+
export interface ClonedDirectory {
|
|
5
|
+
bytes?: number;
|
|
6
|
+
dir: string;
|
|
7
|
+
ms: number;
|
|
8
|
+
}
|
|
9
|
+
export type SyncDirectoryOutcome = {
|
|
10
|
+
kind: "cloned";
|
|
11
|
+
directory: ClonedDirectory;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "skipped";
|
|
14
|
+
dir: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
export interface SyncDirectoryReporter {
|
|
18
|
+
readonly emitCachedFailureWarnings: boolean;
|
|
19
|
+
readonly measureCloneSize: boolean;
|
|
20
|
+
write(message: string): void;
|
|
21
|
+
cloned(directory: ClonedDirectory): void;
|
|
22
|
+
skipped?(directory: {
|
|
23
|
+
dir: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}): void;
|
|
26
|
+
}
|
|
27
|
+
export interface SyncDirectoryExecutionOptions {
|
|
28
|
+
cloneDirectory: CloneDirectory;
|
|
29
|
+
failureStore?: CloneFailureStore;
|
|
30
|
+
repoRoot: string;
|
|
31
|
+
reporter: SyncDirectoryReporter;
|
|
32
|
+
}
|
|
33
|
+
export declare function executeSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[], options: SyncDirectoryExecutionOptions): Promise<SyncDirectoryOutcome[]>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cloneFailureScope, defaultCloneFailureStore, } from "./clone-failure-store.js";
|
|
2
|
+
import { isCloneDestinationExistsError, isCloneInProgressError, isCloneUnsupportedError, } from "./dir-clone.js";
|
|
3
|
+
import { inspectDestination } from "./safe-destination.js";
|
|
4
|
+
export async function executeSyncDirectoryPlan(plan, options) {
|
|
5
|
+
const failureStore = options.failureStore ?? defaultCloneFailureStore;
|
|
6
|
+
const outcomes = [];
|
|
7
|
+
for (const entry of plan) {
|
|
8
|
+
if (entry.destinationWarning) {
|
|
9
|
+
recordSkipped(outcomes, options.reporter, entry.directory, entry.destinationWarning);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const destinationState = await inspectDestination(entry.worktreePath, entry.destination);
|
|
13
|
+
if (destinationState.kind === "exists") {
|
|
14
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (destinationState.kind === "unsafe") {
|
|
18
|
+
const reason = destinationState.reason;
|
|
19
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (entry.warning) {
|
|
23
|
+
recordSkipped(outcomes, options.reporter, entry.directory, entry.warning);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.source) {
|
|
27
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "source does not exist");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const failureScope = await cloneFailureScope(entry.source, entry.destination);
|
|
31
|
+
if (await failureStore.isCached(options.repoRoot, entry.directory, failureScope)) {
|
|
32
|
+
if (options.reporter.emitCachedFailureWarnings ||
|
|
33
|
+
options.reporter.skipped) {
|
|
34
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "copy-on-write failure cached");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
options.reporter.write(`syncDirs: previous copy-on-write failure cached, skipped ${entry.directory}\n`);
|
|
38
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "copy-on-write failure cached", false);
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const refreshedDestinationState = await inspectDestination(entry.worktreePath, entry.destination);
|
|
43
|
+
if (refreshedDestinationState.kind !== "missing") {
|
|
44
|
+
recordSkipped(outcomes, options.reporter, entry.directory, refreshedDestinationState.kind === "unsafe"
|
|
45
|
+
? refreshedDestinationState.reason
|
|
46
|
+
: "destination already exists");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
const cloneOptions = {
|
|
52
|
+
destinationRoot: entry.worktreePath,
|
|
53
|
+
measureBytes: options.reporter.measureCloneSize,
|
|
54
|
+
};
|
|
55
|
+
result = await options.cloneDirectory(entry.source, entry.destination, cloneOptions);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (isCloneDestinationExistsError(error)) {
|
|
59
|
+
recordSkipped(outcomes, options.reporter, entry.directory, "destination already exists");
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (isCloneInProgressError(error)) {
|
|
63
|
+
const reason = "copy-on-write clone already in progress";
|
|
64
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const reason = toErrorMessage(error);
|
|
68
|
+
if (isCloneUnsupportedError(error)) {
|
|
69
|
+
await failureStore.cache(options.repoRoot, entry.directory, reason, failureScope);
|
|
70
|
+
}
|
|
71
|
+
recordSkipped(outcomes, options.reporter, entry.directory, reason);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
await failureStore.clear(options.repoRoot, entry.directory, failureScope);
|
|
75
|
+
const clonedDirectory = {
|
|
76
|
+
bytes: result.bytes,
|
|
77
|
+
dir: entry.directory,
|
|
78
|
+
ms: result.ms,
|
|
79
|
+
};
|
|
80
|
+
const outcome = { kind: "cloned", directory: clonedDirectory };
|
|
81
|
+
outcomes.push(outcome);
|
|
82
|
+
options.reporter.cloned(clonedDirectory);
|
|
83
|
+
}
|
|
84
|
+
return outcomes;
|
|
85
|
+
}
|
|
86
|
+
function recordSkipped(outcomes, reporter, dir, reason, notify = true) {
|
|
87
|
+
outcomes.push({ kind: "skipped", dir, reason });
|
|
88
|
+
if (notify) {
|
|
89
|
+
if (reporter.skipped)
|
|
90
|
+
reporter.skipped({ dir, reason });
|
|
91
|
+
else
|
|
92
|
+
reporter.write(`syncDirs: ${reason}, skipped ${dir}\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function toErrorMessage(error) {
|
|
96
|
+
return error instanceof Error ? error.message : String(error);
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SyncDirectoryPlan {
|
|
2
|
+
directory: string;
|
|
3
|
+
destination: string;
|
|
4
|
+
worktreePath: string;
|
|
5
|
+
destinationWasPresent: boolean;
|
|
6
|
+
destinationWarning?: string;
|
|
7
|
+
source?: string;
|
|
8
|
+
warning?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SyncDirectoryEstimate {
|
|
11
|
+
bytes: number;
|
|
12
|
+
dir: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function prepareSyncDirectoryPlan(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryPlan[]>;
|
|
15
|
+
export declare function estimateSyncDirectoryPlan(plan: readonly SyncDirectoryPlan[]): Promise<SyncDirectoryEstimate[]>;
|
|
16
|
+
export declare function estimateSyncDirectories(repoRoot: string, worktreePath: string, directories: readonly string[]): Promise<SyncDirectoryEstimate[]>;
|