@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.
Files changed (84) hide show
  1. package/README.md +26 -1
  2. package/dist/back.d.ts +1 -1
  3. package/dist/back.js +23 -17
  4. package/dist/clean.d.ts +1 -1
  5. package/dist/clean.js +44 -35
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +266 -165
  8. package/dist/completion.js +3 -3
  9. package/dist/config-command.js +5 -5
  10. package/dist/config.js +41 -35
  11. package/dist/conflict.d.ts +1 -1
  12. package/dist/conflict.js +14 -6
  13. package/dist/editor.js +29 -9
  14. package/dist/file-sync.d.ts +1 -0
  15. package/dist/file-sync.js +15 -11
  16. package/dist/git.d.ts +1 -1
  17. package/dist/git.js +21 -19
  18. package/dist/gji-bundle.mjs +1688 -865
  19. package/dist/go.d.ts +2 -2
  20. package/dist/go.js +39 -26
  21. package/dist/headless.js +1 -1
  22. package/dist/history-command.js +3 -3
  23. package/dist/history.js +12 -12
  24. package/dist/hooks.d.ts +3 -3
  25. package/dist/hooks.js +19 -19
  26. package/dist/index.js +13 -9
  27. package/dist/init.d.ts +5 -5
  28. package/dist/init.js +115 -103
  29. package/dist/install-prompt.d.ts +3 -3
  30. package/dist/install-prompt.js +57 -38
  31. package/dist/ls.d.ts +2 -2
  32. package/dist/ls.js +29 -29
  33. package/dist/new.d.ts +2 -2
  34. package/dist/new.js +97 -82
  35. package/dist/open.d.ts +2 -2
  36. package/dist/open.js +24 -21
  37. package/dist/package-manager.js +96 -45
  38. package/dist/pr.d.ts +2 -2
  39. package/dist/pr.js +47 -34
  40. package/dist/remove.d.ts +1 -1
  41. package/dist/remove.js +39 -27
  42. package/dist/repo-registry.js +14 -14
  43. package/dist/repo.js +29 -28
  44. package/dist/root.js +3 -3
  45. package/dist/run-hook.d.ts +6 -0
  46. package/dist/{trigger-hook.js → run-hook.js} +18 -8
  47. package/dist/shell-completion.d.ts +1 -1
  48. package/dist/shell-completion.js +67 -39
  49. package/dist/shell-handoff.js +2 -2
  50. package/dist/shell.d.ts +1 -1
  51. package/dist/shell.js +4 -4
  52. package/dist/status.d.ts +5 -5
  53. package/dist/status.js +23 -23
  54. package/dist/sync-files-command.d.ts +10 -0
  55. package/dist/sync-files-command.js +137 -0
  56. package/dist/sync.js +23 -15
  57. package/dist/warp.js +37 -33
  58. package/dist/worktree-info.d.ts +9 -9
  59. package/dist/worktree-info.js +31 -29
  60. package/dist/worktree-management.d.ts +1 -1
  61. package/dist/worktree-management.js +26 -11
  62. package/dist/worktree-prompts.js +5 -5
  63. package/man/man1/gji-back.1 +1 -1
  64. package/man/man1/gji-clean.1 +1 -1
  65. package/man/man1/gji-completion.1 +1 -1
  66. package/man/man1/gji-config.1 +1 -1
  67. package/man/man1/gji-go.1 +1 -1
  68. package/man/man1/gji-history.1 +1 -1
  69. package/man/man1/gji-init.1 +1 -1
  70. package/man/man1/gji-ls.1 +1 -1
  71. package/man/man1/gji-new.1 +1 -1
  72. package/man/man1/gji-open.1 +1 -1
  73. package/man/man1/gji-pr.1 +1 -1
  74. package/man/man1/gji-remove.1 +1 -1
  75. package/man/man1/gji-root.1 +1 -1
  76. package/man/man1/gji-run-hook.1 +9 -0
  77. package/man/man1/gji-status.1 +1 -1
  78. package/man/man1/gji-sync-files.1 +23 -0
  79. package/man/man1/gji-sync.1 +1 -1
  80. package/man/man1/gji-warp.1 +1 -1
  81. package/man/man1/gji.1 +10 -4
  82. package/package.json +8 -2
  83. package/dist/trigger-hook.d.ts +0 -6
  84. package/man/man1/gji-trigger-hook.1 +0 -9
