@solaqua/gji 0.12.4 → 0.13.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.
Files changed (214) hide show
  1. package/README.md +12 -0
  2. package/dist/application/worktree/catalog.d.ts +31 -0
  3. package/dist/application/worktree/catalog.js +88 -0
  4. package/dist/application/worktree/context-card.d.ts +14 -0
  5. package/dist/application/worktree/context-card.js +12 -0
  6. package/dist/{go-resolver.d.ts → application/worktree/go-resolution.d.ts} +12 -4
  7. package/dist/{go-resolver.js → application/worktree/go-resolution.js} +46 -44
  8. package/dist/application/worktree/read-models.d.ts +31 -0
  9. package/dist/application/worktree/sources.d.ts +7 -0
  10. package/dist/{worktree-sources.js → application/worktree/sources.js} +12 -24
  11. package/dist/{back.d.ts → cli/commands/back.d.ts} +3 -1
  12. package/dist/{back.js → cli/commands/back.js} +8 -6
  13. package/dist/{clean.d.ts → cli/commands/clean.d.ts} +4 -2
  14. package/dist/{clean.js → cli/commands/clean.js} +32 -24
  15. package/dist/{completion.js → cli/commands/completion.js} +2 -2
  16. package/dist/{config-command.d.ts → cli/commands/config-command.d.ts} +2 -0
  17. package/dist/{config-command.js → cli/commands/config-command.js} +7 -5
  18. package/dist/{doctor.d.ts → cli/commands/doctor.d.ts} +2 -0
  19. package/dist/{doctor.js → cli/commands/doctor.js} +23 -25
  20. package/dist/{done.d.ts → cli/commands/done.d.ts} +2 -0
  21. package/dist/{done.js → cli/commands/done.js} +26 -23
  22. package/dist/{go.d.ts → cli/commands/go.d.ts} +6 -1
  23. package/dist/{go.js → cli/commands/go.js} +57 -41
  24. package/dist/{history-command.d.ts → cli/commands/history-command.d.ts} +2 -0
  25. package/dist/{history-command.js → cli/commands/history-command.js} +3 -1
  26. package/dist/{hub.d.ts → cli/commands/hub.d.ts} +7 -3
  27. package/dist/{hub.js → cli/commands/hub.js} +23 -33
  28. package/dist/{init.d.ts → cli/commands/init.d.ts} +3 -1
  29. package/dist/{init.js → cli/commands/init.js} +37 -34
  30. package/dist/{ls.d.ts → cli/commands/ls.d.ts} +4 -2
  31. package/dist/{ls.js → cli/commands/ls.js} +8 -5
  32. package/dist/{new.d.ts → cli/commands/new.d.ts} +6 -2
  33. package/dist/{new.js → cli/commands/new.js} +237 -109
  34. package/dist/{open.d.ts → cli/commands/open.d.ts} +4 -2
  35. package/dist/{open.js → cli/commands/open.js} +19 -12
  36. package/dist/{pr-open.d.ts → cli/commands/pr-open.d.ts} +4 -2
  37. package/dist/{pr-open.js → cli/commands/pr-open.js} +20 -18
  38. package/dist/{pr.d.ts → cli/commands/pr.d.ts} +5 -3
  39. package/dist/{pr.js → cli/commands/pr.js} +34 -41
  40. package/dist/{remove.d.ts → cli/commands/remove.d.ts} +4 -2
  41. package/dist/{remove.js → cli/commands/remove.js} +23 -17
  42. package/dist/{root.d.ts → cli/commands/root.d.ts} +2 -0
  43. package/dist/{root.js → cli/commands/root.js} +4 -2
  44. package/dist/{run-hook.d.ts → cli/commands/run-hook.d.ts} +2 -0
  45. package/dist/{run-hook.js → cli/commands/run-hook.js} +7 -4
  46. package/dist/{status.d.ts → cli/commands/status.d.ts} +2 -0
  47. package/dist/{status.js → cli/commands/status.js} +10 -7
  48. package/dist/{sync-files-command.d.ts → cli/commands/sync-files-command.d.ts} +2 -0
  49. package/dist/{sync-files-command.js → cli/commands/sync-files-command.js} +11 -9
  50. package/dist/{sync.d.ts → cli/commands/sync.d.ts} +2 -0
  51. package/dist/{sync.js → cli/commands/sync.js} +7 -4
  52. package/dist/{task-command.d.ts → cli/commands/task-command.d.ts} +2 -0
  53. package/dist/{task-command.js → cli/commands/task-command.js} +4 -2
  54. package/dist/{undo.d.ts → cli/commands/undo.d.ts} +8 -6
  55. package/dist/{undo.js → cli/commands/undo.js} +44 -34
  56. package/dist/{warp.d.ts → cli/commands/warp.d.ts} +5 -2
  57. package/dist/{warp.js → cli/commands/warp.js} +20 -10
  58. package/dist/cli/dependencies.d.ts +114 -0
  59. package/dist/cli/dependencies.js +118 -0
  60. package/dist/{cli.d.ts → cli/program.d.ts} +4 -2
  61. package/dist/{cli.js → cli/program.js} +108 -124
  62. package/dist/domain/repository/context.d.ts +6 -0
  63. package/dist/domain/repository/context.js +1 -0
  64. package/dist/domain/repository/registry.d.ts +5 -0
  65. package/dist/domain/repository/registry.js +1 -0
  66. package/dist/domain/shared/concurrency.d.ts +1 -0
  67. package/dist/domain/shared/concurrency.js +15 -0
  68. package/dist/domain/worktree/matching.d.ts +4 -0
  69. package/dist/domain/worktree/matching.js +79 -0
  70. package/dist/domain/worktree/policy.d.ts +2 -0
  71. package/dist/domain/worktree/policy.js +58 -0
  72. package/dist/domain/worktree/pr-reference.d.ts +1 -0
  73. package/dist/domain/worktree/pr-reference.js +9 -0
  74. package/dist/{worktree-source.d.ts → domain/worktree/source.d.ts} +1 -1
  75. package/dist/domain/worktree/source.js +1 -0
  76. package/dist/domain/worktree/types.d.ts +5 -0
  77. package/dist/domain/worktree/types.js +1 -0
  78. package/dist/gji-bundle.mjs +7100 -6265
  79. package/dist/index.js +3 -3
  80. package/dist/{dependency-bootstrap.d.ts → infrastructure/bootstrap/dependency-bootstrap.d.ts} +3 -30
  81. package/dist/{dependency-bootstrap.js → infrastructure/bootstrap/dependency-bootstrap.js} +4 -4
  82. package/dist/infrastructure/git/health.d.ts +9 -0
  83. package/dist/infrastructure/git/health.js +44 -0
  84. package/dist/infrastructure/git/refs.d.ts +6 -0
  85. package/dist/{git.js → infrastructure/git/refs.js} +1 -51
  86. package/dist/infrastructure/git/runner.d.ts +2 -0
  87. package/dist/infrastructure/git/runner.js +16 -0
  88. package/dist/{editor.d.ts → infrastructure/integrations/editor.d.ts} +2 -6
  89. package/dist/{pull-requests.d.ts → infrastructure/integrations/pull-requests.d.ts} +3 -11
  90. package/dist/{config.d.ts → infrastructure/persistence/config.d.ts} +3 -17
  91. package/dist/{config.js → infrastructure/persistence/config.js} +2 -17
  92. package/dist/{history.d.ts → infrastructure/persistence/history.d.ts} +2 -5
  93. package/dist/{task.js → infrastructure/persistence/task.js} +1 -1
  94. package/dist/infrastructure/process/command-runner.d.ts +3 -0
  95. package/dist/{hooks.d.ts → infrastructure/process/hooks.d.ts} +2 -12
  96. package/dist/infrastructure/repository/adapters.d.ts +7 -0
  97. package/dist/infrastructure/repository/adapters.js +23 -0
  98. package/dist/infrastructure/repository/context.d.ts +3 -0
  99. package/dist/infrastructure/repository/context.js +20 -0
  100. package/dist/infrastructure/repository/refs.d.ts +3 -0
  101. package/dist/infrastructure/repository/refs.js +37 -0
  102. package/dist/{repo-registry.d.ts → infrastructure/repository/registry.d.ts} +1 -5
  103. package/dist/{repo-registry.js → infrastructure/repository/registry.js} +1 -1
  104. package/dist/infrastructure/repository/worktrees.d.ts +2 -0
  105. package/dist/infrastructure/repository/worktrees.js +48 -0
  106. package/dist/{uv-validation.d.ts → infrastructure/validation/uv.d.ts} +1 -1
  107. package/dist/{uv-validation.js → infrastructure/validation/uv.js} +1 -1
  108. package/dist/infrastructure/worktree/bootstrap.d.ts +3 -0
  109. package/dist/{worktree-bootstrap.js → infrastructure/worktree/bootstrap.js} +7 -10
  110. package/dist/infrastructure/worktree/info.d.ts +6 -0
  111. package/dist/infrastructure/worktree/info.js +61 -0
  112. package/dist/{worktree-management.d.ts → infrastructure/worktree/lifecycle.d.ts} +0 -6
  113. package/dist/{worktree-management.js → infrastructure/worktree/lifecycle.js} +1 -9
  114. package/dist/ports/bootstrap.d.ts +61 -0
  115. package/dist/ports/bootstrap.js +1 -0
  116. package/dist/ports/config.d.ts +21 -0
  117. package/dist/ports/config.js +17 -0
  118. package/dist/ports/editor.d.ts +6 -0
  119. package/dist/ports/editor.js +1 -0
  120. package/dist/ports/git.d.ts +8 -0
  121. package/dist/ports/git.js +1 -0
  122. package/dist/ports/history.d.ts +5 -0
  123. package/dist/ports/history.js +1 -0
  124. package/dist/ports/hooks.d.ts +12 -0
  125. package/dist/ports/hooks.js +1 -0
  126. package/dist/{command-runner.d.ts → ports/process.d.ts} +0 -1
  127. package/dist/ports/process.js +1 -0
  128. package/dist/ports/pull-requests.d.ts +11 -0
  129. package/dist/ports/pull-requests.js +1 -0
  130. package/dist/ports/repository.d.ts +17 -0
  131. package/dist/ports/repository.js +1 -0
  132. package/dist/{bootstrap-output.d.ts → presentation/bootstrap/output.d.ts} +1 -1
  133. package/dist/presentation/bootstrap/preview.d.ts +2 -0
  134. package/dist/{bootstrap-preview.js → presentation/bootstrap/preview.js} +0 -4
  135. package/dist/{shell-completion.js → presentation/shell/completion.js} +5 -4
  136. package/dist/presentation/terminal/context-card.d.ts +2 -0
  137. package/dist/{context-card.js → presentation/terminal/context-card.js} +3 -12
  138. package/dist/presentation/worktree/format.d.ts +4 -0
  139. package/dist/presentation/worktree/format.js +41 -0
  140. package/dist/{worktree-picker.d.ts → presentation/worktree/picker.d.ts} +7 -11
  141. package/dist/{worktree-picker.js → presentation/worktree/picker.js} +36 -191
  142. package/man/man1/gji-back.1 +1 -1
  143. package/man/man1/gji-clean.1 +1 -1
  144. package/man/man1/gji-completion.1 +1 -1
  145. package/man/man1/gji-config.1 +1 -1
  146. package/man/man1/gji-doctor.1 +1 -1
  147. package/man/man1/gji-done.1 +1 -1
  148. package/man/man1/gji-go.1 +1 -1
  149. package/man/man1/gji-history.1 +1 -1
  150. package/man/man1/gji-init.1 +1 -1
  151. package/man/man1/gji-ls.1 +1 -1
  152. package/man/man1/gji-new.1 +6 -3
  153. package/man/man1/gji-open.1 +1 -1
  154. package/man/man1/gji-pr.1 +1 -1
  155. package/man/man1/gji-remove.1 +1 -1
  156. package/man/man1/gji-root.1 +1 -1
  157. package/man/man1/gji-run-hook.1 +1 -1
  158. package/man/man1/gji-status.1 +1 -1
  159. package/man/man1/gji-sync-files.1 +1 -1
  160. package/man/man1/gji-sync.1 +1 -1
  161. package/man/man1/gji-task.1 +1 -1
  162. package/man/man1/gji-undo.1 +1 -1
  163. package/man/man1/gji-warp.1 +1 -1
  164. package/man/man1/gji.1 +2 -2
  165. package/package.json +1 -1
  166. package/dist/bootstrap-preview.d.ts +0 -9
  167. package/dist/context-card.d.ts +0 -1
  168. package/dist/format-bytes.d.ts +0 -1
  169. package/dist/format-bytes.js +0 -14
  170. package/dist/git.d.ts +0 -18
  171. package/dist/repo.d.ts +0 -20
  172. package/dist/repo.js +0 -161
  173. package/dist/worktree-bootstrap.d.ts +0 -24
  174. package/dist/worktree-info.d.ts +0 -37
  175. package/dist/worktree-info.js +0 -123
  176. package/dist/worktree-sources.d.ts +0 -4
  177. /package/dist/{worktree-source.js → application/worktree/read-models.js} +0 -0
  178. /package/dist/{completion.d.ts → cli/commands/completion.d.ts} +0 -0
  179. /package/dist/{headless.d.ts → cli/runtime/headless.d.ts} +0 -0
  180. /package/dist/{headless.js → cli/runtime/headless.js} +0 -0
  181. /package/dist/{paths.d.ts → domain/shared/paths.d.ts} +0 -0
  182. /package/dist/{paths.js → domain/shared/paths.js} +0 -0
  183. /package/dist/{file-sync.d.ts → infrastructure/filesystem/file-sync.d.ts} +0 -0
  184. /package/dist/{file-sync.js → infrastructure/filesystem/file-sync.js} +0 -0
  185. /package/dist/{fs-utils.d.ts → infrastructure/filesystem/fs-utils.d.ts} +0 -0
  186. /package/dist/{fs-utils.js → infrastructure/filesystem/fs-utils.js} +0 -0
  187. /package/dist/{safe-destination.d.ts → infrastructure/filesystem/safe-destination.d.ts} +0 -0
  188. /package/dist/{safe-destination.js → infrastructure/filesystem/safe-destination.js} +0 -0
  189. /package/dist/{browser.d.ts → infrastructure/integrations/browser.d.ts} +0 -0
  190. /package/dist/{browser.js → infrastructure/integrations/browser.js} +0 -0
  191. /package/dist/{editor.js → infrastructure/integrations/editor.js} +0 -0
  192. /package/dist/{pull-requests.js → infrastructure/integrations/pull-requests.js} +0 -0
  193. /package/dist/{history.js → infrastructure/persistence/history.js} +0 -0
  194. /package/dist/{slots.d.ts → infrastructure/persistence/slots.d.ts} +0 -0
  195. /package/dist/{slots.js → infrastructure/persistence/slots.js} +0 -0
  196. /package/dist/{task.d.ts → infrastructure/persistence/task.d.ts} +0 -0
  197. /package/dist/{command-runner.js → infrastructure/process/command-runner.js} +0 -0
  198. /package/dist/{hooks.js → infrastructure/process/hooks.js} +0 -0
  199. /package/dist/{bootstrap-output.js → presentation/bootstrap/output.js} +0 -0
  200. /package/dist/{conflict.d.ts → presentation/prompts/path-conflict.d.ts} +0 -0
  201. /package/dist/{conflict.js → presentation/prompts/path-conflict.js} +0 -0
  202. /package/dist/{shell-completion.d.ts → presentation/shell/completion.d.ts} +0 -0
  203. /package/dist/{shell-handoff.d.ts → presentation/shell/handoff.d.ts} +0 -0
  204. /package/dist/{shell-handoff.js → presentation/shell/handoff.js} +0 -0
  205. /package/dist/{shell-setup.d.ts → presentation/shell/setup.d.ts} +0 -0
  206. /package/dist/{shell-setup.js → presentation/shell/setup.js} +0 -0
  207. /package/dist/{shell.d.ts → presentation/shell/shell.d.ts} +0 -0
  208. /package/dist/{shell.js → presentation/shell/shell.js} +0 -0
  209. /package/dist/{navigation-output.d.ts → presentation/terminal/navigation.d.ts} +0 -0
  210. /package/dist/{navigation-output.js → presentation/terminal/navigation.js} +0 -0
  211. /package/dist/{terminal-text.d.ts → presentation/terminal/text.d.ts} +0 -0
  212. /package/dist/{terminal-text.js → presentation/terminal/text.js} +0 -0
  213. /package/dist/{worktree-prompts.d.ts → presentation/worktree/prompts.d.ts} +0 -0
  214. /package/dist/{worktree-prompts.js → presentation/worktree/prompts.js} +0 -0
