@solaqua/gji 0.6.2 → 0.7.1
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 +266 -165
- 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 +1688 -865
- 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.d.ts +3 -3
- package/dist/hooks.js +19 -19
- package/dist/index.js +13 -9
- package/dist/init.d.ts +5 -5
- package/dist/init.js +115 -103
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +57 -38
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +97 -82
- 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 +14 -14
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/run-hook.d.ts +6 -0
- package/dist/{trigger-hook.js → run-hook.js} +18 -8
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +67 -39
- 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/warp.js +37 -33
- 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-run-hook.1 +9 -0
- 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-warp.1 +1 -1
- package/man/man1/gji.1 +10 -4
- package/package.json +8 -2
- package/dist/trigger-hook.d.ts +0 -6
- package/man/man1/gji-trigger-hook.1 +0 -9
package/dist/ls.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { formatLastCommit, formatUpstreamState, readWorktreeInfos, } from
|
|
1
|
+
import { comparePaths } from "./paths.js";
|
|
2
|
+
import { listWorktrees } from "./repo.js";
|
|
3
|
+
import { formatLastCommit, formatUpstreamState, readWorktreeInfos, } from "./worktree-info.js";
|
|
4
4
|
export async function runLsCommand(options) {
|
|
5
5
|
const worktrees = sortWorktrees(await listWorktrees(options.cwd));
|
|
6
6
|
if (options.compact) {
|
|
@@ -21,50 +21,50 @@ export async function runLsCommand(options) {
|
|
|
21
21
|
}
|
|
22
22
|
export function formatDetailedWorktreeTable(worktrees) {
|
|
23
23
|
const rows = worktrees.map((worktree) => ({
|
|
24
|
-
branch: worktree.branch ??
|
|
24
|
+
branch: worktree.branch ?? "(detached)",
|
|
25
25
|
isCurrent: worktree.isCurrent,
|
|
26
26
|
lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
|
|
27
27
|
path: worktree.path,
|
|
28
28
|
status: worktree.status,
|
|
29
29
|
upstream: formatUpstreamState(worktree.upstream),
|
|
30
30
|
}));
|
|
31
|
-
const branchWidth = Math.max(
|
|
32
|
-
const statusWidth = Math.max(
|
|
33
|
-
const upstreamWidth = Math.max(
|
|
34
|
-
const lastCommitWidth = Math.max(
|
|
31
|
+
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
32
|
+
const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
|
|
33
|
+
const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
|
|
34
|
+
const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
|
|
35
35
|
const lines = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
" " +
|
|
37
|
+
"BRANCH".padEnd(branchWidth, " ") +
|
|
38
|
+
" " +
|
|
39
|
+
"STATUS".padEnd(statusWidth, " ") +
|
|
40
|
+
" " +
|
|
41
|
+
"UPSTREAM".padEnd(upstreamWidth, " ") +
|
|
42
|
+
" " +
|
|
43
|
+
"LAST".padEnd(lastCommitWidth, " ") +
|
|
44
|
+
" PATH",
|
|
45
45
|
];
|
|
46
46
|
for (const row of rows) {
|
|
47
|
-
lines.push(`${row.isCurrent ?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
lines.push(`${row.isCurrent ? "*" : " "} ` +
|
|
48
|
+
`${row.branch.padEnd(branchWidth, " ")} ` +
|
|
49
|
+
`${row.status.padEnd(statusWidth, " ")} ` +
|
|
50
|
+
`${row.upstream.padEnd(upstreamWidth, " ")} ` +
|
|
51
|
+
`${row.lastCommit.padEnd(lastCommitWidth, " ")} ` +
|
|
52
|
+
row.path);
|
|
53
53
|
}
|
|
54
|
-
return lines.join(
|
|
54
|
+
return lines.join("\n");
|
|
55
55
|
}
|
|
56
56
|
export function formatWorktreeTable(worktrees) {
|
|
57
57
|
const rows = worktrees.map((worktree) => ({
|
|
58
|
-
branch: worktree.branch ??
|
|
58
|
+
branch: worktree.branch ?? "(detached)",
|
|
59
59
|
isCurrent: worktree.isCurrent,
|
|
60
60
|
path: worktree.path,
|
|
61
61
|
}));
|
|
62
|
-
const branchWidth = Math.max(
|
|
63
|
-
const lines = [
|
|
62
|
+
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
63
|
+
const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
|
|
64
64
|
for (const row of rows) {
|
|
65
|
-
lines.push(`${row.isCurrent ?
|
|
65
|
+
lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
|
|
66
66
|
}
|
|
67
|
-
return lines.join(
|
|
67
|
+
return lines.join("\n");
|
|
68
68
|
}
|
|
69
69
|
function sortWorktrees(worktrees) {
|
|
70
70
|
return [...worktrees].sort((left, right) => {
|
package/dist/new.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
+
import { type InstallPromptDependencies } from "./install-prompt.js";
|
|
3
3
|
export type { PathConflictChoice };
|
|
4
4
|
export interface NewCommandOptions {
|
|
5
5
|
branch?: string;
|
package/dist/new.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import { isCancel, text } from
|
|
6
|
-
import { loadEffectiveConfig, resolveConfigString } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { detectRepository, resolveWorktreePath, validateBranchName } from
|
|
15
|
-
import { writeShellOutput } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { isCancel, text } from "@clack/prompts";
|
|
6
|
+
import { loadEffectiveConfig, resolveConfigString } from "./config.js";
|
|
7
|
+
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
8
|
+
import { defaultSpawnEditor, EDITORS } from "./editor.js";
|
|
9
|
+
import { syncFiles } from "./file-sync.js";
|
|
10
|
+
import { isHeadless } from "./headless.js";
|
|
11
|
+
import { appendHistory } from "./history.js";
|
|
12
|
+
import { extractHooks, runHook } from "./hooks.js";
|
|
13
|
+
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
14
|
+
import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
|
|
15
|
+
import { writeShellOutput } from "./shell-handoff.js";
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
17
|
-
const NEW_OUTPUT_FILE_ENV =
|
|
17
|
+
const NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
|
|
18
18
|
export function createNewCommand(dependencies = {}) {
|
|
19
19
|
const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
|
|
20
20
|
const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
|
|
@@ -25,10 +25,12 @@ export function createNewCommand(dependencies = {}) {
|
|
|
25
25
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
26
26
|
const usesGeneratedDetachedName = options.detached && options.branch === undefined;
|
|
27
27
|
if (options.editor && !options.open) {
|
|
28
|
-
options.stderr(
|
|
28
|
+
options.stderr("gji new: --editor has no effect without --open\n");
|
|
29
29
|
}
|
|
30
|
-
if (!options.detached &&
|
|
31
|
-
|
|
30
|
+
if (!options.detached &&
|
|
31
|
+
!options.branch &&
|
|
32
|
+
(options.json || isHeadless())) {
|
|
33
|
+
const message = "branch argument is required";
|
|
32
34
|
if (options.json) {
|
|
33
35
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
34
36
|
}
|
|
@@ -38,14 +40,14 @@ export function createNewCommand(dependencies = {}) {
|
|
|
38
40
|
return 1;
|
|
39
41
|
}
|
|
40
42
|
const rawBranch = options.detached
|
|
41
|
-
? options.branch ?? createBranchPlaceholder()
|
|
42
|
-
: options.branch ?? await promptForBranch(createBranchPlaceholder());
|
|
43
|
+
? (options.branch ?? createBranchPlaceholder())
|
|
44
|
+
: (options.branch ?? (await promptForBranch(createBranchPlaceholder())));
|
|
43
45
|
if (!rawBranch) {
|
|
44
46
|
if (options.json) {
|
|
45
|
-
options.stderr(`${JSON.stringify({ error:
|
|
47
|
+
options.stderr(`${JSON.stringify({ error: "Aborted" }, null, 2)}\n`);
|
|
46
48
|
}
|
|
47
49
|
else {
|
|
48
|
-
options.stderr(
|
|
50
|
+
options.stderr("Aborted\n");
|
|
49
51
|
}
|
|
50
52
|
return 1;
|
|
51
53
|
}
|
|
@@ -61,19 +63,21 @@ export function createNewCommand(dependencies = {}) {
|
|
|
61
63
|
return 1;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
|
-
const rawBasePath = resolveConfigString(config,
|
|
65
|
-
const configuredBasePath = rawBasePath?.startsWith(
|
|
66
|
+
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
67
|
+
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~")
|
|
68
|
+
? rawBasePath
|
|
69
|
+
: undefined;
|
|
66
70
|
const worktreeName = options.detached
|
|
67
71
|
? rawBranch
|
|
68
72
|
: applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
|
|
69
73
|
const worktreePath = usesGeneratedDetachedName
|
|
70
74
|
? await resolveUniqueDetachedWorktreePath(repository.repoRoot, worktreeName, configuredBasePath)
|
|
71
75
|
: resolveWorktreePath(repository.repoRoot, worktreeName, configuredBasePath);
|
|
72
|
-
if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
|
|
76
|
+
if (!usesGeneratedDetachedName && (await pathExists(worktreePath))) {
|
|
73
77
|
if (options.force) {
|
|
74
78
|
if (!options.dryRun) {
|
|
75
79
|
try {
|
|
76
|
-
await execFileAsync(
|
|
80
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
|
|
77
81
|
}
|
|
78
82
|
catch (err) {
|
|
79
83
|
if (!isNotRegisteredWorktreeError(err)) {
|
|
@@ -88,7 +92,9 @@ export function createNewCommand(dependencies = {}) {
|
|
|
88
92
|
}
|
|
89
93
|
if (!options.detached) {
|
|
90
94
|
try {
|
|
91
|
-
await execFileAsync(
|
|
95
|
+
await execFileAsync("git", ["branch", "-D", worktreeName], {
|
|
96
|
+
cwd: repository.repoRoot,
|
|
97
|
+
});
|
|
92
98
|
}
|
|
93
99
|
catch {
|
|
94
100
|
// Branch may not exist; proceed anyway.
|
|
@@ -104,13 +110,13 @@ export function createNewCommand(dependencies = {}) {
|
|
|
104
110
|
else {
|
|
105
111
|
options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
106
112
|
options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
|
|
107
|
-
options.stderr(`Hint: Use 'gji
|
|
113
|
+
options.stderr(`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks\n`);
|
|
108
114
|
}
|
|
109
115
|
return 1;
|
|
110
116
|
}
|
|
111
117
|
else {
|
|
112
118
|
const choice = await prompt(worktreePath);
|
|
113
|
-
if (choice ===
|
|
119
|
+
if (choice === "reuse") {
|
|
114
120
|
appendHistory(worktreePath, worktreeName).catch(() => undefined);
|
|
115
121
|
await writeOutput(worktreePath, options.stdout);
|
|
116
122
|
return 0;
|
|
@@ -125,23 +131,25 @@ export function createNewCommand(dependencies = {}) {
|
|
|
125
131
|
}
|
|
126
132
|
else {
|
|
127
133
|
const resolvedEditor = options.open
|
|
128
|
-
? (options.editor ?? resolveConfigString(config,
|
|
134
|
+
? (options.editor ?? resolveConfigString(config, "editor"))
|
|
129
135
|
: undefined;
|
|
130
|
-
const openNote = resolvedEditor
|
|
136
|
+
const openNote = resolvedEditor
|
|
137
|
+
? `, then open in ${resolvedEditor}`
|
|
138
|
+
: "";
|
|
131
139
|
options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n`);
|
|
132
140
|
}
|
|
133
141
|
return 0;
|
|
134
142
|
}
|
|
135
143
|
await mkdir(dirname(worktreePath), { recursive: true });
|
|
136
144
|
const gitArgs = options.detached
|
|
137
|
-
? [
|
|
138
|
-
: await localBranchExists(repository.repoRoot, worktreeName)
|
|
139
|
-
? [
|
|
140
|
-
: [
|
|
141
|
-
await execFileAsync(
|
|
145
|
+
? ["worktree", "add", "--detach", worktreePath]
|
|
146
|
+
: (await localBranchExists(repository.repoRoot, worktreeName))
|
|
147
|
+
? ["worktree", "add", worktreePath, worktreeName]
|
|
148
|
+
: ["worktree", "add", "-b", worktreeName, worktreePath];
|
|
149
|
+
await execFileAsync("git", gitArgs, { cwd: repository.repoRoot });
|
|
142
150
|
// Sync files from main worktree before afterCreate so synced files are available to install scripts.
|
|
143
151
|
const syncPatterns = Array.isArray(config.syncFiles)
|
|
144
|
-
? config.syncFiles.filter((p) => typeof p ===
|
|
152
|
+
? config.syncFiles.filter((p) => typeof p === "string")
|
|
145
153
|
: [];
|
|
146
154
|
for (const pattern of syncPatterns) {
|
|
147
155
|
try {
|
|
@@ -153,7 +161,11 @@ export function createNewCommand(dependencies = {}) {
|
|
|
153
161
|
}
|
|
154
162
|
await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
|
|
155
163
|
const hooks = extractHooks(config);
|
|
156
|
-
await runHook(hooks
|
|
164
|
+
await runHook(hooks["after-create"], worktreePath, {
|
|
165
|
+
branch: worktreeName,
|
|
166
|
+
path: worktreePath,
|
|
167
|
+
repo: basename(repository.repoRoot),
|
|
168
|
+
}, options.stderr);
|
|
157
169
|
if (options.json) {
|
|
158
170
|
options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}\n`);
|
|
159
171
|
}
|
|
@@ -162,7 +174,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
162
174
|
await writeOutput(worktreePath, options.stdout);
|
|
163
175
|
}
|
|
164
176
|
if (options.open) {
|
|
165
|
-
const resolvedEditor = options.editor ?? resolveConfigString(config,
|
|
177
|
+
const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
|
|
166
178
|
await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
|
|
167
179
|
}
|
|
168
180
|
return 0;
|
|
@@ -171,48 +183,48 @@ export function createNewCommand(dependencies = {}) {
|
|
|
171
183
|
export const runNewCommand = createNewCommand();
|
|
172
184
|
export function generateBranchPlaceholder(random = Math.random) {
|
|
173
185
|
const roots = [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
186
|
+
"socrates",
|
|
187
|
+
"prometheus",
|
|
188
|
+
"beethoven",
|
|
189
|
+
"ada",
|
|
190
|
+
"turing",
|
|
191
|
+
"hypatia",
|
|
192
|
+
"tesla",
|
|
193
|
+
"curie",
|
|
194
|
+
"diogenes",
|
|
195
|
+
"plato",
|
|
196
|
+
"hephaestus",
|
|
197
|
+
"athena",
|
|
198
|
+
"archimedes",
|
|
199
|
+
"euclid",
|
|
200
|
+
"heraclitus",
|
|
201
|
+
"galileo",
|
|
202
|
+
"newton",
|
|
203
|
+
"lovelace",
|
|
204
|
+
"nietzsche",
|
|
205
|
+
"kafka",
|
|
194
206
|
];
|
|
195
207
|
const antics = [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
"borrowed-a-bike",
|
|
209
|
+
"brought-snacks",
|
|
210
|
+
"missed-the-bus",
|
|
211
|
+
"lost-the-keys",
|
|
212
|
+
"spilled-the-coffee",
|
|
213
|
+
"forgot-the-umbrella",
|
|
214
|
+
"walked-the-dog",
|
|
215
|
+
"missed-the-train",
|
|
216
|
+
"wrote-a-poem",
|
|
217
|
+
"burned-the-toast",
|
|
218
|
+
"fed-the-pigeons",
|
|
219
|
+
"watered-the-plants",
|
|
220
|
+
"washed-the-dishes",
|
|
221
|
+
"folded-the-laundry",
|
|
222
|
+
"took-a-nap",
|
|
211
223
|
];
|
|
212
224
|
return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
|
|
213
225
|
}
|
|
214
226
|
function applyConfiguredBranchPrefix(branch, branchPrefix) {
|
|
215
|
-
if (typeof branchPrefix !==
|
|
227
|
+
if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
|
|
216
228
|
return branch;
|
|
217
229
|
}
|
|
218
230
|
if (branch.startsWith(branchPrefix)) {
|
|
@@ -225,7 +237,7 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
|
|
|
225
237
|
while (true) {
|
|
226
238
|
const candidateName = attempt === 1 ? baseName : `${baseName}-${attempt}`;
|
|
227
239
|
const candidatePath = resolveWorktreePath(repoRoot, candidateName, basePath);
|
|
228
|
-
if (!await pathExists(candidatePath)) {
|
|
240
|
+
if (!(await pathExists(candidatePath))) {
|
|
229
241
|
return candidatePath;
|
|
230
242
|
}
|
|
231
243
|
attempt += 1;
|
|
@@ -234,7 +246,7 @@ async function resolveUniqueDetachedWorktreePath(repoRoot, baseName, basePath) {
|
|
|
234
246
|
async function defaultPromptForBranch(placeholder) {
|
|
235
247
|
const choice = await text({
|
|
236
248
|
defaultValue: placeholder,
|
|
237
|
-
message:
|
|
249
|
+
message: "Name the new branch",
|
|
238
250
|
placeholder,
|
|
239
251
|
validate: (value) => {
|
|
240
252
|
const trimmed = value.trim();
|
|
@@ -252,7 +264,7 @@ function pickRandom(values, random) {
|
|
|
252
264
|
}
|
|
253
265
|
async function localBranchExists(repoRoot, branchName) {
|
|
254
266
|
try {
|
|
255
|
-
await execFileAsync(
|
|
267
|
+
await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
256
268
|
return true;
|
|
257
269
|
}
|
|
258
270
|
catch {
|
|
@@ -264,17 +276,20 @@ async function writeOutput(worktreePath, stdout) {
|
|
|
264
276
|
}
|
|
265
277
|
function isNotRegisteredWorktreeError(error) {
|
|
266
278
|
const stderr = hasExecStderr(error) ? error.stderr : String(error);
|
|
267
|
-
return stderr.includes(
|
|
279
|
+
return (stderr.includes("is not a working tree") ||
|
|
280
|
+
stderr.includes("not a linked working tree"));
|
|
268
281
|
}
|
|
269
282
|
function hasExecStderr(error) {
|
|
270
|
-
return error instanceof Error &&
|
|
283
|
+
return (error instanceof Error &&
|
|
284
|
+
"stderr" in error &&
|
|
285
|
+
typeof error.stderr === "string");
|
|
271
286
|
}
|
|
272
287
|
function toExecMessage(error) {
|
|
273
288
|
return hasExecStderr(error) ? error.stderr.trim() : String(error);
|
|
274
289
|
}
|
|
275
290
|
async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
276
291
|
if (!editorCli) {
|
|
277
|
-
stderr(
|
|
292
|
+
stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
|
|
278
293
|
return;
|
|
279
294
|
}
|
|
280
295
|
const editorDef = EDITORS.find((e) => e.cli === editorCli);
|
package/dist/open.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type EditorDefinition } from
|
|
2
|
-
import { type WorktreeEntry } from
|
|
1
|
+
import { type EditorDefinition } from "./editor.js";
|
|
2
|
+
import { type WorktreeEntry } from "./repo.js";
|
|
3
3
|
export type { EditorDefinition };
|
|
4
4
|
export interface OpenCommandOptions {
|
|
5
5
|
branch?: string;
|
package/dist/open.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execFile } from
|
|
2
|
-
import { access, writeFile } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import { isCancel, select } from
|
|
6
|
-
import { loadEffectiveConfig, resolveConfigString, updateGlobalConfigKey } from
|
|
7
|
-
import { defaultSpawnEditor, EDITORS } from
|
|
8
|
-
import { isHeadless } from
|
|
9
|
-
import { detectRepository, listWorktrees, sortByCurrentFirst } from
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { isCancel, select } from "@clack/prompts";
|
|
6
|
+
import { loadEffectiveConfig, resolveConfigString, updateGlobalConfigKey, } from "./config.js";
|
|
7
|
+
import { defaultSpawnEditor, EDITORS, } from "./editor.js";
|
|
8
|
+
import { isHeadless } from "./headless.js";
|
|
9
|
+
import { detectRepository, listWorktrees, sortByCurrentFirst, } from "./repo.js";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
export function createOpenCommand(dependencies = {}) {
|
|
12
12
|
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
@@ -35,14 +35,14 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
35
35
|
else {
|
|
36
36
|
const chosen = await promptForWorktree(sortByCurrentFirst(worktrees));
|
|
37
37
|
if (!chosen) {
|
|
38
|
-
options.stderr(
|
|
38
|
+
options.stderr("Aborted\n");
|
|
39
39
|
return 1;
|
|
40
40
|
}
|
|
41
41
|
targetPath = chosen;
|
|
42
42
|
}
|
|
43
43
|
// Resolve which editor to use.
|
|
44
44
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
45
|
-
const savedEditor = resolveConfigString(config,
|
|
45
|
+
const savedEditor = resolveConfigString(config, "editor");
|
|
46
46
|
let editorCli;
|
|
47
47
|
if (options.editor) {
|
|
48
48
|
editorCli = options.editor;
|
|
@@ -53,7 +53,7 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
53
53
|
else {
|
|
54
54
|
const installed = await detectEditors();
|
|
55
55
|
if (installed.length === 0) {
|
|
56
|
-
options.stderr(
|
|
56
|
+
options.stderr("gji open: no supported editor detected. Use --editor <code|cursor|zed|...> to specify one.\n");
|
|
57
57
|
return 1;
|
|
58
58
|
}
|
|
59
59
|
if (installed.length === 1 || isHeadless()) {
|
|
@@ -62,7 +62,7 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
62
62
|
else {
|
|
63
63
|
const chosen = await promptForEditor(installed);
|
|
64
64
|
if (!chosen) {
|
|
65
|
-
options.stderr(
|
|
65
|
+
options.stderr("Aborted\n");
|
|
66
66
|
return 1;
|
|
67
67
|
}
|
|
68
68
|
editorCli = chosen;
|
|
@@ -70,7 +70,7 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
// Persist editor choice when requested.
|
|
72
72
|
if (options.save && editorCli !== savedEditor) {
|
|
73
|
-
await updateGlobalConfigKey(
|
|
73
|
+
await updateGlobalConfigKey("editor", editorCli);
|
|
74
74
|
const displayName = EDITORS.find((e) => e.cli === editorCli)?.name ?? editorCli;
|
|
75
75
|
options.stdout(`Saved editor "${displayName}" to global config\n`);
|
|
76
76
|
}
|
|
@@ -106,12 +106,15 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
export const runOpenCommand = createOpenCommand();
|
|
108
108
|
async function detectInstalledEditors() {
|
|
109
|
-
const results = await Promise.all(EDITORS.map(async (editor) => ({
|
|
109
|
+
const results = await Promise.all(EDITORS.map(async (editor) => ({
|
|
110
|
+
editor,
|
|
111
|
+
available: await isCommandAvailable(editor.cli),
|
|
112
|
+
})));
|
|
110
113
|
return results.filter((r) => r.available).map((r) => r.editor);
|
|
111
114
|
}
|
|
112
115
|
async function isCommandAvailable(command) {
|
|
113
116
|
try {
|
|
114
|
-
await execFileAsync(
|
|
117
|
+
await execFileAsync("which", [command]);
|
|
115
118
|
return true;
|
|
116
119
|
}
|
|
117
120
|
catch {
|
|
@@ -120,10 +123,10 @@ async function isCommandAvailable(command) {
|
|
|
120
123
|
}
|
|
121
124
|
async function defaultPromptForWorktree(worktrees) {
|
|
122
125
|
const choice = await select({
|
|
123
|
-
message:
|
|
126
|
+
message: "Choose a worktree to open",
|
|
124
127
|
options: worktrees.map((w) => ({
|
|
125
128
|
value: w.path,
|
|
126
|
-
label: w.branch ??
|
|
129
|
+
label: w.branch ?? "(detached)",
|
|
127
130
|
hint: w.isCurrent ? `${w.path} (current)` : w.path,
|
|
128
131
|
})),
|
|
129
132
|
});
|
|
@@ -133,7 +136,7 @@ async function defaultPromptForWorktree(worktrees) {
|
|
|
133
136
|
}
|
|
134
137
|
async function defaultPromptForEditor(editors) {
|
|
135
138
|
const choice = await select({
|
|
136
|
-
message:
|
|
139
|
+
message: "Choose an editor",
|
|
137
140
|
options: editors.map((e) => ({ value: e.cli, label: e.name })),
|
|
138
141
|
});
|
|
139
142
|
if (isCancel(choice))
|
|
@@ -149,7 +152,7 @@ async function ensureWorkspaceFile(worktreePath, repoName) {
|
|
|
149
152
|
catch {
|
|
150
153
|
// File doesn't exist yet — create it.
|
|
151
154
|
}
|
|
152
|
-
const workspace = { folders: [{ path:
|
|
153
|
-
await writeFile(workspacePath, `${JSON.stringify(workspace, null, 2)}\n`,
|
|
155
|
+
const workspace = { folders: [{ path: "." }], settings: {} };
|
|
156
|
+
await writeFile(workspacePath, `${JSON.stringify(workspace, null, 2)}\n`, "utf8");
|
|
154
157
|
return workspacePath;
|
|
155
158
|
}
|