@@ -1,70 +1,121 @@
1
- import { access, readdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
1
+ import { access, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
3
  const ENTRIES = [
4
4
  // JavaScript / TypeScript
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' },
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: '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
- { name: 'conda-lock', signals: ['conda-lock.yml'], command: 'conda-lock install' },
16
- { name: 'conda', signals: ['environment.yml'], command: 'conda env update --file environment.yml' },
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
- { name: 'renv', signals: ['renv.lock'], command: "Rscript -e 'renv::restore()'" },
26
+ {
27
+ name: "renv",
28
+ signals: ["renv.lock"],
29
+ command: "Rscript -e 'renv::restore()'",
30
+ },
19
31
  // Rust
20
- { name: 'cargo', signals: ['Cargo.lock'], command: 'cargo build' },
32
+ { name: "cargo", signals: ["Cargo.lock"], command: "cargo build" },
21
33
  // Go
22
- { name: 'go', signals: ['go.sum'], command: 'go mod download' },
34
+ { name: "go", signals: ["go.sum"], command: "go mod download" },
23
35
  // Ruby
24
- { name: 'bundler', signals: ['Gemfile.lock'], command: 'bundle install' },
36
+ { name: "bundler", signals: ["Gemfile.lock"], command: "bundle install" },
25
37
  // PHP
26
- { name: 'composer', signals: ['composer.lock'], command: 'composer install' },
38
+ { name: "composer", signals: ["composer.lock"], command: "composer install" },
27
39
  // Elixir / Erlang
28
- { name: 'mix', signals: ['mix.lock'], command: 'mix deps.get' },
29
- { name: 'rebar3', signals: ['rebar.lock'], command: 'rebar3 deps' },
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: 'dart', signals: ['pubspec.lock'], command: 'dart pub get' },
43
+ { name: "dart", signals: ["pubspec.lock"], command: "dart pub get" },
32
44
  // Java / Kotlin / Scala
33
- { name: 'maven', signals: ['pom.xml'], command: 'mvn install' },
34
- { name: 'gradle', signals: ['gradlew'], command: './gradlew build' },
35
- { name: 'gradle', signals: ['build.gradle', 'build.gradle.kts'], command: 'gradle build' },
36
- { name: 'sbt', signals: ['build.sbt'], command: 'sbt compile' },
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
- { name: 'dotnet', signals: ['*.sln', '*.csproj', '*.fsproj', '*.vbproj'], command: 'dotnet restore', glob: true },
54
+ {
55
+ name: "dotnet",
56
+ signals: ["*.sln", "*.csproj", "*.fsproj", "*.vbproj"],
57
+ command: "dotnet restore",
58
+ glob: true,
59
+ },
39
60
  // Swift
40
- { name: 'swift', signals: ['Package.swift'], command: 'swift package resolve' },
61
+ {
62
+ name: "swift",
63
+ signals: ["Package.swift"],
64
+ command: "swift package resolve",
65
+ },
41
66
  // Haskell
42
- { name: 'stack', signals: ['stack.yaml'], command: 'stack build' },
43
- { name: 'cabal', signals: ['cabal.project'], command: 'cabal install --only-dependencies' },
44
- { name: 'cabal', signals: ['*.cabal'], command: 'cabal install --only-dependencies', glob: true },
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: 'clojure', signals: ['deps.edn'], command: 'clojure -P' },
47
- { name: 'leiningen', signals: ['project.clj'], command: 'lein deps' },
80
+ { name: "clojure", signals: ["deps.edn"], command: "clojure -P" },
81
+ { name: "leiningen", signals: ["project.clj"], command: "lein deps" },
48
82
  // OCaml
49
- { name: 'dune', signals: ['dune-project'], command: 'dune build' },
83
+ { name: "dune", signals: ["dune-project"], command: "dune build" },
50
84
  // Julia
51
- { name: 'julia', signals: ['Manifest.toml'], command: "julia --project -e 'using Pkg; Pkg.instantiate()'" },
85
+ {
86
+ name: "julia",
87
+ signals: ["Manifest.toml"],
88
+ command: "julia --project -e 'using Pkg; Pkg.instantiate()'",
89
+ },
52
90
  // Nim
53
- { name: 'nimble', signals: ['*.nimble'], command: 'nimble install', glob: true },
91
+ {
92
+ name: "nimble",
93
+ signals: ["*.nimble"],
94
+ command: "nimble install",
95
+ glob: true,
96
+ },
54
97
  // Crystal
55
- { name: 'shards', signals: ['shard.yml'], command: 'shards install' },
98
+ { name: "shards", signals: ["shard.yml"], command: "shards install" },
56
99
  // Perl
57
- { name: 'cpanm', signals: ['cpanfile'], command: 'cpanm --installdeps .' },
100
+ { name: "cpanm", signals: ["cpanfile"], command: "cpanm --installdeps ." },
58
101
  // Zig
59
- { name: 'zig', signals: ['build.zig.zon'], command: 'zig build' },
102
+ { name: "zig", signals: ["build.zig.zon"], command: "zig build" },
60
103
  // C / C++
61
- { name: 'vcpkg', signals: ['vcpkg.json'], command: 'vcpkg install' },
62
- { name: 'conan', signals: ['conanfile.py', 'conanfile.txt'], command: 'conan install .' },
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: 'nix', signals: ['flake.nix'], command: 'nix develop' },
65
- { name: 'nix-shell', signals: ['shell.nix'], command: 'nix-shell' },
111
+ { name: "nix", signals: ["flake.nix"], command: "nix develop" },
112
+ { name: "nix-shell", signals: ["shell.nix"], command: "nix-shell" },
66
113
  // Terraform / OpenTofu
