@solaqua/gji 0.6.1 → 0.7.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 +26 -1
- package/dist/back.d.ts +1 -1
- package/dist/back.js +23 -17
- package/dist/clean.d.ts +1 -1
- package/dist/clean.js +44 -35
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +264 -164
- package/dist/completion.js +3 -3
- package/dist/config-command.js +5 -5
- package/dist/config.js +41 -35
- package/dist/conflict.d.ts +1 -1
- package/dist/conflict.js +14 -6
- package/dist/editor.js +29 -9
- package/dist/file-sync.d.ts +1 -0
- package/dist/file-sync.js +15 -11
- package/dist/git.d.ts +1 -1
- package/dist/git.js +21 -19
- package/dist/gji-bundle.mjs +1709 -850
- package/dist/go.d.ts +2 -2
- package/dist/go.js +39 -26
- package/dist/headless.js +1 -1
- package/dist/history-command.js +3 -3
- package/dist/history.js +12 -12
- package/dist/hooks.js +16 -16
- package/dist/index.js +13 -9
- package/dist/init.d.ts +2 -2
- package/dist/init.js +106 -94
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +46 -28
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +96 -81
- package/dist/open.d.ts +2 -2
- package/dist/open.js +24 -21
- package/dist/package-manager.js +96 -45
- package/dist/pr.d.ts +2 -2
- package/dist/pr.js +47 -34
- package/dist/remove.d.ts +1 -1
- package/dist/remove.js +39 -27
- package/dist/repo-registry.js +45 -19
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +65 -37
- package/dist/shell-handoff.js +2 -2
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +4 -4
- package/dist/status.d.ts +5 -5
- package/dist/status.js +23 -23
- package/dist/sync-files-command.d.ts +10 -0
- package/dist/sync-files-command.js +137 -0
- package/dist/sync.js +23 -15
- package/dist/trigger-hook.js +9 -5
- package/dist/warp.js +66 -34
- package/dist/worktree-info.d.ts +9 -9
- package/dist/worktree-info.js +31 -29
- package/dist/worktree-management.d.ts +1 -1
- package/dist/worktree-management.js +26 -11
- package/dist/worktree-prompts.js +5 -5
- 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-go.1 +1 -1
- 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 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +23 -0
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-trigger-hook.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +5 -1
- package/package.json +8 -2
package/dist/warp.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { isCancel, select } from "@clack/prompts";
|
|
4
|
+
import { readWorktreeHealth } from "./git.js";
|
|
5
|
+
import { isHeadless } from "./headless.js";
|
|
6
|
+
import { appendHistory } from "./history.js";
|
|
7
|
+
import { runNewCommand } from "./new.js";
|
|
8
|
+
import { listWorktrees } from "./repo.js";
|
|
9
|
+
import { loadRegistry } from "./repo-registry.js";
|
|
10
|
+
import { writeShellOutput } from "./shell-handoff.js";
|
|
11
|
+
const WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
|
|
10
12
|
export async function runWarpCommand(options) {
|
|
11
13
|
if (options.newWorktree) {
|
|
12
14
|
const registry = await loadRegistry();
|
|
13
15
|
if (registry.length === 0) {
|
|
14
|
-
options.stderr(
|
|
15
|
-
|
|
16
|
+
options.stderr("gji warp: no repos registered yet.\n" +
|
|
17
|
+
"Use any gji command in a repository to register it automatically.\n");
|
|
16
18
|
return 1;
|
|
17
19
|
}
|
|
18
20
|
return runWarpNew(options, registry);
|
|
@@ -21,16 +23,20 @@ export async function runWarpCommand(options) {
|
|
|
21
23
|
}
|
|
22
24
|
async function runWarpNavigate(options) {
|
|
23
25
|
if ((isHeadless() || options.json) && !options.branch) {
|
|
24
|
-
const message =
|
|
26
|
+
const message = "branch argument is required";
|
|
25
27
|
if (options.json) {
|
|
26
28
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
27
29
|
}
|
|
28
30
|
else {
|
|
29
|
-
options.stderr(
|
|
31
|
+
options.stderr("gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
|
|
30
32
|
}
|
|
31
33
|
return 1;
|
|
32
34
|
}
|
|
33
|
-
const target = await resolveWarpTarget({
|
|
35
|
+
const target = await resolveWarpTarget({
|
|
36
|
+
...options,
|
|
37
|
+
commandName: "gji warp",
|
|
38
|
+
json: options.json,
|
|
39
|
+
});
|
|
34
40
|
if (!target)
|
|
35
41
|
return 1;
|
|
36
42
|
if (options.json) {
|
|
@@ -43,25 +49,26 @@ async function runWarpNavigate(options) {
|
|
|
43
49
|
return 0;
|
|
44
50
|
}
|
|
45
51
|
async function runWarpNew(options, registry) {
|
|
52
|
+
const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
|
|
46
53
|
let targetRepoRoot;
|
|
47
|
-
if (
|
|
48
|
-
targetRepoRoot =
|
|
54
|
+
if (deduplicatedRegistry.length === 1) {
|
|
55
|
+
targetRepoRoot = deduplicatedRegistry[0].path;
|
|
49
56
|
}
|
|
50
57
|
else {
|
|
51
58
|
if (isHeadless()) {
|
|
52
|
-
options.stderr(
|
|
59
|
+
options.stderr("gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
|
|
53
60
|
return 1;
|
|
54
61
|
}
|
|
55
62
|
const choice = await select({
|
|
56
|
-
message:
|
|
57
|
-
options:
|
|
63
|
+
message: "Create worktree in which repo?",
|
|
64
|
+
options: deduplicatedRegistry.map((entry) => ({
|
|
58
65
|
value: entry.path,
|
|
59
66
|
label: entry.name,
|
|
60
67
|
hint: entry.path,
|
|
61
68
|
})),
|
|
62
69
|
});
|
|
63
70
|
if (isCancel(choice)) {
|
|
64
|
-
options.stderr(
|
|
71
|
+
options.stderr("Aborted\n");
|
|
65
72
|
return 1;
|
|
66
73
|
}
|
|
67
74
|
targetRepoRoot = choice;
|
|
@@ -78,7 +85,7 @@ async function runWarpNew(options, registry) {
|
|
|
78
85
|
// runNewCommand writes the created path to options.stdout via writeShellOutput.
|
|
79
86
|
// Since GJI_NEW_OUTPUT_FILE is not set in the warp shell context, it falls
|
|
80
87
|
// through to our captured stdout, giving us the path to hand off.
|
|
81
|
-
let capturedPath =
|
|
88
|
+
let capturedPath = "";
|
|
82
89
|
const captureStdout = (chunk) => {
|
|
83
90
|
capturedPath = chunk.trim();
|
|
84
91
|
};
|
|
@@ -92,14 +99,39 @@ async function runWarpNew(options, registry) {
|
|
|
92
99
|
return exitCode;
|
|
93
100
|
}
|
|
94
101
|
if (!capturedPath) {
|
|
95
|
-
options.stderr(
|
|
102
|
+
options.stderr("gji warp: could not determine new worktree path\n");
|
|
96
103
|
return 1;
|
|
97
104
|
}
|
|
98
105
|
await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
|
|
99
106
|
return 0;
|
|
100
107
|
}
|
|
108
|
+
async function deduplicateRegistryForNew(registry) {
|
|
109
|
+
const deduplicated = [];
|
|
110
|
+
const seenPaths = new Set();
|
|
111
|
+
for (const entry of registry) {
|
|
112
|
+
const canonicalPath = await canonicalizeRepoPath(entry.path);
|
|
113
|
+
if (seenPaths.has(canonicalPath)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
seenPaths.add(canonicalPath);
|
|
117
|
+
deduplicated.push({
|
|
118
|
+
...entry,
|
|
119
|
+
name: basename(canonicalPath),
|
|
120
|
+
path: canonicalPath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return deduplicated;
|
|
124
|
+
}
|
|
125
|
+
async function canonicalizeRepoPath(repoPath) {
|
|
126
|
+
try {
|
|
127
|
+
return await realpath(repoPath);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return resolve(repoPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
101
133
|
function findByQuery(items, query) {
|
|
102
|
-
const slashIdx = query.indexOf(
|
|
134
|
+
const slashIdx = query.indexOf("/");
|
|
103
135
|
if (slashIdx !== -1) {
|
|
104
136
|
const repoQuery = query.slice(0, slashIdx);
|
|
105
137
|
const branchQuery = query.slice(slashIdx + 1);
|
|
@@ -110,7 +142,7 @@ function findByQuery(items, query) {
|
|
|
110
142
|
return items.find((item) => item.worktree.branch === query) ?? null;
|
|
111
143
|
}
|
|
112
144
|
export async function resolveWarpTarget(options) {
|
|
113
|
-
const cmd = options.commandName ??
|
|
145
|
+
const cmd = options.commandName ?? "gji";
|
|
114
146
|
const emitError = (message, hint) => {
|
|
115
147
|
if (options.json) {
|
|
116
148
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
@@ -123,7 +155,7 @@ export async function resolveWarpTarget(options) {
|
|
|
123
155
|
};
|
|
124
156
|
const registry = await loadRegistry();
|
|
125
157
|
if (registry.length === 0) {
|
|
126
|
-
emitError(
|
|
158
|
+
emitError("not in a git repository and no repos registered yet.", "Use any gji command inside a repository to register it.\n");
|
|
127
159
|
return null;
|
|
128
160
|
}
|
|
129
161
|
const results = await Promise.allSettled(registry.map(async (entry) => {
|
|
@@ -132,7 +164,7 @@ export async function resolveWarpTarget(options) {
|
|
|
132
164
|
}));
|
|
133
165
|
const allItems = [];
|
|
134
166
|
for (const result of results) {
|
|
135
|
-
if (result.status ===
|
|
167
|
+
if (result.status === "rejected")
|
|
136
168
|
continue;
|
|
137
169
|
const { repoName, worktrees } = result.value;
|
|
138
170
|
for (const worktree of worktrees) {
|
|
@@ -140,7 +172,7 @@ export async function resolveWarpTarget(options) {
|
|
|
140
172
|
}
|
|
141
173
|
}
|
|
142
174
|
if (allItems.length === 0) {
|
|
143
|
-
emitError(
|
|
175
|
+
emitError("no accessible worktrees found in any registered repo.");
|
|
144
176
|
return null;
|
|
145
177
|
}
|
|
146
178
|
if (options.branch) {
|
|
@@ -153,7 +185,7 @@ export async function resolveWarpTarget(options) {
|
|
|
153
185
|
}
|
|
154
186
|
const path = await promptForWarpTarget(allItems);
|
|
155
187
|
if (!path) {
|
|
156
|
-
options.stderr(
|
|
188
|
+
options.stderr("Aborted\n");
|
|
157
189
|
return null;
|
|
158
190
|
}
|
|
159
191
|
const chosen = allItems.find((item) => item.worktree.path === path);
|
|
@@ -162,11 +194,11 @@ export async function resolveWarpTarget(options) {
|
|
|
162
194
|
async function promptForWarpTarget(items) {
|
|
163
195
|
const healthResults = await Promise.allSettled(items.map((item) => readWorktreeHealth(item.worktree.path)));
|
|
164
196
|
const choice = await select({
|
|
165
|
-
message:
|
|
197
|
+
message: "Warp to a worktree",
|
|
166
198
|
options: items.map((item, i) => {
|
|
167
|
-
const health = healthResults[i].status ===
|
|
199
|
+
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
168
200
|
const upstream = health ? formatHint(item.worktree.branch, health) : null;
|
|
169
|
-
const label = `${item.repoName} › ${item.worktree.branch ??
|
|
201
|
+
const label = `${item.repoName} › ${item.worktree.branch ?? "(detached)"}`;
|
|
170
202
|
const pathHint = item.worktree.isCurrent
|
|
171
203
|
? `${item.worktree.path} (current)`
|
|
172
204
|
: item.worktree.path;
|
|
@@ -183,11 +215,11 @@ function formatHint(branch, health) {
|
|
|
183
215
|
if (branch === null)
|
|
184
216
|
return null;
|
|
185
217
|
if (!health.hasUpstream)
|
|
186
|
-
return
|
|
218
|
+
return "no upstream";
|
|
187
219
|
if (health.upstreamGone)
|
|
188
|
-
return
|
|
220
|
+
return "upstream gone";
|
|
189
221
|
if (health.ahead === 0 && health.behind === 0)
|
|
190
|
-
return
|
|
222
|
+
return "up to date";
|
|
191
223
|
if (health.ahead === 0)
|
|
192
224
|
return `behind ${health.behind}`;
|
|
193
225
|
if (health.behind === 0)
|
package/dist/worktree-info.d.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { type WorktreeHealth } from
|
|
2
|
-
import type { WorktreeEntry } from
|
|
1
|
+
import { type WorktreeHealth } from "./git.js";
|
|
2
|
+
import type { WorktreeEntry } from "./repo.js";
|
|
3
3
|
export interface WorktreeInfo extends WorktreeEntry {
|
|
4
4
|
lastCommitTimestamp: number | null;
|
|
5
|
-
status: WorktreeHealth[
|
|
5
|
+
status: WorktreeHealth["status"] | "unknown";
|
|
6
6
|
upstream: UpstreamState;
|
|
7
7
|
}
|
|
8
8
|
export interface SerializedWorktreeInfo {
|
|
9
9
|
branch: string | null;
|
|
10
10
|
lastCommitTimestamp: number | null;
|
|
11
11
|
path: string;
|
|
12
|
-
status: WorktreeInfo[
|
|
12
|
+
status: WorktreeInfo["status"];
|
|
13
13
|
upstream: UpstreamState;
|
|
14
14
|
}
|
|
15
15
|
export type UpstreamState = {
|
|
16
|
-
kind:
|
|
16
|
+
kind: "detached";
|
|
17
17
|
} | {
|
|
18
|
-
kind:
|
|
18
|
+
kind: "no-upstream";
|
|
19
19
|
} | {
|
|
20
|
-
kind:
|
|
20
|
+
kind: "stale";
|
|
21
21
|
} | {
|
|
22
|
-
kind:
|
|
22
|
+
kind: "tracked";
|
|
23
23
|
ahead: number;
|
|
24
24
|
behind: number;
|
|
25
25
|
} | {
|
|
26
|
-
kind:
|
|
26
|
+
kind: "unknown";
|
|
27
27
|
};
|
|
28
28
|
export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
|
|
29
29
|
export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
|
package/dist/worktree-info.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { readBranchLastCommitTimestamp, readWorktreeHealth } from
|
|
1
|
+
import { readBranchLastCommitTimestamp, readWorktreeHealth, } from "./git.js";
|
|
2
2
|
export async function readWorktreeInfos(worktrees) {
|
|
3
3
|
return Promise.all(worktrees.map((worktree) => readWorktreeInfo(worktree)));
|
|
4
4
|
}
|
|
5
5
|
async function readWorktreeInfo(worktree) {
|
|
6
6
|
const [healthResult, lastCommitResult] = await Promise.allSettled([
|
|
7
7
|
readWorktreeHealth(worktree.path),
|
|
8
|
-
worktree.branch === null
|
|
8
|
+
worktree.branch === null
|
|
9
|
+
? null
|
|
10
|
+
: readBranchLastCommitTimestamp(worktree.path, worktree.branch),
|
|
9
11
|
]);
|
|
10
|
-
const health = healthResult.status ===
|
|
11
|
-
const lastCommitTimestamp = lastCommitResult.status ===
|
|
12
|
-
? lastCommitResult.value
|
|
13
|
-
: null;
|
|
12
|
+
const health = healthResult.status === "fulfilled" ? healthResult.value : null;
|
|
13
|
+
const lastCommitTimestamp = lastCommitResult.status === "fulfilled" ? lastCommitResult.value : null;
|
|
14
14
|
return {
|
|
15
15
|
...worktree,
|
|
16
16
|
lastCommitTimestamp,
|
|
17
|
-
status: health?.status ??
|
|
17
|
+
status: health?.status ?? "unknown",
|
|
18
18
|
upstream: buildUpstreamState(worktree.branch, health),
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
function buildUpstreamState(branch, health) {
|
|
22
22
|
if (branch === null) {
|
|
23
|
-
return { kind:
|
|
23
|
+
return { kind: "detached" };
|
|
24
24
|
}
|
|
25
25
|
if (health === null) {
|
|
26
|
-
return { kind:
|
|
26
|
+
return { kind: "unknown" };
|
|
27
27
|
}
|
|
28
28
|
if (!health.hasUpstream) {
|
|
29
|
-
return { kind:
|
|
29
|
+
return { kind: "no-upstream" };
|
|
30
30
|
}
|
|
31
31
|
if (health.upstreamGone) {
|
|
32
|
-
return { kind:
|
|
32
|
+
return { kind: "stale" };
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
35
35
|
ahead: health.ahead,
|
|
36
36
|
behind: health.behind,
|
|
37
|
-
kind:
|
|
37
|
+
kind: "tracked",
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
export function serializeWorktreeInfo(info) {
|
|
@@ -54,26 +54,26 @@ export function formatWorktreeHint(info) {
|
|
|
54
54
|
if (info.lastCommitTimestamp !== null) {
|
|
55
55
|
details.push(`last: ${formatRelativeAge(info.lastCommitTimestamp)}`);
|
|
56
56
|
}
|
|
57
|
-
return `${info.path} (${details.join(
|
|
57
|
+
return `${info.path} (${details.join(", ")})`;
|
|
58
58
|
}
|
|
59
59
|
export function formatUpstreamState(upstream) {
|
|
60
|
-
if (upstream.kind ===
|
|
61
|
-
return
|
|
60
|
+
if (upstream.kind === "detached") {
|
|
61
|
+
return "n/a";
|
|
62
62
|
}
|
|
63
|
-
if (upstream.kind ===
|
|
64
|
-
return
|
|
63
|
+
if (upstream.kind === "no-upstream") {
|
|
64
|
+
return "no-upstream";
|
|
65
65
|
}
|
|
66
|
-
if (upstream.kind ===
|
|
67
|
-
return
|
|
66
|
+
if (upstream.kind === "stale") {
|
|
67
|
+
return "gone";
|
|
68
68
|
}
|
|
69
|
-
if (upstream.kind ===
|
|
70
|
-
return
|
|
69
|
+
if (upstream.kind === "unknown") {
|
|
70
|
+
return "unknown";
|
|
71
71
|
}
|
|
72
72
|
return formatAheadBehind(upstream.ahead, upstream.behind);
|
|
73
73
|
}
|
|
74
74
|
function formatAheadBehind(ahead, behind) {
|
|
75
75
|
if (ahead === 0 && behind === 0) {
|
|
76
|
-
return
|
|
76
|
+
return "up to date";
|
|
77
77
|
}
|
|
78
78
|
if (ahead === 0) {
|
|
79
79
|
return `behind ${behind}`;
|
|
@@ -84,16 +84,18 @@ function formatAheadBehind(ahead, behind) {
|
|
|
84
84
|
return `ahead ${ahead}, behind ${behind}`;
|
|
85
85
|
}
|
|
86
86
|
export function formatLastCommit(timestampSeconds) {
|
|
87
|
-
return timestampSeconds === null
|
|
87
|
+
return timestampSeconds === null
|
|
88
|
+
? "n/a"
|
|
89
|
+
: formatRelativeAge(timestampSeconds);
|
|
88
90
|
}
|
|
89
91
|
export function formatRelativeAge(timestampSeconds) {
|
|
90
92
|
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - timestampSeconds);
|
|
91
93
|
const units = [
|
|
92
|
-
{ label:
|
|
93
|
-
{ label:
|
|
94
|
-
{ label:
|
|
95
|
-
{ label:
|
|
96
|
-
{ label:
|
|
94
|
+
{ label: "y", seconds: 365 * 24 * 60 * 60 },
|
|
95
|
+
{ label: "mo", seconds: 30 * 24 * 60 * 60 },
|
|
96
|
+
{ label: "d", seconds: 24 * 60 * 60 },
|
|
97
|
+
{ label: "h", seconds: 60 * 60 },
|
|
98
|
+
{ label: "m", seconds: 60 },
|
|
97
99
|
];
|
|
98
100
|
for (const unit of units) {
|
|
99
101
|
const value = Math.floor(ageSeconds / unit.seconds);
|
|
@@ -101,5 +103,5 @@ export function formatRelativeAge(timestampSeconds) {
|
|
|
101
103
|
return `${value}${unit.label} ago`;
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
|
-
return
|
|
106
|
+
return "just now";
|
|
105
107
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { promisify } from
|
|
3
|
-
import { detectRepository, listWorktrees } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { detectRepository, listWorktrees, } from "./repo.js";
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
5
|
// Force English output so error message string matching is locale-independent.
|
|
6
|
-
const GIT_ENV = { ...process.env, LC_ALL:
|
|
6
|
+
const GIT_ENV = { ...process.env, LC_ALL: "C" };
|
|
7
7
|
export async function loadLinkedWorktrees(cwd) {
|
|
8
8
|
const repository = await detectRepository(cwd);
|
|
9
9
|
const linkedWorktrees = (await listWorktrees(cwd)).filter((worktree) => worktree.path !== repository.repoRoot);
|
|
@@ -13,23 +13,38 @@ export async function loadLinkedWorktrees(cwd) {
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
export async function removeWorktree(repoRoot, worktreePath) {
|
|
16
|
-
await execFileAsync(
|
|
16
|
+
await execFileAsync("git", ["worktree", "remove", worktreePath], {
|
|
17
|
+
cwd: repoRoot,
|
|
18
|
+
env: GIT_ENV,
|
|
19
|
+
});
|
|
17
20
|
}
|
|
18
21
|
export async function forceRemoveWorktree(repoRoot, worktreePath) {
|
|
19
|
-
await execFileAsync(
|
|
22
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
|
|
23
|
+
cwd: repoRoot,
|
|
24
|
+
env: GIT_ENV,
|
|
25
|
+
});
|
|
20
26
|
}
|
|
21
27
|
export async function deleteBranch(repoRoot, branch) {
|
|
22
|
-
await execFileAsync(
|
|
28
|
+
await execFileAsync("git", ["branch", "-d", branch], {
|
|
29
|
+
cwd: repoRoot,
|
|
30
|
+
env: GIT_ENV,
|
|
31
|
+
});
|
|
23
32
|
}
|
|
24
33
|
export async function forceDeleteBranch(repoRoot, branch) {
|
|
25
|
-
await execFileAsync(
|
|
34
|
+
await execFileAsync("git", ["branch", "-D", branch], {
|
|
35
|
+
cwd: repoRoot,
|
|
36
|
+
env: GIT_ENV,
|
|
37
|
+
});
|
|
26
38
|
}
|
|
27
39
|
export function isWorktreeDirtyError(error) {
|
|
28
|
-
return hasStderr(error) &&
|
|
40
|
+
return (hasStderr(error) &&
|
|
41
|
+
error.stderr.includes("contains modified or untracked files"));
|
|
29
42
|
}
|
|
30
43
|
export function isBranchUnmergedError(error) {
|
|
31
|
-
return hasStderr(error) && error.stderr.includes(
|
|
44
|
+
return hasStderr(error) && error.stderr.includes("is not fully merged");
|
|
32
45
|
}
|
|
33
46
|
function hasStderr(error) {
|
|
34
|
-
return error instanceof Error &&
|
|
47
|
+
return (error instanceof Error &&
|
|
48
|
+
"stderr" in error &&
|
|
49
|
+
typeof error.stderr === "string");
|
|
35
50
|
}
|
package/dist/worktree-prompts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { confirm, isCancel } from
|
|
1
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
2
2
|
export async function defaultConfirmForceRemoveWorktree(worktreePath) {
|
|
3
3
|
const choice = await confirm({
|
|
4
|
-
active:
|
|
5
|
-
inactive:
|
|
4
|
+
active: "Yes",
|
|
5
|
+
inactive: "No",
|
|
6
6
|
initialValue: false,
|
|
7
7
|
message: `Worktree at ${worktreePath} has untracked or modified files. Force remove?`,
|
|
8
8
|
});
|
|
@@ -10,8 +10,8 @@ export async function defaultConfirmForceRemoveWorktree(worktreePath) {
|
|
|
10
10
|
}
|
|
11
11
|
export async function defaultConfirmForceDeleteBranch(branch) {
|
|
12
12
|
const choice = await confirm({
|
|
13
|
-
active:
|
|
14
|
-
inactive:
|
|
13
|
+
active: "Yes",
|
|
14
|
+
inactive: "No",
|
|
15
15
|
initialValue: false,
|
|
16
16
|
message: `Branch ${branch} has unmerged commits. Force delete?`,
|
|
17
17
|
});
|
package/man/man1/gji-back.1
CHANGED
package/man/man1/gji-clean.1
CHANGED
package/man/man1/gji-config.1
CHANGED
package/man/man1/gji-go.1
CHANGED
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED
package/man/man1/gji-new.1
CHANGED
package/man/man1/gji-open.1
CHANGED
package/man/man1/gji-pr.1
CHANGED
package/man/man1/gji-remove.1
CHANGED
package/man/man1/gji-root.1
CHANGED
package/man/man1/gji-status.1
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.TH GJI\-SYNC\-FILES 1 "May 2026" "gji 0.7.0" "User Commands"
|
|
2
|
+
.SH NAME
|
|
3
|
+
gji\-sync\-files \- manage local files copied into new worktrees
|
|
4
|
+
.SH SYNOPSIS
|
|
5
|
+
.B gji sync\-files [\fIoptions\fR] [options] [command]
|
|
6
|
+
.SH DESCRIPTION
|
|
7
|
+
manage local files copied into new worktrees
|
|
8
|
+
.SH OPTIONS
|
|
9
|
+
.TP
|
|
10
|
+
.B \-\-json
|
|
11
|
+
emit JSON instead of human\-readable output
|
|
12
|
+
.SH SUBCOMMANDS
|
|
13
|
+
.TP
|
|
14
|
+
.B list
|
|
15
|
+
list files synced into new worktrees for this repo
|
|
16
|
+
.TP
|
|
17
|
+
.B add
|
|
18
|
+
add repo\-local sync files to global config
|
|
19
|
+
.TP
|
|
20
|
+
.B remove
|
|
21
|
+
remove repo\-local sync files from global config
|
|
22
|
+
.SH "SEE ALSO"
|
|
23
|
+
.BR gji (1)
|
package/man/man1/gji-sync.1
CHANGED
package/man/man1/gji-warp.1
CHANGED
package/man/man1/gji.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH GJI 1 "May 2026" "gji 0.
|
|
1
|
+
.TH GJI 1 "May 2026" "gji 0.7.0" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
3
|
gji \- Context switching without the mess.
|
|
4
4
|
.SH SYNOPSIS
|
|
@@ -46,6 +46,9 @@ summarize repository and worktree health
|
|
|
46
46
|
.B sync [options]
|
|
47
47
|
fetch and update one or all worktrees
|
|
48
48
|
.TP
|
|
49
|
+
.B sync\-files [options] [command]
|
|
50
|
+
manage local files copied into new worktrees
|
|
51
|
+
.TP
|
|
49
52
|
.B ls [options]
|
|
50
53
|
list active worktrees
|
|
51
54
|
.TP
|
|
@@ -81,6 +84,7 @@ output the version number
|
|
|
81
84
|
.BR gji\-root (1),
|
|
82
85
|
.BR gji\-status (1),
|
|
83
86
|
.BR gji\-sync (1),
|
|
87
|
+
.BR gji\-sync\-files (1),
|
|
84
88
|
.BR gji\-ls (1),
|
|
85
89
|
.BR gji\-clean (1),
|
|
86
90
|
.BR gji\-remove (1),
|