@camaradesuk/git-worktree-tools 1.7.0 → 1.9.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 (295) hide show
  1. package/README.md +69 -8
  2. package/dist/cli/cleanpr.js +11 -5
  3. package/dist/cli/cleanpr.js.map +1 -1
  4. package/dist/cli/cleanpr.test.js +12 -1
  5. package/dist/cli/cleanpr.test.js.map +1 -1
  6. package/dist/cli/newpr.js +302 -37
  7. package/dist/cli/newpr.js.map +1 -1
  8. package/dist/cli/newpr.test.js +336 -9
  9. package/dist/cli/newpr.test.js.map +1 -1
  10. package/dist/cli/prs.d.ts +22 -0
  11. package/dist/cli/prs.d.ts.map +1 -0
  12. package/dist/cli/prs.js +275 -0
  13. package/dist/cli/prs.js.map +1 -0
  14. package/dist/cli/prs.test.d.ts +8 -0
  15. package/dist/cli/prs.test.d.ts.map +1 -0
  16. package/dist/cli/prs.test.js +410 -0
  17. package/dist/cli/prs.test.js.map +1 -0
  18. package/dist/cli/wt/interactive-menu.d.ts +2 -0
  19. package/dist/cli/wt/interactive-menu.d.ts.map +1 -1
  20. package/dist/cli/wt/interactive-menu.js +17 -0
  21. package/dist/cli/wt/interactive-menu.js.map +1 -1
  22. package/dist/cli/wt/interactive-menu.test.js +28 -0
  23. package/dist/cli/wt/interactive-menu.test.js.map +1 -1
  24. package/dist/cli/wt/prs.d.ts +21 -0
  25. package/dist/cli/wt/prs.d.ts.map +1 -0
  26. package/dist/cli/wt/prs.js +251 -0
  27. package/dist/cli/wt/prs.js.map +1 -0
  28. package/dist/cli/wt/prs.test.d.ts +5 -0
  29. package/dist/cli/wt/prs.test.d.ts.map +1 -0
  30. package/dist/cli/wt/prs.test.js +410 -0
  31. package/dist/cli/wt/prs.test.js.map +1 -0
  32. package/dist/cli/wt.d.ts +1 -0
  33. package/dist/cli/wt.d.ts.map +1 -1
  34. package/dist/cli/wt.js +5 -0
  35. package/dist/cli/wt.js.map +1 -1
  36. package/dist/cli/wtconfig.d.ts +1 -0
  37. package/dist/cli/wtconfig.d.ts.map +1 -1
  38. package/dist/cli/wtconfig.js +115 -0
  39. package/dist/cli/wtconfig.js.map +1 -1
  40. package/dist/cli/wtconfig.test.js +6 -0
  41. package/dist/cli/wtconfig.test.js.map +1 -1
  42. package/dist/cli/wtlink.d.ts +2 -1
  43. package/dist/cli/wtlink.d.ts.map +1 -1
  44. package/dist/cli/wtlink.js +60 -2
  45. package/dist/cli/wtlink.js.map +1 -1
  46. package/dist/e2e/cli.e2e.test.js +6 -2
  47. package/dist/e2e/cli.e2e.test.js.map +1 -1
  48. package/dist/e2e/helpers/cli-runner.d.ts.map +1 -1
  49. package/dist/e2e/helpers/cli-runner.js +1 -0
  50. package/dist/e2e/helpers/cli-runner.js.map +1 -1
  51. package/dist/e2e/helpers/gh-mock.d.ts.map +1 -1
  52. package/dist/e2e/helpers/gh-mock.js +55 -1
  53. package/dist/e2e/helpers/gh-mock.js.map +1 -1
  54. package/dist/e2e/helpers/pty-wrapper.d.ts +15 -0
  55. package/dist/e2e/helpers/pty-wrapper.d.ts.map +1 -1
  56. package/dist/e2e/helpers/pty-wrapper.js +65 -0
  57. package/dist/e2e/helpers/pty-wrapper.js.map +1 -1
  58. package/dist/e2e/newpr-full-flow.e2e.test.js +1 -0
  59. package/dist/e2e/newpr-full-flow.e2e.test.js.map +1 -1
  60. package/dist/e2e/prs/prs.e2e.test.d.ts +7 -0
  61. package/dist/e2e/prs/prs.e2e.test.d.ts.map +1 -0
  62. package/dist/e2e/prs/prs.e2e.test.js +606 -0
  63. package/dist/e2e/prs/prs.e2e.test.js.map +1 -0
  64. package/dist/e2e/wt/interactive-menu.e2e.test.d.ts +8 -0
  65. package/dist/e2e/wt/interactive-menu.e2e.test.d.ts.map +1 -0
  66. package/dist/e2e/wt/interactive-menu.e2e.test.js +583 -0
  67. package/dist/e2e/wt/interactive-menu.e2e.test.js.map +1 -0
  68. package/dist/e2e/wt/wt.e2e.test.js +217 -4
  69. package/dist/e2e/wt/wt.e2e.test.js.map +1 -1
  70. package/dist/integration/prs.integration.test.d.ts +8 -0
  71. package/dist/integration/prs.integration.test.d.ts.map +1 -0
  72. package/dist/integration/prs.integration.test.js +478 -0
  73. package/dist/integration/prs.integration.test.js.map +1 -0
  74. package/dist/lib/ai/types.d.ts +12 -0
  75. package/dist/lib/ai/types.d.ts.map +1 -1
  76. package/dist/lib/ai/types.js.map +1 -1
  77. package/dist/lib/cleanpr/worktree-info.d.ts.map +1 -1
  78. package/dist/lib/cleanpr/worktree-info.js +1 -6
  79. package/dist/lib/cleanpr/worktree-info.js.map +1 -1
  80. package/dist/lib/cleanpr/worktree-info.test.js +10 -13
  81. package/dist/lib/cleanpr/worktree-info.test.js.map +1 -1
  82. package/dist/lib/config-editor.d.ts.map +1 -1
  83. package/dist/lib/config-editor.js.map +1 -1
  84. package/dist/lib/config-migration/detector.d.ts +25 -0
  85. package/dist/lib/config-migration/detector.d.ts.map +1 -0
  86. package/dist/lib/config-migration/detector.js +372 -0
  87. package/dist/lib/config-migration/detector.js.map +1 -0
  88. package/dist/lib/config-migration/detector.test.d.ts +5 -0
  89. package/dist/lib/config-migration/detector.test.d.ts.map +1 -0
  90. package/dist/lib/config-migration/detector.test.js +201 -0
  91. package/dist/lib/config-migration/detector.test.js.map +1 -0
  92. package/dist/lib/config-migration/index.d.ts +29 -0
  93. package/dist/lib/config-migration/index.d.ts.map +1 -0
  94. package/dist/lib/config-migration/index.js +33 -0
  95. package/dist/lib/config-migration/index.js.map +1 -0
  96. package/dist/lib/config-migration/reporter.d.ts +53 -0
  97. package/dist/lib/config-migration/reporter.d.ts.map +1 -0
  98. package/dist/lib/config-migration/reporter.js +257 -0
  99. package/dist/lib/config-migration/reporter.js.map +1 -0
  100. package/dist/lib/config-migration/reporter.test.d.ts +5 -0
  101. package/dist/lib/config-migration/reporter.test.d.ts.map +1 -0
  102. package/dist/lib/config-migration/reporter.test.js +305 -0
  103. package/dist/lib/config-migration/reporter.test.js.map +1 -0
  104. package/dist/lib/config-migration/runner.d.ts +46 -0
  105. package/dist/lib/config-migration/runner.d.ts.map +1 -0
  106. package/dist/lib/config-migration/runner.js +364 -0
  107. package/dist/lib/config-migration/runner.js.map +1 -0
  108. package/dist/lib/config-migration/runner.test.d.ts +5 -0
  109. package/dist/lib/config-migration/runner.test.d.ts.map +1 -0
  110. package/dist/lib/config-migration/runner.test.js +235 -0
  111. package/dist/lib/config-migration/runner.test.js.map +1 -0
  112. package/dist/lib/config-migration/types.d.ts +120 -0
  113. package/dist/lib/config-migration/types.d.ts.map +1 -0
  114. package/dist/lib/config-migration/types.js +70 -0
  115. package/dist/lib/config-migration/types.js.map +1 -0
  116. package/dist/lib/config-validation.d.ts.map +1 -1
  117. package/dist/lib/config-validation.js +62 -0
  118. package/dist/lib/config-validation.js.map +1 -1
  119. package/dist/lib/config-validation.test.js +25 -0
  120. package/dist/lib/config-validation.test.js.map +1 -1
  121. package/dist/lib/config.d.ts +65 -7
  122. package/dist/lib/config.d.ts.map +1 -1
  123. package/dist/lib/config.js +12 -0
  124. package/dist/lib/config.js.map +1 -1
  125. package/dist/lib/git.d.ts +32 -0
  126. package/dist/lib/git.d.ts.map +1 -1
  127. package/dist/lib/git.js +95 -1
  128. package/dist/lib/git.js.map +1 -1
  129. package/dist/lib/git.test.js +118 -1
  130. package/dist/lib/git.test.js.map +1 -1
  131. package/dist/lib/github.d.ts +41 -0
  132. package/dist/lib/github.d.ts.map +1 -1
  133. package/dist/lib/github.js +109 -0
  134. package/dist/lib/github.js.map +1 -1
  135. package/dist/lib/global-check.d.ts +2 -2
  136. package/dist/lib/global-check.js +3 -3
  137. package/dist/lib/global-check.js.map +1 -1
  138. package/dist/lib/global-check.test.js +3 -6
  139. package/dist/lib/global-check.test.js.map +1 -1
  140. package/dist/lib/hooks/confirmation.d.ts +49 -0
  141. package/dist/lib/hooks/confirmation.d.ts.map +1 -0
  142. package/dist/lib/hooks/confirmation.js +147 -0
  143. package/dist/lib/hooks/confirmation.js.map +1 -0
  144. package/dist/lib/hooks/confirmation.test.d.ts +7 -0
  145. package/dist/lib/hooks/confirmation.test.d.ts.map +1 -0
  146. package/dist/lib/hooks/confirmation.test.js +300 -0
  147. package/dist/lib/hooks/confirmation.test.js.map +1 -0
  148. package/dist/lib/hooks/executor.d.ts +16 -1
  149. package/dist/lib/hooks/executor.d.ts.map +1 -1
  150. package/dist/lib/hooks/executor.js +53 -4
  151. package/dist/lib/hooks/executor.js.map +1 -1
  152. package/dist/lib/hooks/index.d.ts +4 -2
  153. package/dist/lib/hooks/index.d.ts.map +1 -1
  154. package/dist/lib/hooks/index.js +3 -2
  155. package/dist/lib/hooks/index.js.map +1 -1
  156. package/dist/lib/hooks/types.d.ts +16 -0
  157. package/dist/lib/hooks/types.d.ts.map +1 -1
  158. package/dist/lib/hooks/types.js +12 -0
  159. package/dist/lib/hooks/types.js.map +1 -1
  160. package/dist/lib/json-output.d.ts +1 -0
  161. package/dist/lib/json-output.d.ts.map +1 -1
  162. package/dist/lib/json-output.js +2 -0
  163. package/dist/lib/json-output.js.map +1 -1
  164. package/dist/lib/lswt/action-executors.d.ts +2 -0
  165. package/dist/lib/lswt/action-executors.d.ts.map +1 -1
  166. package/dist/lib/lswt/action-executors.js +6 -4
  167. package/dist/lib/lswt/action-executors.js.map +1 -1
  168. package/dist/lib/lswt/action-executors.test.js +8 -0
  169. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  170. package/dist/lib/lswt/actions.d.ts +1 -0
  171. package/dist/lib/lswt/actions.d.ts.map +1 -1
  172. package/dist/lib/lswt/actions.js +38 -10
  173. package/dist/lib/lswt/actions.js.map +1 -1
  174. package/dist/lib/lswt/actions.test.js +34 -22
  175. package/dist/lib/lswt/actions.test.js.map +1 -1
  176. package/dist/lib/lswt/environment.d.ts +21 -2
  177. package/dist/lib/lswt/environment.d.ts.map +1 -1
  178. package/dist/lib/lswt/environment.js +73 -32
  179. package/dist/lib/lswt/environment.js.map +1 -1
  180. package/dist/lib/lswt/environment.test.js +79 -1
  181. package/dist/lib/lswt/environment.test.js.map +1 -1
  182. package/dist/lib/lswt/interactive.js +8 -8
  183. package/dist/lib/lswt/interactive.js.map +1 -1
  184. package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
  185. package/dist/lib/lswt/worktree-info.js +1 -6
  186. package/dist/lib/lswt/worktree-info.js.map +1 -1
  187. package/dist/lib/lswt/worktree-info.test.js +5 -17
  188. package/dist/lib/lswt/worktree-info.test.js.map +1 -1
  189. package/dist/lib/newpr/args.d.ts.map +1 -1
  190. package/dist/lib/newpr/args.js +15 -1
  191. package/dist/lib/newpr/args.js.map +1 -1
  192. package/dist/lib/newpr/hook-runner.d.ts +11 -0
  193. package/dist/lib/newpr/hook-runner.d.ts.map +1 -1
  194. package/dist/lib/newpr/hook-runner.js +49 -1
  195. package/dist/lib/newpr/hook-runner.js.map +1 -1
  196. package/dist/lib/newpr/hook-runner.test.js +121 -0
  197. package/dist/lib/newpr/hook-runner.test.js.map +1 -1
  198. package/dist/lib/newpr/plan-generator.d.ts +121 -0
  199. package/dist/lib/newpr/plan-generator.d.ts.map +1 -0
  200. package/dist/lib/newpr/plan-generator.js +185 -0
  201. package/dist/lib/newpr/plan-generator.js.map +1 -0
  202. package/dist/lib/newpr/plan-generator.test.d.ts +7 -0
  203. package/dist/lib/newpr/plan-generator.test.d.ts.map +1 -0
  204. package/dist/lib/newpr/plan-generator.test.js +387 -0
  205. package/dist/lib/newpr/plan-generator.test.js.map +1 -0
  206. package/dist/lib/newpr/types.d.ts +6 -0
  207. package/dist/lib/newpr/types.d.ts.map +1 -1
  208. package/dist/lib/prompts.d.ts.map +1 -1
  209. package/dist/lib/prompts.js +16 -12
  210. package/dist/lib/prompts.js.map +1 -1
  211. package/dist/lib/prs/actions.d.ts +74 -0
  212. package/dist/lib/prs/actions.d.ts.map +1 -0
  213. package/dist/lib/prs/actions.js +446 -0
  214. package/dist/lib/prs/actions.js.map +1 -0
  215. package/dist/lib/prs/actions.test.d.ts +5 -0
  216. package/dist/lib/prs/actions.test.d.ts.map +1 -0
  217. package/dist/lib/prs/actions.test.js +356 -0
  218. package/dist/lib/prs/actions.test.js.map +1 -0
  219. package/dist/lib/prs/data.d.ts +48 -0
  220. package/dist/lib/prs/data.d.ts.map +1 -0
  221. package/dist/lib/prs/data.js +171 -0
  222. package/dist/lib/prs/data.js.map +1 -0
  223. package/dist/lib/prs/data.test.d.ts +5 -0
  224. package/dist/lib/prs/data.test.d.ts.map +1 -0
  225. package/dist/lib/prs/data.test.js +417 -0
  226. package/dist/lib/prs/data.test.js.map +1 -0
  227. package/dist/lib/prs/details.d.ts +57 -0
  228. package/dist/lib/prs/details.d.ts.map +1 -0
  229. package/dist/lib/prs/details.js +246 -0
  230. package/dist/lib/prs/details.js.map +1 -0
  231. package/dist/lib/prs/details.test.d.ts +5 -0
  232. package/dist/lib/prs/details.test.d.ts.map +1 -0
  233. package/dist/lib/prs/details.test.js +325 -0
  234. package/dist/lib/prs/details.test.js.map +1 -0
  235. package/dist/lib/prs/filters.d.ts +56 -0
  236. package/dist/lib/prs/filters.d.ts.map +1 -0
  237. package/dist/lib/prs/filters.js +171 -0
  238. package/dist/lib/prs/filters.js.map +1 -0
  239. package/dist/lib/prs/filters.test.d.ts +5 -0
  240. package/dist/lib/prs/filters.test.d.ts.map +1 -0
  241. package/dist/lib/prs/filters.test.js +312 -0
  242. package/dist/lib/prs/filters.test.js.map +1 -0
  243. package/dist/lib/prs/formatters.d.ts +83 -0
  244. package/dist/lib/prs/formatters.d.ts.map +1 -0
  245. package/dist/lib/prs/formatters.js +389 -0
  246. package/dist/lib/prs/formatters.js.map +1 -0
  247. package/dist/lib/prs/formatters.test.d.ts +2 -0
  248. package/dist/lib/prs/formatters.test.d.ts.map +1 -0
  249. package/dist/lib/prs/formatters.test.js +387 -0
  250. package/dist/lib/prs/formatters.test.js.map +1 -0
  251. package/dist/lib/prs/interactive.d.ts +50 -0
  252. package/dist/lib/prs/interactive.d.ts.map +1 -0
  253. package/dist/lib/prs/interactive.js +453 -0
  254. package/dist/lib/prs/interactive.js.map +1 -0
  255. package/dist/lib/prs/interactive.test.d.ts +5 -0
  256. package/dist/lib/prs/interactive.test.d.ts.map +1 -0
  257. package/dist/lib/prs/interactive.test.js +364 -0
  258. package/dist/lib/prs/interactive.test.js.map +1 -0
  259. package/dist/lib/prs/types.d.ts +152 -0
  260. package/dist/lib/prs/types.d.ts.map +1 -0
  261. package/dist/lib/prs/types.js +17 -0
  262. package/dist/lib/prs/types.js.map +1 -0
  263. package/dist/lib/wtconfig/environment.d.ts +18 -1
  264. package/dist/lib/wtconfig/environment.d.ts.map +1 -1
  265. package/dist/lib/wtconfig/environment.js +60 -24
  266. package/dist/lib/wtconfig/environment.js.map +1 -1
  267. package/dist/lib/wtconfig/environment.test.js +45 -1
  268. package/dist/lib/wtconfig/environment.test.js.map +1 -1
  269. package/dist/lib/wtlink/config-manifest.d.ts +101 -0
  270. package/dist/lib/wtlink/config-manifest.d.ts.map +1 -0
  271. package/dist/lib/wtlink/config-manifest.js +219 -0
  272. package/dist/lib/wtlink/config-manifest.js.map +1 -0
  273. package/dist/lib/wtlink/config-manifest.test.d.ts +2 -0
  274. package/dist/lib/wtlink/config-manifest.test.d.ts.map +1 -0
  275. package/dist/lib/wtlink/config-manifest.test.js +486 -0
  276. package/dist/lib/wtlink/config-manifest.test.js.map +1 -0
  277. package/dist/lib/wtlink/link-configs.d.ts.map +1 -1
  278. package/dist/lib/wtlink/link-configs.js +36 -11
  279. package/dist/lib/wtlink/link-configs.js.map +1 -1
  280. package/dist/lib/wtlink/main-menu.d.ts.map +1 -1
  281. package/dist/lib/wtlink/main-menu.js +58 -50
  282. package/dist/lib/wtlink/main-menu.js.map +1 -1
  283. package/dist/lib/wtlink/main-menu.test.js +42 -40
  284. package/dist/lib/wtlink/main-menu.test.js.map +1 -1
  285. package/dist/lib/wtlink/manage-manifest.d.ts +9 -0
  286. package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -1
  287. package/dist/lib/wtlink/manage-manifest.js +346 -25
  288. package/dist/lib/wtlink/manage-manifest.js.map +1 -1
  289. package/dist/lib/wtlink/manage-manifest.test.js +52 -1
  290. package/dist/lib/wtlink/manage-manifest.test.js.map +1 -1
  291. package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -1
  292. package/dist/lib/wtlink/validate-manifest.js +27 -6
  293. package/dist/lib/wtlink/validate-manifest.js.map +1 -1
  294. package/package.json +2 -1
  295. package/schemas/worktreerc.schema.json +49 -1