67
- { name: 'terraform', signals: ['terraform.lock.hcl'], command: 'terraform init' },
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 './conflict.js';
2
- import { type InstallPromptDependencies } from './install-prompt.js';
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 { mkdir } from 'node:fs/promises';
2
- import { basename, dirname } from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
- import { loadEffectiveConfig, resolveConfigString } from './config.js';
6
- import { syncFiles } from './file-sync.js';
7
- import { pathExists, promptForPathConflict } from './conflict.js';
8
- import { extractHooks, runHook } from './hooks.js';
9
- import { appendHistory } from './history.js';
10
- import { isHeadless } from './headless.js';
11
- import { maybeRunInstallPrompt } from './install-prompt.js';
12
- import { detectRepository, resolveWorktreePath } from './repo.js';
13
- import { writeShellOutput } from './shell-handoff.js';
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 = 'GJI_PR_OUTPUT_FILE';
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, 'worktreePath');
46
- const configuredBasePath = rawBasePath?.startsWith('/') || rawBasePath?.startsWith('~') ? rawBasePath : undefined;
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 === 'reuse') {
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
- ? ['worktree', 'add', worktreePath, branchName]
96
- : ['worktree', 'add', '-b', branchName, worktreePath, remoteRef];
97
- await execFileAsync('git', worktreeArgs, { cwd: repository.repoRoot });
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 === 'string')
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.afterCreate, worktreePath, { branch: branchName, path: worktreePath, repo: basename(repository.repoRoot) }, options.stderr);
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('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], { cwd: repoRoot });
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('git', ['fetch', 'origin', `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
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 = ['github', 'gitlab', 'bitbucket'];
152
+ const allForges = [
153
+ "github",
154
+ "gitlab",
155
+ "bitbucket",
156
+ ];
147
157
  const preferredForge = detectPullRequestForge(input);
148
- const orderedForges = preferredForge === 'unknown'
158
+ const orderedForges = preferredForge === "unknown"
149
159
  ? allForges
150
- : [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
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 'bitbucket';
168
+ return "bitbucket";
156
169
  }
157
170
  if (/\/merge_requests\/\d+/.test(input)) {
158
- return 'gitlab';
171
+ return "gitlab";
159
172
  }
160
173
  if (/\/pull\/\d+/.test(input)) {
161
- return 'github';
174
+ return "github";
162
175
  }
163
- return 'unknown';
176
+ return "unknown";
164
177
  }
165
178
  function sourceRefForForge(forge, prNumber) {
166
179
  switch (forge) {
167
- case 'bitbucket':
180
+ case "bitbucket":
168
181
  return `refs/pull-requests/${prNumber}/from`;
169
- case 'github':
182
+ case "github":
170
183
  return `refs/pull/${prNumber}/head`;
171
- case 'gitlab':
184
+ case "gitlab":
172
185
  return `refs/merge-requests/${prNumber}/head`;
173
186
  }
174
187
  }
package/dist/remove.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WorktreeEntry } from './repo.js';
1
+ import { type WorktreeEntry } from "./repo.js";
2
2
  export interface RemoveCommandOptions {
3
3
  branch?: string;
4
4
  cwd: string;
package/dist/remove.js CHANGED
@@ -1,26 +1,27 @@
1
- import { basename } from 'node:path';
2
- import { confirm, isCancel, select } from '@clack/prompts';
3
- import { loadEffectiveConfig } from './config.js';
4
- import { extractHooks, runHook } from './hooks.js';
5
- import { isHeadless } from './headless.js';
6
- import { sortByCurrentFirst } from './repo.js';
7
- import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
8
- import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree } from './worktree-prompts.js';
9
- import { writeShellOutput } from './shell-handoff.js';
10
- const REMOVE_OUTPUT_FILE_ENV = 'GJI_REMOVE_OUTPUT_FILE';
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 ?? defaultConfirmForceRemoveWorktree;
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, 'No linked worktrees to finish');
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 = 'branch argument is required';
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 ?? (await promptForWorktree(sortByCurrentFirst(linkedWorktrees)));
33
+ const selection = options.branch ??
34
+ (await promptForWorktree(sortByCurrentFirst(linkedWorktrees)));
33
35
  if (!selection) {
34
- options.stderr('Aborted\n');
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 = '--force is required';
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 && !options.force && !(await confirmRemoval(worktree))) {
53
- options.stderr('Aborted\n');
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 ? `branch: ${worktree.branch}` : 'detached';
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.beforeRemove, worktree.path, { branch: worktree.branch ?? undefined, path: worktree.path, repo: basename(repository.repoRoot) }, options.stderr);
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 && !(await confirmForceRemoveWorktree(worktree.path))) {
77
- options.stderr('Aborted\n');
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 || (await confirmForceDeleteBranch(worktree.branch))) {
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: 'Choose a worktree to finish',
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 ?? '(detached)',
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: 'Yes',
136
- inactive: 'No',
147
+ active: "Yes",
148
+ inactive: "No",
137
149
  initialValue: true,
138
150
  });
139
151
  return !isCancel(choice) && choice;
@@ -1,8 +1,8 @@
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';
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, 'utf8');
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`, 'utf8');
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 === 'object' &&
70
+ return (typeof value === "object" &&
71
71
  value !== null &&
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');
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
  }