@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/package-manager.js
CHANGED
|
@@ -1,70 +1,121 @@
|
|
|
1
|
-
import { access, readdir } from
|
|
2
|
-
import { join } from
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
const ENTRIES = [
|
|
4
4
|
// JavaScript / TypeScript
|
|
5
|
-
{ name:
|
|
6
|
-
{ name:
|
|
7
|
-
{ name:
|
|
8
|
-
{ name:
|
|
9
|
-
{ name:
|
|
5
|
+
{ name: "pnpm", signals: ["pnpm-lock.yaml"], command: "pnpm install" },
|
|
6
|
+
{ name: "yarn", signals: ["yarn.lock"], command: "yarn install" },
|
|
7
|
+
{ name: "bun", signals: ["bun.lockb"], command: "bun install" },
|
|
8
|
+
{ name: "npm", signals: ["package-lock.json"], command: "npm install" },
|
|
9
|
+
{ name: "deno", signals: ["deno.json", "deno.jsonc"], command: "deno cache" },
|
|
10
10
|
// Python
|
|
11
|
-
{ name:
|
|
12
|
-
{ name:
|
|
13
|
-
{ name:
|
|
14
|
-
{ name:
|
|
15
|
-
{
|
|
16
|
-
|
|
11
|
+
{ name: "poetry", signals: ["poetry.lock"], command: "poetry install" },
|
|
12
|
+
{ name: "uv", signals: ["uv.lock"], command: "uv sync" },
|
|
13
|
+
{ name: "pipenv", signals: ["Pipfile.lock"], command: "pipenv install" },
|
|
14
|
+
{ name: "pdm", signals: ["pdm.lock"], command: "pdm install" },
|
|
15
|
+
{
|
|
16
|
+
name: "conda-lock",
|
|
17
|
+
signals: ["conda-lock.yml"],
|
|
18
|
+
command: "conda-lock install",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "conda",
|
|
22
|
+
signals: ["environment.yml"],
|
|
23
|
+
command: "conda env update --file environment.yml",
|
|
24
|
+
},
|
|
17
25
|
// R
|
|
18
|
-
{
|
|
26
|
+
{
|
|
27
|
+
name: "renv",
|
|
28
|
+
signals: ["renv.lock"],
|
|
29
|
+
command: "Rscript -e 'renv::restore()'",
|
|
30
|
+
},
|
|
19
31
|
// Rust
|
|
20
|
-
{ name:
|
|
32
|
+
{ name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
|
|
21
33
|
// Go
|
|
22
|
-
{ name:
|
|
34
|
+
{ name: "go", signals: ["go.sum"], command: "go mod download" },
|
|
23
35
|
// Ruby
|
|
24
|
-
{ name:
|
|
36
|
+
{ name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
|
|
25
37
|
// PHP
|
|
26
|
-
{ name:
|
|
38
|
+
{ name: "composer", signals: ["composer.lock"], command: "composer install" },
|
|
27
39
|
// Elixir / Erlang
|
|
28
|
-
{ name:
|
|
29
|
-
{ name:
|
|
40
|
+
{ name: "mix", signals: ["mix.lock"], command: "mix deps.get" },
|
|
41
|
+
{ name: "rebar3", signals: ["rebar.lock"], command: "rebar3 deps" },
|
|
30
42
|
// Dart / Flutter
|
|
31
|
-
{ name:
|
|
43
|
+
{ name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
|
|
32
44
|
// Java / Kotlin / Scala
|
|
33
|
-
{ name:
|
|
34
|
-
{ name:
|
|
35
|
-
{
|
|
36
|
-
|
|
45
|
+
{ name: "maven", signals: ["pom.xml"], command: "mvn install" },
|
|
46
|
+
{ name: "gradle", signals: ["gradlew"], command: "./gradlew build" },
|
|
47
|
+
{
|
|
48
|
+
name: "gradle",
|
|
49
|
+
signals: ["build.gradle", "build.gradle.kts"],
|
|
50
|
+
command: "gradle build",
|
|
51
|
+
},
|
|
52
|
+
{ name: "sbt", signals: ["build.sbt"], command: "sbt compile" },
|
|
37
53
|
// .NET (C# / F# / VB)
|
|
38
|
-
{
|
|
54
|
+
{
|
|
55
|
+
name: "dotnet",
|
|
56
|
+
signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
|
|
57
|
+
command: "dotnet restore",
|
|
58
|
+
glob: true,
|
|
59
|
+
},
|
|
39
60
|
// Swift
|
|
40
|
-
{
|
|
61
|
+
{
|
|
62
|
+
name: "swift",
|
|
63
|
+
signals: ["Package.swift"],
|
|
64
|
+
command: "swift package resolve",
|
|
65
|
+
},
|
|
41
66
|
// Haskell
|
|
42
|
-
{ name:
|
|
43
|
-
{
|
|
44
|
-
|
|
67
|
+
{ name: "stack", signals: ["stack.yaml"], command: "stack build" },
|
|
68
|
+
{
|
|
69
|
+
name: "cabal",
|
|
70
|
+
signals: ["cabal.project"],
|
|
71
|
+
command: "cabal install --only-dependencies",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "cabal",
|
|
75
|
+
signals: ["*.cabal"],
|
|
76
|
+
command: "cabal install --only-dependencies",
|
|
77
|
+
glob: true,
|
|
78
|
+
},
|
|
45
79
|
// Clojure
|
|
46
|
-
{ name:
|
|
47
|
-
{ name:
|
|
80
|
+
{ name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
|
|
81
|
+
{ name: "leiningen", signals: ["project.clj"], command: "lein deps" },
|
|
48
82
|
// OCaml
|
|
49
|
-
{ name:
|
|
83
|
+
{ name: "dune", signals: ["dune-project"], command: "dune build" },
|
|
50
84
|
// Julia
|
|
51
|
-
{
|
|
85
|
+
{
|
|
86
|
+
name: "julia",
|
|
87
|
+
signals: ["Manifest.toml"],
|
|
88
|
+
command: "julia --project -e 'using Pkg; Pkg.instantiate()'",
|
|
89
|
+
},
|
|
52
90
|
// Nim
|
|
53
|
-
{
|
|
91
|
+
{
|
|
92
|
+
name: "nimble",
|
|
93
|
+
signals: ["*.nimble"],
|
|
94
|
+
command: "nimble install",
|
|
95
|
+
glob: true,
|
|
96
|
+
},
|
|
54
97
|
// Crystal
|
|
55
|
-
{ name:
|
|
98
|
+
{ name: "shards", signals: ["shard.yml"], command: "shards install" },
|
|
56
99
|
// Perl
|
|
57
|
-
{ name:
|
|
100
|
+
{ name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
|
|
58
101
|
// Zig
|
|
59
|
-
{ name:
|
|
102
|
+
{ name: "zig", signals: ["build.zig.zon"], command: "zig build" },
|
|
60
103
|
// C / C++
|
|
61
|
-
{ name:
|
|
62
|
-
{
|
|
104
|
+
{ name: "vcpkg", signals: ["vcpkg.json"], command: "vcpkg install" },
|
|
105
|
+
{
|
|
106
|
+
name: "conan",
|
|
107
|
+
signals: ["conanfile.py", "conanfile.txt"],
|
|
108
|
+
command: "conan install .",
|
|
109
|
+
},
|
|
63
110
|
// Nix
|
|
64
|
-
{ name:
|
|
65
|
-
{ name:
|
|
111
|
+
{ name: "nix", signals: ["flake.nix"], command: "nix develop" },
|
|
112
|
+
{ name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
|
|
66
113
|
// Terraform / OpenTofu
|
|
67
|
-
{
|
|
114
|
+
{
|
|
115
|
+
name: "terraform",
|
|
116
|
+
signals: ["terraform.lock.hcl"],
|
|
117
|
+
command: "terraform init",
|
|
118
|
+
},
|
|
68
119
|
];
|
|
69
120
|
export async function detectPackageManager(repoRoot) {
|
|
70
121
|
for (const entry of ENTRIES) {
|
|
@@ -102,7 +153,7 @@ async function matchesGlob(repoRoot, patterns) {
|
|
|
102
153
|
}
|
|
103
154
|
function patternToRegex(pattern) {
|
|
104
155
|
const escaped = pattern
|
|
105
|
-
.replace(/[.+^${}()|[\]\\]/g,
|
|
106
|
-
.replace(/\*/g,
|
|
156
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
157
|
+
.replace(/\*/g, "[^/]*");
|
|
107
158
|
return new RegExp(`^${escaped}$`);
|
|
108
159
|
}
|
package/dist/pr.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type PathConflictChoice } from
|
|
2
|
-
import { type InstallPromptDependencies } from
|
|
1
|
+
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
+
import { type InstallPromptDependencies } from "./install-prompt.js";
|
|
3
3
|
export type { PathConflictChoice };
|
|
4
4
|
export interface PrCommandOptions {
|
|
5
5
|
cwd: string;
|
package/dist/pr.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import { loadEffectiveConfig, resolveConfigString } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { appendHistory } from
|
|
10
|
-
import {
|
|
11
|
-
import { maybeRunInstallPrompt } from
|
|
12
|
-
import { detectRepository, resolveWorktreePath } from
|
|
13
|
-
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 { loadEffectiveConfig, resolveConfigString } from "./config.js";
|
|
6
|
+
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
7
|
+
import { syncFiles } from "./file-sync.js";
|
|
8
|
+
import { isHeadless } from "./headless.js";
|
|
9
|
+
import { appendHistory } from "./history.js";
|
|
10
|
+
import { extractHooks, runHook } from "./hooks.js";
|
|
11
|
+
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
12
|
+
import { detectRepository, resolveWorktreePath } from "./repo.js";
|
|
13
|
+
import { writeShellOutput } from "./shell-handoff.js";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
-
const PR_OUTPUT_FILE_ENV =
|
|
15
|
+
const PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
|
|
16
16
|
export function parsePrInput(input) {
|
|
17
17
|
if (/^\d+$/.test(input))
|
|
18
18
|
return input;
|
|
@@ -42,8 +42,10 @@ export function createPrCommand(dependencies = {}) {
|
|
|
42
42
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
43
43
|
const branchName = `pr/${prNumber}`;
|
|
44
44
|
const remoteRef = `refs/remotes/origin/pull/${prNumber}/head`;
|
|
45
|
-
const rawBasePath = resolveConfigString(config,
|
|
46
|
-
const configuredBasePath = rawBasePath?.startsWith(
|
|
45
|
+
const rawBasePath = resolveConfigString(config, "worktreePath");
|
|
46
|
+
const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~")
|
|
47
|
+
? rawBasePath
|
|
48
|
+
: undefined;
|
|
47
49
|
const worktreePath = resolveWorktreePath(repository.repoRoot, branchName, configuredBasePath);
|
|
48
50
|
if (await pathExists(worktreePath)) {
|
|
49
51
|
if (options.json || isHeadless()) {
|
|
@@ -58,7 +60,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
58
60
|
return 1;
|
|
59
61
|
}
|
|
60
62
|
const choice = await prompt(worktreePath);
|
|
61
|
-
if (choice ===
|
|
63
|
+
if (choice === "reuse") {
|
|
62
64
|
appendHistory(worktreePath, branchName).catch(() => undefined);
|
|
63
65
|
await writeOutput(worktreePath, options.stdout);
|
|
64
66
|
return 0;
|
|
@@ -92,12 +94,12 @@ export function createPrCommand(dependencies = {}) {
|
|
|
92
94
|
await mkdir(dirname(worktreePath), { recursive: true });
|
|
93
95
|
const branchAlreadyExists = await localBranchExists(repository.repoRoot, branchName);
|
|
94
96
|
const worktreeArgs = branchAlreadyExists
|
|
95
|
-
? [
|
|
96
|
-
: [
|
|
97
|
-
await execFileAsync(
|
|
97
|
+
? ["worktree", "add", worktreePath, branchName]
|
|
98
|
+
: ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
|
|
99
|
+
await execFileAsync("git", worktreeArgs, { cwd: repository.repoRoot });
|
|
98
100
|
// Sync files from main worktree before afterCreate so synced files are available to install scripts.
|
|
99
101
|
const syncPatterns = Array.isArray(config.syncFiles)
|
|
100
|
-
? config.syncFiles.filter((p) => typeof p ===
|
|
102
|
+
? config.syncFiles.filter((p) => typeof p === "string")
|
|
101
103
|
: [];
|
|
102
104
|
for (const pattern of syncPatterns) {
|
|
103
105
|
try {
|
|
@@ -109,7 +111,11 @@ export function createPrCommand(dependencies = {}) {
|
|
|
109
111
|
}
|
|
110
112
|
await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
|
|
111
113
|
const hooks = extractHooks(config);
|
|
112
|
-
await runHook(hooks
|
|
114
|
+
await runHook(hooks["after-create"], worktreePath, {
|
|
115
|
+
branch: branchName,
|
|
116
|
+
path: worktreePath,
|
|
117
|
+
repo: basename(repository.repoRoot),
|
|
118
|
+
}, options.stderr);
|
|
113
119
|
if (options.json) {
|
|
114
120
|
options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}\n`);
|
|
115
121
|
}
|
|
@@ -122,7 +128,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
122
128
|
}
|
|
123
129
|
async function localBranchExists(repoRoot, branchName) {
|
|
124
130
|
try {
|
|
125
|
-
await execFileAsync(
|
|
131
|
+
await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
|
|
126
132
|
return true;
|
|
127
133
|
}
|
|
128
134
|
catch {
|
|
@@ -133,7 +139,7 @@ export const runPrCommand = createPrCommand();
|
|
|
133
139
|
async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
|
|
134
140
|
for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
|
|
135
141
|
try {
|
|
136
|
-
await execFileAsync(
|
|
142
|
+
await execFileAsync("git", ["fetch", "origin", `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
139
145
|
catch {
|
|
@@ -143,32 +149,39 @@ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
|
|
|
143
149
|
throw new Error(`No pull request ref found for #${prNumber}`);
|
|
144
150
|
}
|
|
145
151
|
function listPullRequestSourceRefs(input, prNumber) {
|
|
146
|
-
const allForges = [
|
|
152
|
+
const allForges = [
|
|
153
|
+
"github",
|
|
154
|
+
"gitlab",
|
|
155
|
+
"bitbucket",
|
|
156
|
+
];
|
|
147
157
|
const preferredForge = detectPullRequestForge(input);
|
|
148
|
-
const orderedForges = preferredForge ===
|
|
158
|
+
const orderedForges = preferredForge === "unknown"
|
|
149
159
|
? allForges
|
|
150
|
-
: [
|
|
160
|
+
: [
|
|
161
|
+
preferredForge,
|
|
162
|
+
...allForges.filter((forge) => forge !== preferredForge),
|
|
163
|
+
];
|
|
151
164
|
return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
|
|
152
165
|
}
|
|
153
166
|
function detectPullRequestForge(input) {
|
|
154
167
|
if (/\/pull-requests\/\d+/.test(input)) {
|
|
155
|
-
return
|
|
168
|
+
return "bitbucket";
|
|
156
169
|
}
|
|
157
170
|
if (/\/merge_requests\/\d+/.test(input)) {
|
|
158
|
-
return
|
|
171
|
+
return "gitlab";
|
|
159
172
|
}
|
|
160
173
|
if (/\/pull\/\d+/.test(input)) {
|
|
161
|
-
return
|
|
174
|
+
return "github";
|
|
162
175
|
}
|
|
163
|
-
return
|
|
176
|
+
return "unknown";
|
|
164
177
|
}
|
|
165
178
|
function sourceRefForForge(forge, prNumber) {
|
|
166
179
|
switch (forge) {
|
|
167
|
-
case
|
|
180
|
+
case "bitbucket":
|
|
168
181
|
return `refs/pull-requests/${prNumber}/from`;
|
|
169
|
-
case
|
|
182
|
+
case "github":
|
|
170
183
|
return `refs/pull/${prNumber}/head`;
|
|
171
|
-
case
|
|
184
|
+
case "gitlab":
|
|
172
185
|
return `refs/merge-requests/${prNumber}/head`;
|
|
173
186
|
}
|
|
174
187
|
}
|
package/dist/remove.d.ts
CHANGED
package/dist/remove.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import { basename } from
|
|
2
|
-
import { confirm, isCancel, select } from
|
|
3
|
-
import { loadEffectiveConfig } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { sortByCurrentFirst } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
const REMOVE_OUTPUT_FILE_ENV =
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { confirm, isCancel, select } from "@clack/prompts";
|
|
3
|
+
import { loadEffectiveConfig } from "./config.js";
|
|
4
|
+
import { isHeadless } from "./headless.js";
|
|
5
|
+
import { extractHooks, runHook } from "./hooks.js";
|
|
6
|
+
import { sortByCurrentFirst } from "./repo.js";
|
|
7
|
+
import { writeShellOutput } from "./shell-handoff.js";
|
|
8
|
+
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
|
|
9
|
+
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
|
|
10
|
+
const REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
|
|
11
11
|
export function createRemoveCommand(dependencies = {}) {
|
|
12
12
|
const promptForWorktree = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
13
13
|
const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
|
|
14
|
-
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ??
|
|
14
|
+
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ??
|
|
15
|
+
defaultConfirmForceRemoveWorktree;
|
|
15
16
|
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
16
17
|
return async function runRemoveCommand(options) {
|
|
17
18
|
const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
|
|
18
19
|
if (linkedWorktrees.length === 0) {
|
|
19
|
-
emitError(options,
|
|
20
|
+
emitError(options, "No linked worktrees to finish");
|
|
20
21
|
return 1;
|
|
21
22
|
}
|
|
22
23
|
if (!options.branch && (options.json || isHeadless())) {
|
|
23
|
-
const message =
|
|
24
|
+
const message = "branch argument is required";
|
|
24
25
|
if (options.json) {
|
|
25
26
|
emitError(options, message);
|
|
26
27
|
}
|
|
@@ -29,9 +30,10 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
29
30
|
}
|
|
30
31
|
return 1;
|
|
31
32
|
}
|
|
32
|
-
const selection = options.branch ??
|
|
33
|
+
const selection = options.branch ??
|
|
34
|
+
(await promptForWorktree(sortByCurrentFirst(linkedWorktrees)));
|
|
33
35
|
if (!selection) {
|
|
34
|
-
options.stderr(
|
|
36
|
+
options.stderr("Aborted\n");
|
|
35
37
|
return 1;
|
|
36
38
|
}
|
|
37
39
|
const worktree = linkedWorktrees.find((entry) => entry.branch === selection || entry.path === selection);
|
|
@@ -40,7 +42,7 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
40
42
|
return 1;
|
|
41
43
|
}
|
|
42
44
|
if (!options.dryRun && !options.force && (options.json || isHeadless())) {
|
|
43
|
-
const message =
|
|
45
|
+
const message = "--force is required";
|
|
44
46
|
if (options.json) {
|
|
45
47
|
emitError(options, message);
|
|
46
48
|
}
|
|
@@ -49,8 +51,10 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
49
51
|
}
|
|
50
52
|
return 1;
|
|
51
53
|
}
|
|
52
|
-
if (!options.dryRun &&
|
|
53
|
-
options.
|
|
54
|
+
if (!options.dryRun &&
|
|
55
|
+
!options.force &&
|
|
56
|
+
!(await confirmRemoval(worktree))) {
|
|
57
|
+
options.stderr("Aborted\n");
|
|
54
58
|
return 1;
|
|
55
59
|
}
|
|
56
60
|
if (options.dryRun) {
|
|
@@ -58,14 +62,20 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
58
62
|
options.stdout(`${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}\n`);
|
|
59
63
|
}
|
|
60
64
|
else {
|
|
61
|
-
const desc = worktree.branch
|
|
65
|
+
const desc = worktree.branch
|
|
66
|
+
? `branch: ${worktree.branch}`
|
|
67
|
+
: "detached";
|
|
62
68
|
options.stdout(`Would remove worktree at ${worktree.path} (${desc})\n`);
|
|
63
69
|
}
|
|
64
70
|
return 0;
|
|
65
71
|
}
|
|
66
72
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
67
73
|
const hooks = extractHooks(config);
|
|
68
|
-
await runHook(hooks
|
|
74
|
+
await runHook(hooks["before-remove"], worktree.path, {
|
|
75
|
+
branch: worktree.branch ?? undefined,
|
|
76
|
+
path: worktree.path,
|
|
77
|
+
repo: basename(repository.repoRoot),
|
|
78
|
+
}, options.stderr);
|
|
69
79
|
try {
|
|
70
80
|
await removeWorktree(repository.repoRoot, worktree.path);
|
|
71
81
|
}
|
|
@@ -73,8 +83,9 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
73
83
|
if (!isWorktreeDirtyError(error)) {
|
|
74
84
|
throw error;
|
|
75
85
|
}
|
|
76
|
-
if (!options.force &&
|
|
77
|
-
|
|
86
|
+
if (!options.force &&
|
|
87
|
+
!(await confirmForceRemoveWorktree(worktree.path))) {
|
|
88
|
+
options.stderr("Aborted\n");
|
|
78
89
|
return 1;
|
|
79
90
|
}
|
|
80
91
|
try {
|
|
@@ -93,7 +104,8 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
93
104
|
if (!isBranchUnmergedError(error)) {
|
|
94
105
|
throw error;
|
|
95
106
|
}
|
|
96
|
-
if (options.force ||
|
|
107
|
+
if (options.force ||
|
|
108
|
+
(await confirmForceDeleteBranch(worktree.branch))) {
|
|
97
109
|
try {
|
|
98
110
|
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
99
111
|
}
|
|
@@ -118,10 +130,10 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
118
130
|
export const runRemoveCommand = createRemoveCommand();
|
|
119
131
|
async function defaultPromptForWorktree(worktrees) {
|
|
120
132
|
const choice = await select({
|
|
121
|
-
message:
|
|
133
|
+
message: "Choose a worktree to finish",
|
|
122
134
|
options: worktrees.map((worktree) => ({
|
|
123
135
|
hint: worktree.isCurrent ? `${worktree.path} (current)` : worktree.path,
|
|
124
|
-
label: worktree.branch ??
|
|
136
|
+
label: worktree.branch ?? "(detached)",
|
|
125
137
|
value: worktree.path,
|
|
126
138
|
})),
|
|
127
139
|
});
|
|
@@ -132,8 +144,8 @@ async function defaultConfirmRemoval(worktree) {
|
|
|
132
144
|
message: worktree.branch
|
|
133
145
|
? `Remove worktree and delete branch ${worktree.branch}?`
|
|
134
146
|
: `Remove detached worktree ${worktree.path}?`,
|
|
135
|
-
active:
|
|
136
|
-
inactive:
|
|
147
|
+
active: "Yes",
|
|
148
|
+
inactive: "No",
|
|
137
149
|
initialValue: true,
|
|
138
150
|
});
|
|
139
151
|
return !isCancel(choice) && choice;
|
package/dist/repo-registry.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile, realpath, writeFile } from
|
|
2
|
-
import { homedir } from
|
|
3
|
-
import { basename, dirname, join, resolve } from
|
|
4
|
-
import { GLOBAL_CONFIG_DIRECTORY } from
|
|
5
|
-
const REGISTRY_FILE_NAME =
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { GLOBAL_CONFIG_DIRECTORY } from "./config.js";
|
|
5
|
+
const REGISTRY_FILE_NAME = "repos.json";
|
|
6
6
|
const MAX_REGISTRY_ENTRIES = 100;
|
|
7
7
|
export function REGISTRY_FILE_PATH(home = homedir()) {
|
|
8
8
|
const configDir = process.env.GJI_CONFIG_DIR;
|
|
@@ -14,7 +14,7 @@ export function REGISTRY_FILE_PATH(home = homedir()) {
|
|
|
14
14
|
export async function loadRegistry(home = homedir()) {
|
|
15
15
|
const path = REGISTRY_FILE_PATH(home);
|
|
16
16
|
try {
|
|
17
|
-
const raw = await readFile(path,
|
|
17
|
+
const raw = await readFile(path, "utf8");
|
|
18
18
|
const parsed = JSON.parse(raw);
|
|
19
19
|
if (!Array.isArray(parsed))
|
|
20
20
|
return [];
|
|
@@ -47,7 +47,7 @@ export async function registerRepo(repoPath, home = homedir()) {
|
|
|
47
47
|
const filtered = existing.filter((e) => e.path !== canonicalRepoPath);
|
|
48
48
|
const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
|
|
49
49
|
await mkdir(dirname(registryPath), { recursive: true });
|
|
50
|
-
await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`,
|
|
50
|
+
await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
51
51
|
}
|
|
52
52
|
async function normalizeRegistryForWrite(entries) {
|
|
53
53
|
const normalized = [];
|
|
@@ -67,12 +67,12 @@ async function normalizeRegistryForWrite(entries) {
|
|
|
67
67
|
return normalized;
|
|
68
68
|
}
|
|
69
69
|
function isRegistryEntry(value) {
|
|
70
|
-
return (typeof value ===
|
|
70
|
+
return (typeof value === "object" &&
|
|
71
71
|
value !== null &&
|
|
72
|
-
|
|
73
|
-
typeof value.path ===
|
|
74
|
-
|
|
75
|
-
typeof value.name ===
|
|
76
|
-
|
|
77
|
-
typeof value.lastUsed ===
|
|
72
|
+
"path" in value &&
|
|
73
|
+
typeof value.path === "string" &&
|
|
74
|
+
"name" in value &&
|
|
75
|
+
typeof value.name === "string" &&
|
|
76
|
+
"lastUsed" in value &&
|
|
77
|
+
typeof value.lastUsed === "number");
|
|
78
78
|
}
|