@@ -3,11 +3,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
3
  vi.mock('../lib/git.js', () => ({
4
4
  getRepoRoot: vi.fn(),
5
5
  getRepoName: vi.fn(),
6
+ getMainWorktreeRoot: vi.fn(),
6
7
  fetch: vi.fn(),
8
+ fetchAsync: vi.fn().mockResolvedValue(undefined),
7
9
  getCurrentBranch: vi.fn(),
8
10
  checkout: vi.fn(),
9
11
  exec: vi.fn(),
10
12
  push: vi.fn(),
13
+ pushAsync: vi.fn().mockResolvedValue(undefined),
11
14
  commit: vi.fn(),
12
15
  add: vi.fn(),
13
16
  stash: vi.fn(),
@@ -15,6 +18,7 @@ vi.mock('../lib/git.js', () => ({
15
18
  stashDrop: vi.fn(),
16
19
  stashPop: vi.fn(),
17
20
  addWorktree: vi.fn(),
21
+ addWorktreeAsync: vi.fn().mockResolvedValue(undefined),
18
22
  getStagedFiles: vi.fn(),
19
23
  getUnstagedFiles: vi.fn(),
20
24
  getStatusOutput: vi.fn(),
@@ -33,6 +37,10 @@ vi.mock('../lib/github.js', () => ({
33
37
  }));
34
38
  vi.mock('../lib/prompts.js', () => ({
35
39
  promptChoiceIndex: vi.fn(),
40
+ promptConfirm: vi.fn(),
41
+ withSpinner: vi.fn(async (message, fn) => {
42
+ return await fn();
43
+ }),
36
44
  }));
37
45
  vi.mock('../lib/config.js', () => ({
38
46
  loadConfig: vi.fn(),
@@ -80,6 +88,12 @@ vi.mock('fs', () => ({
80
88
  existsSync: vi.fn(),
81
89
  symlinkSync: vi.fn(),
82
90
  }));
91
+ vi.mock('../lib/wtlink/config-manifest.js', () => ({
92
+ getEnabledFiles: vi.fn(),
93
+ }));
94
+ vi.mock('../lib/wtlink/link-configs.js', () => ({
95
+ run: vi.fn(),
96
+ }));
83
97
  // Import after mocking
84
98
  import * as git from '../lib/git.js';
85
99
  import * as github from '../lib/github.js';
@@ -88,18 +102,22 @@ import { loadConfig, generateBranchNameAsync, generateWorktreePath, generatePRCo
88
102
  import { analyzeGitState, detectScenario } from '../lib/state-detection.js';
89
103
  import * as newpr from '../lib/newpr/index.js';
90
104
  import fs from 'fs';
105
+ import { getEnabledFiles } from '../lib/wtlink/config-manifest.js';
106
+ import { run as runWtlink } from '../lib/wtlink/link-configs.js';
91
107
  describe('cli/newpr', () => {
92
108
  let mockConsoleLog;
93
109
  let mockConsoleError;
94
110
  let mockProcessExit;
95
111
  let originalArgv;
96
112
  const defaultConfig = {
113
+ configVersion: 1,
97
114
  baseBranch: 'main',
98
115
  worktreePattern: '{repo}.pr{number}',
99
116
  worktreeParent: '..',
100
117
  draftPr: false,
101
118
  sharedRepos: [],
102
119
  branchPrefix: 'feature',
120
+ previewLabel: 'preview',
103
121
  syncPatterns: [],
104
122
  preferredEditor: 'auto',
105
123
  ai: { provider: 'none' },
@@ -110,6 +128,8 @@ describe('cli/newpr', () => {
110
128
  integrations: {},
111
129
  logging: { level: 'info', timestamps: true },
112
130
  global: { warnNotGlobal: true },
131
+ wtlink: { enabled: [], disabled: [] },
132
+ linkConfigFiles: undefined,
113
133
  };
114
134
  const defaultOptions = {
115
135
  baseBranch: 'main',
@@ -146,6 +166,14 @@ describe('cli/newpr', () => {
146
166
  });
147
167
  beforeEach(() => {
148
168
  vi.resetAllMocks();
169
+ // Reset async git mocks (resetAllMocks clears implementations)
170
+ vi.mocked(git.fetchAsync).mockResolvedValue(undefined);
171
+ vi.mocked(git.pushAsync).mockResolvedValue(undefined);
172
+ vi.mocked(git.addWorktreeAsync).mockResolvedValue(undefined);
173
+ // Reset withSpinner mock (resetAllMocks clears the implementation)
174
+ vi.mocked(prompts.withSpinner).mockImplementation(async (message, fn) => {
175
+ return await fn();
176
+ });
149
177
  // Reset the hook runner mock to allow all hooks to pass
150
178
  mockHookRunner.runHook.mockResolvedValue(true);
151
179
  mockHookRunner.runCleanup.mockResolvedValue(undefined);
@@ -170,6 +198,12 @@ describe('cli/newpr', () => {
170
198
  });
171
199
  vi.mocked(git.getChangedFiles).mockReturnValue([]);
172
200
  vi.mocked(git.getCommitMessages).mockReturnValue([]);
201
+ // Default mocks for auto-link feature (throw by default to skip auto-link)
202
+ vi.mocked(git.getMainWorktreeRoot).mockImplementation(() => {
203
+ throw new Error('Mock: getMainWorktreeRoot not configured');
204
+ });
205
+ vi.mocked(getEnabledFiles).mockReturnValue([]);
206
+ vi.mocked(runWtlink).mockResolvedValue(undefined);
173
207
  mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => { });
174
208
  mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
175
209
  // @ts-expect-error - process.exit mock type is complex
@@ -186,7 +220,8 @@ describe('cli/newpr', () => {
186
220
  async function runCli(args = []) {
187
221
  process.argv = ['node', 'newpr', ...args];
188
222
  await import('./newpr.js');
189
- await new Promise((resolve) => setTimeout(resolve, 10));
223
+ // Allow time for all async operations to complete
224
+ await new Promise((resolve) => setTimeout(resolve, 100));
190
225
  }
191
226
  describe('help option', () => {
192
227
  it('prints help and exits 0 on --help', async () => {
@@ -246,7 +281,8 @@ describe('cli/newpr', () => {
246
281
  vi.mocked(fs.existsSync).mockReturnValue(false);
247
282
  await runCli(['--pr', '123']);
248
283
  // Verify wiring: worktree path, branch, and options are correctly passed through
249
- expect(git.addWorktree).toHaveBeenCalledWith('/repo.pr123', // path from generateWorktreePath
284
+ // Non-JSON mode uses addWorktreeAsync
285
+ expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr123', // path from generateWorktreePath
250
286
  'feature-123', // branch from PR info
251
287
  expect.objectContaining({
252
288
  createBranch: true,
@@ -292,8 +328,8 @@ describe('cli/newpr', () => {
292
328
  head: 'my-feature',
293
329
  base: 'main', // from config
294
330
  }));
295
- // Verify wiring: addWorktree receives correct path, branch, and options
296
- expect(git.addWorktree).toHaveBeenCalledWith('/repo.pr456', // path from generateWorktreePath
331
+ // Verify wiring: addWorktreeAsync receives correct path, branch, and options (non-JSON mode)
332
+ expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr456', // path from generateWorktreePath
297
333
  'my-feature', // the branch name
298
334
  expect.objectContaining({
299
335
  createBranch: true,
@@ -362,22 +398,24 @@ describe('cli/newpr', () => {
362
398
  aiGenerated: false,
363
399
  });
364
400
  await runCli(['Add new feature']);
365
- // Verify wiring: checkout uses generated branch name and branch point
401
+ // Verify wiring: checkout uses generated branch name and branch point with repoRoot
366
402
  expect(git.exec).toHaveBeenCalledWith([
367
403
  'checkout',
368
404
  '-b',
369
405
  'feature/add-new-feature', // from generateBranchNameAsync
370
406
  'origin/main', // from getBranchPoint
371
- ]);
407
+ ], { cwd: '/repo' } // repoRoot from getRepoRoot()
408
+ );
372
409
  // Verify wiring: createPr receives correct branch and title (from generatePRContentAsync)
373
410
  expect(github.createPr).toHaveBeenCalledWith(expect.objectContaining({
374
411
  head: 'feature/add-new-feature',
375
412
  base: 'main',
376
413
  title: 'Add new feature',
377
414
  }));
378
- // Verify wiring: addWorktree receives correct path and branch
379
- expect(git.addWorktree).toHaveBeenCalledWith('/repo.pr100', // path from generateWorktreePath
380
- 'feature/add-new-feature' // the branch name
415
+ // Verify wiring: addWorktreeAsync receives correct path, branch, and cwd option
416
+ expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr100', // path from generateWorktreePath
417
+ 'feature/add-new-feature', // the branch name
418
+ { cwd: '/repo' } // repoRoot from getRepoRoot()
381
419
  );
382
420
  });
383
421
  it('exits 1 when user cancels', async () => {
@@ -851,5 +889,294 @@ describe('cli/newpr', () => {
851
889
  );
852
890
  });
853
891
  });
892
+ describe('Bug fix: git operations must use repoRoot (empty commit worktree bug)', () => {
893
+ // These tests verify the fix for the bug where creating a PR worktree
894
+ // with an empty commit failed because git.checkout() didn't switch back
895
+ // to main (missing repoRoot parameter), causing git.addWorktree() to fail
896
+ // with "branch already checked out" error.
897
+ it('git.checkout after push must use repoRoot parameter', async () => {
898
+ const repoRoot = '/repo';
899
+ vi.mocked(newpr.parseArgs).mockReturnValue({
900
+ kind: 'success',
901
+ options: { mode: 'new', description: 'Test feature', ...defaultOptions },
902
+ });
903
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
904
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
905
+ vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
906
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
907
+ vi.mocked(loadConfig).mockReturnValue(defaultConfig);
908
+ vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
909
+ vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
910
+ vi.mocked(detectScenario).mockReturnValue('main_clean_same');
911
+ vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
912
+ vi.mocked(newpr.getScenarioContext).mockReturnValue({
913
+ message: 'No changes detected',
914
+ choices: [
915
+ {
916
+ label: 'Continue with empty initial commit',
917
+ action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
918
+ },
919
+ { label: 'Cancel', action: null },
920
+ ],
921
+ });
922
+ vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
923
+ vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
924
+ vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
925
+ vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
926
+ vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
927
+ vi.mocked(git.remoteBranchExists).mockReturnValue(false);
928
+ vi.mocked(git.getCurrentBranch).mockReturnValue('main');
929
+ vi.mocked(git.getStagedFiles).mockReturnValue([]);
930
+ vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
931
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
932
+ await runCli(['Test feature']);
933
+ // CRITICAL: git.checkout must be called with repoRoot to switch back to main
934
+ // Without this, the branch remains checked out and worktree creation fails
935
+ expect(git.checkout).toHaveBeenCalledWith('main', repoRoot);
936
+ });
937
+ it('git.addWorktreeAsync must use { cwd: repoRoot } option', async () => {
938
+ const repoRoot = '/repo';
939
+ vi.mocked(newpr.parseArgs).mockReturnValue({
940
+ kind: 'success',
941
+ options: { mode: 'new', description: 'Test feature', ...defaultOptions },
942
+ });
943
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
944
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
945
+ vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
946
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
947
+ vi.mocked(loadConfig).mockReturnValue(defaultConfig);
948
+ vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
949
+ vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
950
+ vi.mocked(detectScenario).mockReturnValue('main_clean_same');
951
+ vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
952
+ vi.mocked(newpr.getScenarioContext).mockReturnValue({
953
+ message: 'No changes detected',
954
+ choices: [
955
+ {
956
+ label: 'Continue with empty initial commit',
957
+ action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
958
+ },
959
+ { label: 'Cancel', action: null },
960
+ ],
961
+ });
962
+ vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
963
+ vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
964
+ vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
965
+ vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
966
+ vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
967
+ vi.mocked(git.remoteBranchExists).mockReturnValue(false);
968
+ vi.mocked(git.getCurrentBranch).mockReturnValue('main');
969
+ vi.mocked(git.getStagedFiles).mockReturnValue([]);
970
+ vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
971
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
972
+ await runCli(['Test feature']);
973
+ // CRITICAL: git.addWorktreeAsync must be called with { cwd: repoRoot }
974
+ // to ensure worktree is created from the correct directory
975
+ expect(git.addWorktreeAsync).toHaveBeenCalledWith('/repo.pr100', 'feat/test-feature', {
976
+ cwd: repoRoot,
977
+ });
978
+ });
979
+ it('git.pushAsync must use repoRoot parameter', async () => {
980
+ const repoRoot = '/repo';
981
+ vi.mocked(newpr.parseArgs).mockReturnValue({
982
+ kind: 'success',
983
+ options: { mode: 'new', description: 'Test feature', ...defaultOptions },
984
+ });
985
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
986
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
987
+ vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
988
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
989
+ vi.mocked(loadConfig).mockReturnValue(defaultConfig);
990
+ vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
991
+ vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
992
+ vi.mocked(detectScenario).mockReturnValue('main_clean_same');
993
+ vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
994
+ vi.mocked(newpr.getScenarioContext).mockReturnValue({
995
+ message: 'No changes detected',
996
+ choices: [
997
+ {
998
+ label: 'Continue with empty initial commit',
999
+ action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
1000
+ },
1001
+ { label: 'Cancel', action: null },
1002
+ ],
1003
+ });
1004
+ vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
1005
+ vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
1006
+ vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
1007
+ vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
1008
+ vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
1009
+ vi.mocked(git.remoteBranchExists).mockReturnValue(false);
1010
+ vi.mocked(git.getCurrentBranch).mockReturnValue('main');
1011
+ vi.mocked(git.getStagedFiles).mockReturnValue([]);
1012
+ vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
1013
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
1014
+ await runCli(['Test feature']);
1015
+ // git.pushAsync must be called with repoRoot to push from correct directory
1016
+ expect(git.pushAsync).toHaveBeenCalledWith({ setUpstream: true, remote: 'origin', branch: 'feat/test-feature' }, repoRoot);
1017
+ });
1018
+ it('git.exec for branch checkout must use { cwd: repoRoot } option', async () => {
1019
+ const repoRoot = '/repo';
1020
+ vi.mocked(newpr.parseArgs).mockReturnValue({
1021
+ kind: 'success',
1022
+ options: { mode: 'new', description: 'Test feature', ...defaultOptions },
1023
+ });
1024
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
1025
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
1026
+ vi.mocked(git.getRepoRoot).mockReturnValue(repoRoot);
1027
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
1028
+ vi.mocked(loadConfig).mockReturnValue(defaultConfig);
1029
+ vi.mocked(generateBranchNameAsync).mockResolvedValue('feat/test-feature');
1030
+ vi.mocked(analyzeGitState).mockReturnValue(makeGitState());
1031
+ vi.mocked(detectScenario).mockReturnValue('main_clean_same');
1032
+ vi.mocked(newpr.isPrWorktreeScenario).mockReturnValue(false);
1033
+ vi.mocked(newpr.getScenarioContext).mockReturnValue({
1034
+ message: 'No changes detected',
1035
+ choices: [
1036
+ {
1037
+ label: 'Continue with empty initial commit',
1038
+ action: { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false },
1039
+ },
1040
+ { label: 'Cancel', action: null },
1041
+ ],
1042
+ });
1043
+ vi.mocked(newpr.getScenarioMessageLevel).mockReturnValue('warning');
1044
+ vi.mocked(prompts.promptChoiceIndex).mockResolvedValue(1);
1045
+ vi.mocked(newpr.isExistingBranchAction).mockReturnValue(false);
1046
+ vi.mocked(newpr.executeStateAction).mockReturnValue({ success: true, stashRef: null });
1047
+ vi.mocked(newpr.getBranchPoint).mockReturnValue('origin/main');
1048
+ vi.mocked(git.remoteBranchExists).mockReturnValue(false);
1049
+ vi.mocked(git.getCurrentBranch).mockReturnValue('main');
1050
+ vi.mocked(git.getStagedFiles).mockReturnValue([]);
1051
+ vi.mocked(github.createPr).mockReturnValue(makePrInfo({ number: 100 }));
1052
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr100');
1053
+ await runCli(['Test feature']);
1054
+ // git.exec for branch creation must use cwd option to ensure
1055
+ // branch is created in the correct worktree
1056
+ expect(git.exec).toHaveBeenCalledWith(['checkout', '-b', 'feat/test-feature', 'origin/main'], { cwd: repoRoot });
1057
+ });
1058
+ });
1059
+ describe('auto-link config files (linkConfigFiles feature)', () => {
1060
+ // Helper to set up all the mocks for a successful PR creation
1061
+ const setupPrCreationMocks = (configOverrides = {}) => {
1062
+ vi.mocked(newpr.parseArgs).mockReturnValue({
1063
+ kind: 'success',
1064
+ options: { mode: 'pr', prNumber: 123, ...defaultOptions },
1065
+ });
1066
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
1067
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
1068
+ vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1069
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
1070
+ vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1071
+ vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, ...configOverrides });
1072
+ vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1073
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1074
+ vi.mocked(fs.existsSync).mockReturnValue(false);
1075
+ };
1076
+ it('auto-links config files when linkConfigFiles is true', async () => {
1077
+ setupPrCreationMocks({ linkConfigFiles: true });
1078
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local', '.vscode/settings.json']);
1079
+ vi.mocked(runWtlink).mockResolvedValue(undefined);
1080
+ await runCli(['--pr', '123']);
1081
+ expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1082
+ expect(runWtlink).toHaveBeenCalledWith(expect.objectContaining({
1083
+ source: '/main-repo',
1084
+ destination: '/repo.pr123',
1085
+ dryRun: false,
1086
+ yes: true,
1087
+ }));
1088
+ expect(prompts.promptConfirm).not.toHaveBeenCalled();
1089
+ });
1090
+ it('skips linking when linkConfigFiles is false', async () => {
1091
+ setupPrCreationMocks({ linkConfigFiles: false });
1092
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local', '.vscode/settings.json']);
1093
+ await runCli(['--pr', '123']);
1094
+ expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1095
+ expect(runWtlink).not.toHaveBeenCalled();
1096
+ expect(prompts.promptConfirm).not.toHaveBeenCalled();
1097
+ });
1098
+ it('prompts user when linkConfigFiles is undefined in interactive mode', async () => {
1099
+ setupPrCreationMocks({ linkConfigFiles: undefined });
1100
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1101
+ vi.mocked(prompts.promptConfirm).mockResolvedValue(true);
1102
+ vi.mocked(runWtlink).mockResolvedValue(undefined);
1103
+ await runCli(['--pr', '123']);
1104
+ expect(getEnabledFiles).toHaveBeenCalledWith('/main-repo');
1105
+ expect(prompts.promptConfirm).toHaveBeenCalledWith(expect.stringContaining('Link these config files'), true);
1106
+ expect(runWtlink).toHaveBeenCalled();
1107
+ });
1108
+ it('skips linking when user declines the prompt', async () => {
1109
+ setupPrCreationMocks({ linkConfigFiles: undefined });
1110
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1111
+ vi.mocked(prompts.promptConfirm).mockResolvedValue(false);
1112
+ await runCli(['--pr', '123']);
1113
+ expect(prompts.promptConfirm).toHaveBeenCalled();
1114
+ expect(runWtlink).not.toHaveBeenCalled();
1115
+ });
1116
+ it('defaults to linking in non-interactive mode when linkConfigFiles is undefined', async () => {
1117
+ vi.mocked(newpr.parseArgs).mockReturnValue({
1118
+ kind: 'success',
1119
+ options: { mode: 'pr', prNumber: 123, ...defaultOptions, nonInteractive: true },
1120
+ });
1121
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
1122
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
1123
+ vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1124
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
1125
+ vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1126
+ vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, linkConfigFiles: undefined });
1127
+ vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1128
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1129
+ vi.mocked(fs.existsSync).mockReturnValue(false);
1130
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1131
+ vi.mocked(runWtlink).mockResolvedValue(undefined);
1132
+ await runCli(['--pr', '123', '--non-interactive']);
1133
+ expect(prompts.promptConfirm).not.toHaveBeenCalled();
1134
+ expect(runWtlink).toHaveBeenCalled();
1135
+ });
1136
+ it('defaults to linking in JSON mode when linkConfigFiles is undefined', async () => {
1137
+ vi.mocked(newpr.parseArgs).mockReturnValue({
1138
+ kind: 'success',
1139
+ options: { mode: 'pr', prNumber: 123, ...defaultOptions, json: true },
1140
+ });
1141
+ vi.mocked(github.isGhInstalled).mockReturnValue(true);
1142
+ vi.mocked(github.isAuthenticated).mockReturnValue(true);
1143
+ vi.mocked(git.getRepoRoot).mockReturnValue('/repo');
1144
+ vi.mocked(git.getRepoName).mockReturnValue('repo');
1145
+ vi.mocked(git.getMainWorktreeRoot).mockReturnValue('/main-repo');
1146
+ vi.mocked(loadConfig).mockReturnValue({ ...defaultConfig, linkConfigFiles: undefined });
1147
+ vi.mocked(github.getPr).mockReturnValue(makePrInfo());
1148
+ vi.mocked(generateWorktreePath).mockReturnValue('/repo.pr123');
1149
+ vi.mocked(fs.existsSync).mockReturnValue(false);
1150
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1151
+ vi.mocked(runWtlink).mockResolvedValue(undefined);
1152
+ await runCli(['--pr', '123', '--json']);
1153
+ expect(prompts.promptConfirm).not.toHaveBeenCalled();
1154
+ expect(runWtlink).toHaveBeenCalled();
1155
+ });
1156
+ it('does not link when there are no enabled files', async () => {
1157
+ setupPrCreationMocks({ linkConfigFiles: true });
1158
+ vi.mocked(getEnabledFiles).mockReturnValue([]);
1159
+ await runCli(['--pr', '123']);
1160
+ expect(runWtlink).not.toHaveBeenCalled();
1161
+ expect(prompts.promptConfirm).not.toHaveBeenCalled();
1162
+ });
1163
+ it('handles linking errors gracefully', async () => {
1164
+ setupPrCreationMocks({ linkConfigFiles: true });
1165
+ vi.mocked(getEnabledFiles).mockReturnValue(['.env.local']);
1166
+ vi.mocked(runWtlink).mockRejectedValue(new Error('Permission denied'));
1167
+ await runCli(['--pr', '123']);
1168
+ // Should not crash, just warn about the failure
1169
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Failed to link config files'));
1170
+ });
1171
+ it('skips config linking when main worktree cannot be determined', async () => {
1172
+ setupPrCreationMocks({ linkConfigFiles: true });
1173
+ vi.mocked(git.getMainWorktreeRoot).mockImplementation(() => {
1174
+ throw new Error('Not in a git worktree');
1175
+ });
1176
+ await runCli(['--pr', '123']);
1177
+ expect(getEnabledFiles).not.toHaveBeenCalled();
1178
+ expect(runWtlink).not.toHaveBeenCalled();
1179
+ });
1180
+ });
854
1181
  });
855
1182
  //# sourceMappingURL=newpr.test.js.map