@@ -1,9 +1,11 @@
1
- import { type PathConflictChoice } from "./conflict.js";
2
- import { type BootstrapCommandRunner } from "./dependency-bootstrap.js";
1
+ import type { CommandRunner as BootstrapCommandRunner } from "../../ports/process.js";
2
+ import { type PathConflictChoice } from "../../presentation/prompts/path-conflict.js";
3
+ import { type CliRuntime } from "../dependencies.js";
3
4
  export type { PathConflictChoice };
4
5
  export type NewWorktreeMode = "create" | "checkout" | "track";
5
6
  export interface NewCommandOptions {
6
7
  branch?: string;
8
+ branchOnly?: boolean;
7
9
  copy?: boolean;
8
10
  cwd: string;
9
11
  detached?: boolean;
@@ -22,7 +24,9 @@ export interface NewCommandOptions {
22
24
  stderr: (chunk: string) => void;
23
25
  stdout: (chunk: string) => void;
24
26
  task?: string;
27
+ runtime?: NewRuntime;
25
28
  }
29
+ type NewRuntime = CliRuntime<"bootstrap" | "config" | "git" | "history" | "integrations" | "repositoryContext" | "tasks">;
26
30
  export interface NewCommandDependencies {
27
31
  createBranchPlaceholder: () => string;
28
32
  promptForBranch: (placeholder: string) => Promise<string | null>;
@@ -1,33 +1,31 @@
1
- import { execFile } from "node:child_process";
2
1
  import { access, mkdir } from "node:fs/promises";
3
2
  import { dirname, resolve } from "node:path";
4
- import { promisify } from "node:util";
5
3
  import { confirm, isCancel, text } from "@clack/prompts";
6
- import { createBootstrapReporter } from "./bootstrap-output.js";
7
- import { createDependencyBootstrapPreview, formatDependencyBootstrapPreview, } from "./bootstrap-preview.js";
8
- import { loadEffectiveConfig, resolveConfigString, } from "./config.js";
9
- import { pathExists, promptForPathConflict, } from "./conflict.js";
10
- import { resolveDependencyBootstrapMode, } from "./dependency-bootstrap.js";
11
- import { defaultSpawnEditor, EDITORS } from "./editor.js";
12
- import { resolveRemoteBase, runGit } from "./git.js";
13
- import { isHeadless } from "./headless.js";
14
- import { recordWorktreeUsage } from "./history.js";
15
- import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
16
- import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
17
- import { writeShellOutput } from "./shell-handoff.js";
18
- import { writeTask } from "./task.js";
19
- import { bootstrapWorktree } from "./worktree-bootstrap.js";
20
- const execFileAsync = promisify(execFile);
4
+ import { resolveWorktreePath, validateBranchName, } from "../../domain/worktree/policy.js";
5
+ import { createBootstrapReporter } from "../../presentation/bootstrap/output.js";
6
+ import { formatDependencyBootstrapPreview } from "../../presentation/bootstrap/preview.js";
7
+ import { pathExists, promptForPathConflict, } from "../../presentation/prompts/path-conflict.js";
8
+ import { writeShellOutput } from "../../presentation/shell/handoff.js";
9
+ import { createNavigationRepository, createNavigationTarget, } from "../../presentation/terminal/navigation.js";
10
+ import { defaultCliDependencies, } from "../dependencies.js";
11
+ import { isHeadless } from "../runtime/headless.js";
21
12
  const NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
22
13
  export function createNewCommand(dependencies = {}) {
23
14
  const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
24
15
  const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
25
16
  const promptForFetchFailure = dependencies.promptForFetchFailure ?? defaultPromptForFetchFailure;
26
17
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
27
- const spawnEditor = dependencies.spawnEditor ?? defaultSpawnEditor;
28
18
  return async function runNewCommand(options) {
19
+ const runtime = options.runtime ?? defaultCliDependencies;
20
+ const spawnEditor = dependencies.spawnEditor ?? runtime.integrations.defaultSpawnEditor;
29
21
  if (options.copy && !options.take)
30
22
  return emitNewError(options, "--copy requires --take");
23
+ if (options.branchOnly) {
24
+ const incompatibleOption = findBranchOnlyIncompatibleOption(options);
25
+ if (incompatibleOption) {
26
+ return emitNewError(options, `${incompatibleOption} cannot be used with --branch-only`);
27
+ }
28
+ }
31
29
  if (options.detached && options.fromCurrent) {
32
30
  const message = "--from-current cannot be used with --detached";
33
31
  if (options.json) {
@@ -38,10 +36,10 @@ export function createNewCommand(dependencies = {}) {
38
36
  }
39
37
  return 1;
40
38
  }
41
- const repository = await detectRepository(options.cwd);
39
+ const repository = await runtime.repositoryContext.detectRepository(options.cwd);
42
40
  let config;
43
41
  try {
44
- config = await loadEffectiveConfig(repository.repoRoot, undefined, options.json ? undefined : options.stderr);
42
+ config = await runtime.config.loadEffectiveConfig(repository.repoRoot, undefined, options.json ? undefined : options.stderr);
45
43
  }
46
44
  catch (error) {
47
45
  return emitNewError(options, error instanceof Error ? error.message : String(error));
@@ -86,7 +84,10 @@ export function createNewCommand(dependencies = {}) {
86
84
  return 1;
87
85
  }
88
86
  }
89
- const rawBasePath = resolveConfigString(config, "worktreePath");
87
+ if (options.branchOnly) {
88
+ return runBranchOnly(options, runtime, repository, config, rawBranch);
89
+ }
90
+ const rawBasePath = runtime.config.resolveConfigString(config, "worktreePath");
90
91
  const configuredBasePath = rawBasePath?.startsWith("/") || rawBasePath?.startsWith("~")
91
92
  ? rawBasePath
92
93
  : undefined;
@@ -102,7 +103,12 @@ export function createNewCommand(dependencies = {}) {
102
103
  if (options.force) {
103
104
  if (!options.dryRun) {
104
105
  try {
105
- await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
106
+ await runtime.git.runGit(repository.repoRoot, [
107
+ "worktree",
108
+ "remove",
109
+ "--force",
110
+ worktreePath,
111
+ ]);
106
112
  }
107
113
  catch (err) {
108
114
  if (!isNotRegisteredWorktreeError(err)) {
@@ -117,9 +123,11 @@ export function createNewCommand(dependencies = {}) {
117
123
  }
118
124
  if (!options.detached) {
119
125
  try {
120
- await execFileAsync("git", ["branch", "-D", worktreeName], {
121
- cwd: repository.repoRoot,
122
- });
126
+ await runtime.git.runGit(repository.repoRoot, [
127
+ "branch",
128
+ "-D",
129
+ worktreeName,
130
+ ]);
123
131
  }
124
132
  catch {
125
133
  // Branch may not exist; proceed anyway.
@@ -142,7 +150,7 @@ export function createNewCommand(dependencies = {}) {
142
150
  else {
143
151
  const choice = await prompt(worktreePath);
144
152
  if (choice === "reuse") {
145
- await recordWorktreeUsage(worktreePath, worktreeName);
153
+ await runtime.history.recordWorktreeUsage(worktreePath, worktreeName);
146
154
  await writeOutput(worktreePath, options.stdout, options.outputEnv);
147
155
  return 0;
148
156
  }
@@ -150,13 +158,13 @@ export function createNewCommand(dependencies = {}) {
150
158
  return 1;
151
159
  }
152
160
  }
153
- const dependencyMode = resolveDependencyBootstrapMode(config.dependencyBootstrap, options.noInstall);
161
+ const dependencyMode = runtime.bootstrap.resolveMode(config.dependencyBootstrap, options.noInstall);
154
162
  if (options.dryRun) {
155
163
  if (options.take) {
156
- const changedFiles = await listTakeFiles(options.cwd);
157
- const submoduleFiles = await listSubmoduleFiles(options.cwd, changedFiles);
164
+ const changedFiles = await listTakeFiles(options.cwd, runtime.git.runGitRaw);
165
+ const submoduleFiles = await listSubmoduleFiles(options.cwd, changedFiles, runtime.git.runGitRaw);
158
166
  const transferableFiles = changedFiles.filter((file) => !submoduleFiles.includes(file));
159
- const ignoredFiles = await listIgnoredFiles(options.cwd);
167
+ const ignoredFiles = await listIgnoredFiles(options.cwd, runtime.git.runGitRaw);
160
168
  if (transferableFiles.length === 0)
161
169
  return emitNewError(options, submoduleFiles.length > 0
162
170
  ? "nothing to take: submodule changes are not supported"
@@ -170,7 +178,7 @@ export function createNewCommand(dependencies = {}) {
170
178
  }
171
179
  return 0;
172
180
  }
173
- const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(dependencyMode, {
181
+ const dryRunDependencyBootstrap = await runtime.bootstrap.preview(dependencyMode, {
174
182
  currentRoot: repository.currentRoot,
175
183
  repoRoot: repository.repoRoot,
176
184
  cargoBuildCommand: config.dependencyBuildCommand,
@@ -187,7 +195,8 @@ export function createNewCommand(dependencies = {}) {
187
195
  }
188
196
  else {
189
197
  const resolvedEditor = options.open
190
- ? (options.editor ?? resolveConfigString(config, "editor"))
198
+ ? (options.editor ??
199
+ runtime.config.resolveConfigString(config, "editor"))
191
200
  : undefined;
192
201
  const openNote = resolvedEditor
193
202
  ? `, then open in ${resolvedEditor}`
@@ -198,20 +207,20 @@ export function createNewCommand(dependencies = {}) {
198
207
  }
199
208
  await mkdir(dirname(worktreePath), { recursive: true });
200
209
  const startPoint = options.fromCurrent && !options.detached
201
- ? await resolveCurrentWorktreeHead(options.cwd)
210
+ ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit)
202
211
  : undefined;
203
212
  const takeStartPoint = options.take
204
- ? await resolveCurrentWorktreeHead(options.cwd)
213
+ ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit)
205
214
  : undefined;
206
215
  const changedTakeFiles = options.take
207
- ? await listTakeFiles(options.cwd)
216
+ ? await listTakeFiles(options.cwd, runtime.git.runGitRaw)
208
217
  : [];
209
218
  const submoduleFiles = options.take
210
- ? await listSubmoduleFiles(options.cwd, changedTakeFiles)
219
+ ? await listSubmoduleFiles(options.cwd, changedTakeFiles, runtime.git.runGitRaw)
211
220
  : [];
212
221
  const takeFiles = changedTakeFiles.filter((file) => !submoduleFiles.includes(file));
213
222
  const takeUntracked = options.take
214
- ? await countUntrackedFiles(options.cwd)
223
+ ? await countUntrackedFiles(options.cwd, runtime.git.runGitRaw)
215
224
  : 0;
216
225
  if (options.take && takeFiles.length === 0)
217
226
  return emitNewError(options, submoduleFiles.length > 0
@@ -219,7 +228,7 @@ export function createNewCommand(dependencies = {}) {
219
228
  : "nothing to take: working tree is clean");
220
229
  if (options.take && submoduleFiles.length > 0)
221
230
  options.stderr(`Warning: submodule changes are not transferred: ${submoduleFiles.join(", ")}\n`);
222
- const existingLocalBranch = await localBranchExists(repository.repoRoot, worktreeName);
231
+ const existingLocalBranch = await localBranchExists(repository.repoRoot, worktreeName, runtime);
223
232
  const shouldRefreshBase = !options.noFetch &&
224
233
  !options.detached &&
225
234
  !options.fromCurrent &&
@@ -228,17 +237,17 @@ export function createNewCommand(dependencies = {}) {
228
237
  options.mode !== "checkout" &&
229
238
  !existingLocalBranch;
230
239
  const freshBaseRef = shouldRefreshBase
231
- ? await resolveFreshBaseRef(repository.repoRoot, config, options, promptForFetchFailure)
240
+ ? await resolveFreshBaseRef(repository.repoRoot, config, options, promptForFetchFailure, runtime)
232
241
  : undefined;
233
242
  if (freshBaseRef === null)
234
243
  return 1;
235
244
  let stashSha = null;
236
245
  if (options.take) {
237
246
  try {
238
- const inProgressState = await detectInProgressGitState(options.cwd);
247
+ const inProgressState = await detectInProgressGitState(options.cwd, runtime.git.runGit);
239
248
  if (inProgressState)
240
249
  return emitNewError(options, `cannot take changes while Git is in progress (${inProgressState})`);
241
- stashSha = await createTakeStash(options.cwd, worktreeName);
250
+ stashSha = await createTakeStash(options.cwd, worktreeName, runtime.git.runGit);
242
251
  }
243
252
  catch (error) {
244
253
  return emitNewError(options, `could not stash changes safely: ${toExecMessage(error)}`);
@@ -279,17 +288,22 @@ export function createNewCommand(dependencies = {}) {
279
288
  : []),
280
289
  ];
281
290
  try {
282
- await execFileAsync("git", gitArgs, { cwd: repository.repoRoot });
291
+ await runtime.git.runGit(repository.repoRoot, gitArgs);
283
292
  }
284
293
  catch (error) {
285
294
  if (stashSha)
286
- await restoreTakeStash(options.cwd, stashSha, options.stderr);
295
+ await restoreTakeStash(options.cwd, stashSha, options.stderr, runtime.git.runGit);
287
296
  return emitNewError(options, `failed to create worktree: ${toExecMessage(error)}`);
288
297
  }
289
298
  if (stashSha &&
290
- !(await applyTakeStash(worktreePath, options.cwd, stashSha, !!options.copy, options.stderr))) {
299
+ !(await applyTakeStash(worktreePath, options.cwd, stashSha, !!options.copy, options.stderr, runtime.git.runGit))) {
291
300
  try {
292
- await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repository.repoRoot });
301
+ await runtime.git.runGit(repository.repoRoot, [
302
+ "worktree",
303
+ "remove",
304
+ "--force",
305
+ worktreePath,
306
+ ]);
293
307
  }
294
308
  catch {
295
309
  /* rollback best effort */
@@ -298,13 +312,13 @@ export function createNewCommand(dependencies = {}) {
298
312
  }
299
313
  if (options.task !== undefined) {
300
314
  try {
301
- await writeTask(worktreePath, options.task);
315
+ await runtime.tasks.writeTask(worktreePath, options.task);
302
316
  }
303
317
  catch (error) {
304
318
  options.stderr(`Warning: could not save task metadata: ${error instanceof Error ? error.message : String(error)}\n`);
305
319
  }
306
320
  }
307
- const bootstrap = await bootstrapWorktree({
321
+ const bootstrap = await runtime.bootstrap.bootstrapWorktree({
308
322
  branch: worktreeName,
309
323
  config,
310
324
  currentRoot: repository.currentRoot,
@@ -315,7 +329,6 @@ export function createNewCommand(dependencies = {}) {
315
329
  runCommand: dependencies.runCommand,
316
330
  commandStdout: options.json ? () => undefined : options.stdout,
317
331
  commandStderr: options.json ? () => undefined : options.stderr,
318
- json: options.json,
319
332
  worktreePath,
320
333
  });
321
334
  if (!bootstrap.ready) {
@@ -344,30 +357,135 @@ export function createNewCommand(dependencies = {}) {
344
357
  else {
345
358
  if (options.take)
346
359
  options.stderr(`✓ took ${takeFiles.length} changed file${takeFiles.length === 1 ? "" : "s"} (${takeUntracked} untracked) → ${worktreeName}\n`);
347
- await recordWorktreeUsage(worktreePath, worktreeName);
360
+ await runtime.history.recordWorktreeUsage(worktreePath, worktreeName);
348
361
  await writeOutput(worktreePath, options.stdout, options.outputEnv);
349
362
  }
350
363
  if (options.open) {
351
- const resolvedEditor = options.editor ?? resolveConfigString(config, "editor");
352
- await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr);
364
+ const resolvedEditor = options.editor ?? runtime.config.resolveConfigString(config, "editor");
365
+ await openWorktree(worktreePath, resolvedEditor, spawnEditor, options.stderr, runtime.integrations.EDITORS);
353
366
  }
354
367
  return 0;
355
368
  };
356
369
  }
370
+ function findBranchOnlyIncompatibleOption(options) {
371
+ if (options.detached)
372
+ return "--detached";
373
+ if (options.fromCurrent)
374
+ return "--from-current";
375
+ if (options.take)
376
+ return "--take";
377
+ if (options.copy)
378
+ return "--copy";
379
+ if (options.task !== undefined)
380
+ return "--task";
381
+ if (options.force)
382
+ return "--force";
383
+ if (options.open)
384
+ return "--open";
385
+ if (options.editor)
386
+ return "--editor";
387
+ return null;
388
+ }
389
+ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
390
+ const branchName = applyConfiguredBranchPrefix(rawBranch, config.branchPrefix);
391
+ const remote = runtime.config.resolveConfigString(config, "syncRemote") ?? "origin";
392
+ const configuredBaseBranch = runtime.config.resolveConfigString(config, "syncDefaultBranch");
393
+ if (options.dryRun) {
394
+ const baseDescription = configuredBaseBranch
395
+ ? configuredBaseBranch
396
+ : `the default branch from ${remote}`;
397
+ if (options.json) {
398
+ const navigation = createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), repository.currentRoot, branchName);
399
+ options.stdout(`${JSON.stringify({ ...navigation, baseBranch: configuredBaseBranch ?? null, branchOnly: true, dryRun: true }, null, 2)}\n`);
400
+ }
401
+ else {
402
+ const sourceDescription = configuredBaseBranch
403
+ ? options.noFetch
404
+ ? `local ${configuredBaseBranch}`
405
+ : `${remote}/${configuredBaseBranch}`
406
+ : baseDescription;
407
+ options.stdout(`Would create branch ${branchName} from ${sourceDescription} in the current worktree\n`);
408
+ }
409
+ return 0;
410
+ }
411
+ if (await localBranchExists(repository.repoRoot, branchName, runtime)) {
412
+ return emitNewError(options, `branch already exists: ${branchName}`);
413
+ }
414
+ if (await runtime.git.isDirtyWorktree(options.cwd)) {
415
+ return emitNewError(options, "current worktree has uncommitted changes; commit or stash them before using --branch-only");
416
+ }
417
+ const inProgressState = await detectInProgressGitState(options.cwd, runtime.git.runGit);
418
+ if (inProgressState) {
419
+ return emitNewError(options, `cannot create a branch while Git is in progress (${inProgressState})`);
420
+ }
421
+ let baseBranch = configuredBaseBranch;
422
+ if (!baseBranch || !options.noFetch) {
423
+ try {
424
+ const remoteBase = await runtime.git.resolveRemoteBase(repository.repoRoot, remote, configuredBaseBranch);
425
+ baseBranch = remoteBase?.branch ?? baseBranch;
426
+ }
427
+ catch (error) {
428
+ return emitNewError(options, `could not determine the default branch: ${toExecMessage(error)}`);
429
+ }
430
+ }
431
+ if (!baseBranch) {
432
+ return emitNewError(options, `could not determine the default branch; configure syncDefaultBranch or add a remote named ${remote}`);
433
+ }
434
+ if (options.noFetch &&
435
+ !(await localBranchExists(repository.repoRoot, baseBranch, runtime))) {
436
+ return emitNewError(options, `base branch does not exist locally: ${baseBranch}`);
437
+ }
438
+ try {
439
+ let startPoint = baseBranch;
440
+ if (!options.noFetch) {
441
+ await runtime.git.runGit(repository.repoRoot, [
442
+ "fetch",
443
+ "--prune",
444
+ remote,
445
+ baseBranch,
446
+ ]);
447
+ startPoint = `${remote}/${baseBranch}`;
448
+ }
449
+ await runtime.git.runGit(options.cwd, [
450
+ "switch",
451
+ "-c",
452
+ branchName,
453
+ startPoint,
454
+ ]);
455
+ }
456
+ catch (error) {
457
+ return emitNewError(options, `failed to create branch in the current worktree: ${toExecMessage(error)}`);
458
+ }
459
+ if (options.json) {
460
+ const navigation = createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), repository.currentRoot, branchName);
461
+ options.stdout(`${JSON.stringify({ ...navigation, baseBranch, branchOnly: true }, null, 2)}\n`);
462
+ }
463
+ else {
464
+ options.stderr(`✓ created ${branchName} from ${options.noFetch ? baseBranch : `${remote}/${baseBranch}`} in the current worktree\n`);
465
+ await runtime.history.recordWorktreeUsage(repository.currentRoot, branchName);
466
+ await writeOutput(repository.currentRoot, options.stdout, options.outputEnv);
467
+ }
468
+ return 0;
469
+ }
357
470
  export const runNewCommand = createNewCommand();
358
- async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFailure) {
359
- const remote = resolveConfigString(config, "syncRemote") ?? "origin";
360
- const configuredRemote = resolveConfigString(config, "syncRemote");
361
- let baseBranch = resolveConfigString(config, "syncDefaultBranch");
471
+ async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFailure, runtime) {
472
+ const remote = runtime.config.resolveConfigString(config, "syncRemote") ?? "origin";
473
+ const configuredRemote = runtime.config.resolveConfigString(config, "syncRemote");
474
+ let baseBranch = runtime.config.resolveConfigString(config, "syncDefaultBranch");
362
475
  try {
363
- const remoteBase = await resolveRemoteBase(repoRoot, remote, baseBranch);
476
+ const remoteBase = await runtime.git.resolveRemoteBase(repoRoot, remote, baseBranch);
364
477
  if (!remoteBase) {
365
478
  if (!configuredRemote)
366
479
  return "HEAD";
367
480
  throw new Error("the remote default branch is unknown");
368
481
  }
369
482
  baseBranch = remoteBase.branch;
370
- await runGit(repoRoot, ["fetch", "--prune", remote, remoteBase.branch]);
483
+ await runtime.git.runGit(repoRoot, [
484
+ "fetch",
485
+ "--prune",
486
+ remote,
487
+ remoteBase.branch,
488
+ ]);
371
489
  return remoteBase.ref;
372
490
  }
373
491
  catch (error) {
@@ -383,7 +501,7 @@ async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFail
383
501
  return null;
384
502
  }
385
503
  if (baseBranch &&
386
- (await localRefExists(repoRoot, `refs/remotes/${remote}/${baseBranch}`))) {
504
+ (await localRefExists(repoRoot, `refs/remotes/${remote}/${baseBranch}`, runtime))) {
387
505
  options.stderr(`Continuing with the cached ${remote}/${baseBranch}.\n`);
388
506
  return `${remote}/${baseBranch}`;
389
507
  }
@@ -400,9 +518,14 @@ async function defaultPromptForFetchFailure(message) {
400
518
  });
401
519
  return !isCancel(choice) && choice;
402
520
  }
403
- async function localRefExists(repoRoot, ref) {
521
+ async function localRefExists(repoRoot, ref, runtime) {
404
522
  try {
405
- await runGit(repoRoot, ["rev-parse", "--verify", "--quiet", ref]);
523
+ await runtime.git.runGit(repoRoot, [
524
+ "rev-parse",
525
+ "--verify",
526
+ "--quiet",
527
+ ref,
528
+ ]);
406
529
  return true;
407
530
  }
408
531
  catch {
@@ -552,23 +675,28 @@ async function defaultPromptForBranch(placeholder) {
552
675
  }
553
676
  return choice.trim();
554
677
  }
555
- async function localBranchExists(repoRoot, branchName) {
678
+ async function localBranchExists(repoRoot, branchName, runtime) {
556
679
  try {
557
- await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
680
+ await runtime.git.runGit(repoRoot, [
681
+ "show-ref",
682
+ "--verify",
683
+ "--quiet",
684
+ `refs/heads/${branchName}`,
685
+ ]);
558
686
  return true;
559
687
  }
560
688
  catch {
561
689
  return false;
562
690
  }
563
691
  }
564
- async function resolveCurrentWorktreeHead(cwd) {
565
- return execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { cwd }).then(({ stdout }) => stdout.trim());
692
+ async function resolveCurrentWorktreeHead(cwd, runGit) {
693
+ return runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
566
694
  }
567
- async function createTakeStash(cwd, worktreeName) {
695
+ async function createTakeStash(cwd, worktreeName, runGit) {
568
696
  const message = `gji-take: ${worktreeName}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
569
- await execFileAsync("git", ["stash", "push", "--include-untracked", "-m", message], { cwd });
570
- const { stdout } = await execFileAsync("git", ["stash", "list", "--format=%H%x00%s"], { cwd });
571
- const line = stdout
697
+ await runGit(cwd, ["stash", "push", "--include-untracked", "-m", message]);
698
+ const output = await runGit(cwd, ["stash", "list", "--format=%H%x00%s"]);
699
+ const line = output
572
700
  .split("\n")
573
701
  .map((candidate) => candidate.split("\0"))
574
702
  .find(([, subject]) => subject?.endsWith(message));
@@ -576,7 +704,7 @@ async function createTakeStash(cwd, worktreeName) {
576
704
  throw new Error("could not identify the stash created for --take");
577
705
  return line[0];
578
706
  }
579
- async function detectInProgressGitState(cwd) {
707
+ async function detectInProgressGitState(cwd, runGit) {
580
708
  const markers = [
581
709
  ["MERGE_HEAD", "merge"],
582
710
  ["rebase-merge", "rebase"],
@@ -587,8 +715,8 @@ async function detectInProgressGitState(cwd) {
587
715
  ];
588
716
  for (const [marker, name] of markers) {
589
717
  try {
590
- const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", marker], { cwd });
591
- await access(resolve(cwd, stdout.trim()));
718
+ const gitPath = await runGit(cwd, ["rev-parse", "--git-path", marker]);
719
+ await access(resolve(cwd, gitPath));
592
720
  return name;
593
721
  }
594
722
  catch {
@@ -601,24 +729,19 @@ async function writeOutput(worktreePath, stdout, outputEnv) {
601
729
  await writeShellOutput(outputEnv ?? NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
602
730
  }
603
731
  function isNotRegisteredWorktreeError(error) {
604
- const stderr = hasExecStderr(error) ? error.stderr : String(error);
732
+ const stderr = error instanceof Error ? error.message : String(error);
605
733
  return (stderr.includes("is not a working tree") ||
606
734
  stderr.includes("not a linked working tree"));
607
735
  }
608
- function hasExecStderr(error) {
609
- return (error instanceof Error &&
610
- "stderr" in error &&
611
- typeof error.stderr === "string");
612
- }
613
736
  function toExecMessage(error) {
614
- return hasExecStderr(error) ? error.stderr.trim() : String(error);
737
+ return error instanceof Error ? error.message : String(error);
615
738
  }
616
- async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
739
+ async function openWorktree(worktreePath, editorCli, spawnFn, stderr, editors) {
617
740
  if (!editorCli) {
618
741
  stderr("gji new: --open requires --editor <cli> or a saved editor in config\n");
619
742
  return;
620
743
  }
621
- const editorDef = EDITORS.find((e) => e.cli === editorCli);
744
+ const editorDef = editors.find((e) => e.cli === editorCli);
622
745
  const args = [];
623
746
  if (editorDef?.newWindowFlag) {
624
747
  args.push(editorDef.newWindowFlag);
@@ -632,10 +755,10 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
632
755
  stderr(`gji new: failed to open editor: ${message}\n`);
633
756
  }
634
757
  }
635
- async function listTakeFiles(cwd) {
758
+ async function listTakeFiles(cwd, runGit) {
636
759
  try {
637
- const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1"], { cwd });
638
- return stdout
760
+ const output = await runGit(cwd, ["status", "--porcelain=v1"]);
761
+ return output
639
762
  .split("\n")
640
763
  .filter((line) => line.length > 3)
641
764
  .map((line) => line.slice(3).replace(/^"|"$/g, ""));
@@ -644,10 +767,15 @@ async function listTakeFiles(cwd) {
644
767
  return [];
645
768
  }
646
769
  }
647
- async function listIgnoredFiles(cwd) {
770
+ async function listIgnoredFiles(cwd, runGit) {
648
771
  try {
649
- const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--ignored", "--untracked-files=all"], { cwd });
650
- return stdout
772
+ const output = await runGit(cwd, [
773
+ "status",
774
+ "--porcelain=v1",
775
+ "--ignored",
776
+ "--untracked-files=all",
777
+ ]);
778
+ return output
651
779
  .split("\n")
652
780
  .filter((line) => line.startsWith("!! "))
653
781
  .map((line) => line.slice(3).replace(/^"|"$/g, ""));
@@ -656,14 +784,12 @@ async function listIgnoredFiles(cwd) {
656
784
  return [];
657
785
  }
658
786
  }
659
- async function listSubmoduleFiles(cwd, changedFiles) {
787
+ async function listSubmoduleFiles(cwd, changedFiles, runGit) {
660
788
  if (changedFiles.length === 0)
661
789
  return [];
662
790
  try {
663
- const { stdout } = await execFileAsync("git", ["ls-files", "--stage"], {
664
- cwd,
665
- });
666
- const submodules = new Set(stdout
791
+ const output = await runGit(cwd, ["ls-files", "--stage"]);
792
+ const submodules = new Set(output
667
793
  .split("\n")
668
794
  .filter((line) => line.startsWith("160000 "))
669
795
  .map((line) => line.slice(line.indexOf("\t") + 1)));
@@ -673,31 +799,31 @@ async function listSubmoduleFiles(cwd, changedFiles) {
673
799
  return [];
674
800
  }
675
801
  }
676
- async function countUntrackedFiles(cwd) {
802
+ async function countUntrackedFiles(cwd, runGit) {
677
803
  try {
678
- const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd });
679
- return stdout.split("\n").filter((line) => line.startsWith("?? ")).length;
804
+ const output = await runGit(cwd, [
805
+ "status",
806
+ "--porcelain=v1",
807
+ "--untracked-files=all",
808
+ ]);
809
+ return output.split("\n").filter((line) => line.startsWith("?? ")).length;
680
810
  }
681
811
  catch {
682
812
  return 0;
683
813
  }
684
814
  }
685
- async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr) {
815
+ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr, runGit) {
686
816
  try {
687
- await execFileAsync("git", ["stash", "apply", stashSha], {
688
- cwd: worktreePath,
689
- });
817
+ await runGit(worktreePath, ["stash", "apply", stashSha]);
690
818
  }
691
819
  catch (error) {
692
820
  stderr(`Warning: stash apply failed: ${toExecMessage(error)}\n`);
693
- await restoreTakeStash(sourcePath, stashSha, stderr);
821
+ await restoreTakeStash(sourcePath, stashSha, stderr, runGit);
694
822
  return false;
695
823
  }
696
824
  if (copy) {
697
825
  try {
698
- await execFileAsync("git", ["stash", "apply", stashSha], {
699
- cwd: sourcePath,
700
- });
826
+ await runGit(sourcePath, ["stash", "apply", stashSha]);
701
827
  }
702
828
  catch (error) {
703
829
  stderr(`Warning: could not restore copied changes to the source: ${toExecMessage(error)}\n`);
@@ -705,24 +831,26 @@ async function applyTakeStash(worktreePath, sourcePath, stashSha, copy, stderr)
705
831
  }
706
832
  }
707
833
  try {
708
- const { stdout } = await execFileAsync("git", ["stash", "list", "--format=%H %gd"], { cwd: sourcePath });
709
- const line = stdout
834
+ const output = await runGit(sourcePath, [
835
+ "stash",
836
+ "list",
837
+ "--format=%H %gd",
838
+ ]);
839
+ const line = output
710
840
  .split("\n")
711
841
  .find((candidate) => candidate.startsWith(stashSha));
712
842
  const ref = line?.trim().split(/\s+/).at(1);
713
843
  if (ref)
714
- await execFileAsync("git", ["stash", "drop", ref], { cwd: sourcePath });
844
+ await runGit(sourcePath, ["stash", "drop", ref]);
715
845
  }
716
846
  catch {
717
847
  /* preserving the stash is safer */
718
848
  }
719
849
  return true;
720
850
  }
721
- async function restoreTakeStash(sourcePath, stashSha, stderr) {
851
+ async function restoreTakeStash(sourcePath, stashSha, stderr, runGit) {
722
852
  try {
723
- await execFileAsync("git", ["stash", "apply", stashSha], {
724
- cwd: sourcePath,
725
- });
853
+ await runGit(sourcePath, ["stash", "apply", stashSha]);
726
854
  }
727
855
  catch {
728
856
  stderr(`Your changes are safe in stash: ${stashSha} — run "git stash apply ${stashSha}" to restore\n`);