@camaradesuk/git-worktree-tools 1.8.0 → 1.10.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 (353) hide show
  1. package/README.md +48 -27
  2. package/dist/cli/cleanpr.js +74 -53
  3. package/dist/cli/cleanpr.js.map +1 -1
  4. package/dist/cli/cleanpr.test.js +2 -0
  5. package/dist/cli/cleanpr.test.js.map +1 -1
  6. package/dist/cli/lswt.js +32 -56
  7. package/dist/cli/lswt.js.map +1 -1
  8. package/dist/cli/lswt.test.js +17 -27
  9. package/dist/cli/lswt.test.js.map +1 -1
  10. package/dist/cli/newpr.d.ts +13 -1
  11. package/dist/cli/newpr.d.ts.map +1 -1
  12. package/dist/cli/newpr.js +350 -151
  13. package/dist/cli/newpr.js.map +1 -1
  14. package/dist/cli/newpr.test.js +314 -5
  15. package/dist/cli/newpr.test.js.map +1 -1
  16. package/dist/cli/prs.d.ts +3 -10
  17. package/dist/cli/prs.d.ts.map +1 -1
  18. package/dist/cli/prs.js +6 -168
  19. package/dist/cli/prs.js.map +1 -1
  20. package/dist/cli/prs.test.js +55 -0
  21. package/dist/cli/prs.test.js.map +1 -1
  22. package/dist/cli/wt/clean.d.ts +6 -2
  23. package/dist/cli/wt/clean.d.ts.map +1 -1
  24. package/dist/cli/wt/clean.js +401 -20
  25. package/dist/cli/wt/clean.js.map +1 -1
  26. package/dist/cli/wt/clean.test.d.ts +8 -0
  27. package/dist/cli/wt/clean.test.d.ts.map +1 -0
  28. package/dist/cli/wt/clean.test.js +624 -0
  29. package/dist/cli/wt/clean.test.js.map +1 -0
  30. package/dist/cli/wt/completion.d.ts +3 -0
  31. package/dist/cli/wt/completion.d.ts.map +1 -1
  32. package/dist/cli/wt/completion.js +80 -9
  33. package/dist/cli/wt/completion.js.map +1 -1
  34. package/dist/cli/wt/completion.test.js +102 -0
  35. package/dist/cli/wt/completion.test.js.map +1 -1
  36. package/dist/cli/wt/config.d.ts +3 -1
  37. package/dist/cli/wt/config.d.ts.map +1 -1
  38. package/dist/cli/wt/config.js +323 -32
  39. package/dist/cli/wt/config.js.map +1 -1
  40. package/dist/cli/wt/config.test.d.ts +2 -0
  41. package/dist/cli/wt/config.test.d.ts.map +1 -1
  42. package/dist/cli/wt/config.test.js +206 -26
  43. package/dist/cli/wt/config.test.js.map +1 -1
  44. package/dist/cli/wt/interactive-menu.d.ts +2 -0
  45. package/dist/cli/wt/interactive-menu.d.ts.map +1 -1
  46. package/dist/cli/wt/interactive-menu.js +346 -73
  47. package/dist/cli/wt/interactive-menu.js.map +1 -1
  48. package/dist/cli/wt/interactive-menu.test.d.ts +4 -2
  49. package/dist/cli/wt/interactive-menu.test.d.ts.map +1 -1
  50. package/dist/cli/wt/interactive-menu.test.js +383 -323
  51. package/dist/cli/wt/interactive-menu.test.js.map +1 -1
  52. package/dist/cli/wt/link.d.ts +3 -1
  53. package/dist/cli/wt/link.d.ts.map +1 -1
  54. package/dist/cli/wt/link.js +125 -38
  55. package/dist/cli/wt/link.js.map +1 -1
  56. package/dist/cli/wt/list.d.ts +4 -1
  57. package/dist/cli/wt/list.d.ts.map +1 -1
  58. package/dist/cli/wt/list.js +85 -16
  59. package/dist/cli/wt/list.js.map +1 -1
  60. package/dist/cli/wt/list.test.d.ts +10 -0
  61. package/dist/cli/wt/list.test.d.ts.map +1 -0
  62. package/dist/cli/wt/list.test.js +157 -0
  63. package/dist/cli/wt/list.test.js.map +1 -0
  64. package/dist/cli/wt/new.d.ts +8 -2
  65. package/dist/cli/wt/new.d.ts.map +1 -1
  66. package/dist/cli/wt/new.js +91 -46
  67. package/dist/cli/wt/new.js.map +1 -1
  68. package/dist/cli/wt/prs.d.ts +2 -1
  69. package/dist/cli/wt/prs.d.ts.map +1 -1
  70. package/dist/cli/wt/prs.js +3 -164
  71. package/dist/cli/wt/prs.js.map +1 -1
  72. package/dist/cli/wt/run-command.d.ts +4 -2
  73. package/dist/cli/wt/run-command.d.ts.map +1 -1
  74. package/dist/cli/wt/run-command.js +6 -4
  75. package/dist/cli/wt/run-command.js.map +1 -1
  76. package/dist/cli/wt/state.d.ts +3 -1
  77. package/dist/cli/wt/state.d.ts.map +1 -1
  78. package/dist/cli/wt/state.js +74 -10
  79. package/dist/cli/wt/state.js.map +1 -1
  80. package/dist/cli/wt/state.test.d.ts +9 -0
  81. package/dist/cli/wt/state.test.d.ts.map +1 -0
  82. package/dist/cli/wt/state.test.js +127 -0
  83. package/dist/cli/wt/state.test.js.map +1 -0
  84. package/dist/cli/wt/wt.test.d.ts +2 -2
  85. package/dist/cli/wt/wt.test.js +430 -212
  86. package/dist/cli/wt/wt.test.js.map +1 -1
  87. package/dist/cli/wt.d.ts.map +1 -1
  88. package/dist/cli/wt.js +50 -36
  89. package/dist/cli/wt.js.map +1 -1
  90. package/dist/cli/wt.unit.test.js +16 -38
  91. package/dist/cli/wt.unit.test.js.map +1 -1
  92. package/dist/cli/wtconfig.d.ts +1 -0
  93. package/dist/cli/wtconfig.d.ts.map +1 -1
  94. package/dist/cli/wtconfig.js +213 -21
  95. package/dist/cli/wtconfig.js.map +1 -1
  96. package/dist/cli/wtconfig.test.js +3 -0
  97. package/dist/cli/wtconfig.test.js.map +1 -1
  98. package/dist/cli/wtlink.js +116 -73
  99. package/dist/cli/wtlink.js.map +1 -1
  100. package/dist/cli/wtstate.js +21 -2
  101. package/dist/cli/wtstate.js.map +1 -1
  102. package/dist/e2e/wt/interactive-menu.e2e.test.js +17 -17
  103. package/dist/e2e/wt/interactive-menu.e2e.test.js.map +1 -1
  104. package/dist/lib/ai/types.d.ts +12 -0
  105. package/dist/lib/ai/types.d.ts.map +1 -1
  106. package/dist/lib/ai/types.js.map +1 -1
  107. package/dist/lib/cleanpr/args.d.ts.map +1 -1
  108. package/dist/lib/cleanpr/args.js +20 -0
  109. package/dist/lib/cleanpr/args.js.map +1 -1
  110. package/dist/lib/cleanpr/types.d.ts +6 -0
  111. package/dist/lib/cleanpr/types.d.ts.map +1 -1
  112. package/dist/lib/cleanpr/worktree-info.d.ts.map +1 -1
  113. package/dist/lib/cleanpr/worktree-info.js +1 -6
  114. package/dist/lib/cleanpr/worktree-info.js.map +1 -1
  115. package/dist/lib/cleanpr/worktree-info.test.js +10 -13
  116. package/dist/lib/cleanpr/worktree-info.test.js.map +1 -1
  117. package/dist/lib/colors.d.ts +5 -0
  118. package/dist/lib/colors.d.ts.map +1 -1
  119. package/dist/lib/colors.js +13 -6
  120. package/dist/lib/colors.js.map +1 -1
  121. package/dist/lib/config-editor.d.ts.map +1 -1
  122. package/dist/lib/config-editor.js.map +1 -1
  123. package/dist/lib/config-migration/detector.d.ts +25 -0
  124. package/dist/lib/config-migration/detector.d.ts.map +1 -0
  125. package/dist/lib/config-migration/detector.js +372 -0
  126. package/dist/lib/config-migration/detector.js.map +1 -0
  127. package/dist/lib/config-migration/detector.test.d.ts +5 -0
  128. package/dist/lib/config-migration/detector.test.d.ts.map +1 -0
  129. package/dist/lib/config-migration/detector.test.js +201 -0
  130. package/dist/lib/config-migration/detector.test.js.map +1 -0
  131. package/dist/lib/config-migration/index.d.ts +29 -0
  132. package/dist/lib/config-migration/index.d.ts.map +1 -0
  133. package/dist/lib/config-migration/index.js +33 -0
  134. package/dist/lib/config-migration/index.js.map +1 -0
  135. package/dist/lib/config-migration/reporter.d.ts +53 -0
  136. package/dist/lib/config-migration/reporter.d.ts.map +1 -0
  137. package/dist/lib/config-migration/reporter.js +257 -0
  138. package/dist/lib/config-migration/reporter.js.map +1 -0
  139. package/dist/lib/config-migration/reporter.test.d.ts +5 -0
  140. package/dist/lib/config-migration/reporter.test.d.ts.map +1 -0
  141. package/dist/lib/config-migration/reporter.test.js +305 -0
  142. package/dist/lib/config-migration/reporter.test.js.map +1 -0
  143. package/dist/lib/config-migration/runner.d.ts +46 -0
  144. package/dist/lib/config-migration/runner.d.ts.map +1 -0
  145. package/dist/lib/config-migration/runner.js +364 -0
  146. package/dist/lib/config-migration/runner.js.map +1 -0
  147. package/dist/lib/config-migration/runner.test.d.ts +5 -0
  148. package/dist/lib/config-migration/runner.test.d.ts.map +1 -0
  149. package/dist/lib/config-migration/runner.test.js +235 -0
  150. package/dist/lib/config-migration/runner.test.js.map +1 -0
  151. package/dist/lib/config-migration/types.d.ts +120 -0
  152. package/dist/lib/config-migration/types.d.ts.map +1 -0
  153. package/dist/lib/config-migration/types.js +70 -0
  154. package/dist/lib/config-migration/types.js.map +1 -0
  155. package/dist/lib/config-validation.d.ts.map +1 -1
  156. package/dist/lib/config-validation.js +6 -0
  157. package/dist/lib/config-validation.js.map +1 -1
  158. package/dist/lib/config-validation.test.js +25 -0
  159. package/dist/lib/config-validation.test.js.map +1 -1
  160. package/dist/lib/config.d.ts +31 -7
  161. package/dist/lib/config.d.ts.map +1 -1
  162. package/dist/lib/config.js +2 -0
  163. package/dist/lib/config.js.map +1 -1
  164. package/dist/lib/config.test.js +3 -15
  165. package/dist/lib/config.test.js.map +1 -1
  166. package/dist/lib/constants.d.ts +12 -4
  167. package/dist/lib/constants.d.ts.map +1 -1
  168. package/dist/lib/constants.js +24 -5
  169. package/dist/lib/constants.js.map +1 -1
  170. package/dist/lib/constants.test.js +88 -29
  171. package/dist/lib/constants.test.js.map +1 -1
  172. package/dist/lib/deprecation.d.ts +18 -0
  173. package/dist/lib/deprecation.d.ts.map +1 -0
  174. package/dist/lib/deprecation.js +28 -0
  175. package/dist/lib/deprecation.js.map +1 -0
  176. package/dist/lib/deprecation.test.d.ts +2 -0
  177. package/dist/lib/deprecation.test.d.ts.map +1 -0
  178. package/dist/lib/deprecation.test.js +71 -0
  179. package/dist/lib/deprecation.test.js.map +1 -0
  180. package/dist/lib/hooks/confirmation.d.ts +49 -0
  181. package/dist/lib/hooks/confirmation.d.ts.map +1 -0
  182. package/dist/lib/hooks/confirmation.js +147 -0
  183. package/dist/lib/hooks/confirmation.js.map +1 -0
  184. package/dist/lib/hooks/confirmation.test.d.ts +7 -0
  185. package/dist/lib/hooks/confirmation.test.d.ts.map +1 -0
  186. package/dist/lib/hooks/confirmation.test.js +300 -0
  187. package/dist/lib/hooks/confirmation.test.js.map +1 -0
  188. package/dist/lib/hooks/executor.d.ts +16 -1
  189. package/dist/lib/hooks/executor.d.ts.map +1 -1
  190. package/dist/lib/hooks/executor.js +53 -4
  191. package/dist/lib/hooks/executor.js.map +1 -1
  192. package/dist/lib/hooks/index.d.ts +4 -2
  193. package/dist/lib/hooks/index.d.ts.map +1 -1
  194. package/dist/lib/hooks/index.js +3 -2
  195. package/dist/lib/hooks/index.js.map +1 -1
  196. package/dist/lib/hooks/types.d.ts +16 -0
  197. package/dist/lib/hooks/types.d.ts.map +1 -1
  198. package/dist/lib/hooks/types.js +12 -0
  199. package/dist/lib/hooks/types.js.map +1 -1
  200. package/dist/lib/logger.d.ts +40 -155
  201. package/dist/lib/logger.d.ts.map +1 -1
  202. package/dist/lib/logger.js +349 -420
  203. package/dist/lib/logger.js.map +1 -1
  204. package/dist/lib/logger.test.d.ts +10 -1
  205. package/dist/lib/logger.test.d.ts.map +1 -1
  206. package/dist/lib/logger.test.js +658 -258
  207. package/dist/lib/logger.test.js.map +1 -1
  208. package/dist/lib/lswt/action-executors.d.ts +2 -0
  209. package/dist/lib/lswt/action-executors.d.ts.map +1 -1
  210. package/dist/lib/lswt/action-executors.js +4 -3
  211. package/dist/lib/lswt/action-executors.js.map +1 -1
  212. package/dist/lib/lswt/action-executors.test.js +7 -0
  213. package/dist/lib/lswt/action-executors.test.js.map +1 -1
  214. package/dist/lib/lswt/args.d.ts.map +1 -1
  215. package/dist/lib/lswt/args.js +15 -1
  216. package/dist/lib/lswt/args.js.map +1 -1
  217. package/dist/lib/lswt/environment.d.ts +21 -2
  218. package/dist/lib/lswt/environment.d.ts.map +1 -1
  219. package/dist/lib/lswt/environment.js +73 -32
  220. package/dist/lib/lswt/environment.js.map +1 -1
  221. package/dist/lib/lswt/environment.test.js +79 -1
  222. package/dist/lib/lswt/environment.test.js.map +1 -1
  223. package/dist/lib/lswt/index.d.ts +1 -0
  224. package/dist/lib/lswt/index.d.ts.map +1 -1
  225. package/dist/lib/lswt/index.js +2 -0
  226. package/dist/lib/lswt/index.js.map +1 -1
  227. package/dist/lib/lswt/table.d.ts +15 -0
  228. package/dist/lib/lswt/table.d.ts.map +1 -0
  229. package/dist/lib/lswt/table.js +61 -0
  230. package/dist/lib/lswt/table.js.map +1 -0
  231. package/dist/lib/lswt/table.test.d.ts +5 -0
  232. package/dist/lib/lswt/table.test.d.ts.map +1 -0
  233. package/dist/lib/lswt/table.test.js +262 -0
  234. package/dist/lib/lswt/table.test.js.map +1 -0
  235. package/dist/lib/lswt/types.d.ts +4 -0
  236. package/dist/lib/lswt/types.d.ts.map +1 -1
  237. package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
  238. package/dist/lib/lswt/worktree-info.js +1 -6
  239. package/dist/lib/lswt/worktree-info.js.map +1 -1
  240. package/dist/lib/lswt/worktree-info.test.js +5 -17
  241. package/dist/lib/lswt/worktree-info.test.js.map +1 -1
  242. package/dist/lib/newpr/args.d.ts.map +1 -1
  243. package/dist/lib/newpr/args.js +36 -1
  244. package/dist/lib/newpr/args.js.map +1 -1
  245. package/dist/lib/newpr/hook-runner.d.ts +11 -0
  246. package/dist/lib/newpr/hook-runner.d.ts.map +1 -1
  247. package/dist/lib/newpr/hook-runner.js +49 -1
  248. package/dist/lib/newpr/hook-runner.js.map +1 -1
  249. package/dist/lib/newpr/hook-runner.test.js +121 -0
  250. package/dist/lib/newpr/hook-runner.test.js.map +1 -1
  251. package/dist/lib/newpr/plan-generator.d.ts +121 -0
  252. package/dist/lib/newpr/plan-generator.d.ts.map +1 -0
  253. package/dist/lib/newpr/plan-generator.js +185 -0
  254. package/dist/lib/newpr/plan-generator.js.map +1 -0
  255. package/dist/lib/newpr/plan-generator.test.d.ts +7 -0
  256. package/dist/lib/newpr/plan-generator.test.d.ts.map +1 -0
  257. package/dist/lib/newpr/plan-generator.test.js +387 -0
  258. package/dist/lib/newpr/plan-generator.test.js.map +1 -0
  259. package/dist/lib/newpr/types.d.ts +12 -0
  260. package/dist/lib/newpr/types.d.ts.map +1 -1
  261. package/dist/lib/prs/actions.d.ts +5 -1
  262. package/dist/lib/prs/actions.d.ts.map +1 -1
  263. package/dist/lib/prs/actions.js +12 -10
  264. package/dist/lib/prs/actions.js.map +1 -1
  265. package/dist/lib/prs/actions.test.js +48 -5
  266. package/dist/lib/prs/actions.test.js.map +1 -1
  267. package/dist/lib/prs/command.d.ts +21 -0
  268. package/dist/lib/prs/command.d.ts.map +1 -0
  269. package/dist/lib/prs/command.js +175 -0
  270. package/dist/lib/prs/command.js.map +1 -0
  271. package/dist/lib/prs/command.test.d.ts +11 -0
  272. package/dist/lib/prs/command.test.d.ts.map +1 -0
  273. package/dist/lib/prs/command.test.js +409 -0
  274. package/dist/lib/prs/command.test.js.map +1 -0
  275. package/dist/lib/prs/interactive.d.ts.map +1 -1
  276. package/dist/lib/prs/interactive.js +15 -2
  277. package/dist/lib/prs/interactive.js.map +1 -1
  278. package/dist/lib/prs/interactive.test.js +153 -0
  279. package/dist/lib/prs/interactive.test.js.map +1 -1
  280. package/dist/lib/prs/types.d.ts +15 -0
  281. package/dist/lib/prs/types.d.ts.map +1 -1
  282. package/dist/lib/ui/error.d.ts +31 -0
  283. package/dist/lib/ui/error.d.ts.map +1 -0
  284. package/dist/lib/ui/error.js +47 -0
  285. package/dist/lib/ui/error.js.map +1 -0
  286. package/dist/lib/ui/error.test.d.ts +2 -0
  287. package/dist/lib/ui/error.test.d.ts.map +1 -0
  288. package/dist/lib/ui/error.test.js +143 -0
  289. package/dist/lib/ui/error.test.js.map +1 -0
  290. package/dist/lib/ui/index.d.ts +15 -0
  291. package/dist/lib/ui/index.d.ts.map +1 -0
  292. package/dist/lib/ui/index.js +19 -0
  293. package/dist/lib/ui/index.js.map +1 -0
  294. package/dist/lib/ui/output.d.ts +18 -0
  295. package/dist/lib/ui/output.d.ts.map +1 -0
  296. package/dist/lib/ui/output.js +31 -0
  297. package/dist/lib/ui/output.js.map +1 -0
  298. package/dist/lib/ui/output.test.d.ts +2 -0
  299. package/dist/lib/ui/output.test.d.ts.map +1 -0
  300. package/dist/lib/ui/output.test.js +59 -0
  301. package/dist/lib/ui/output.test.js.map +1 -0
  302. package/dist/lib/ui/spinner.d.ts +10 -0
  303. package/dist/lib/ui/spinner.d.ts.map +1 -0
  304. package/dist/lib/ui/spinner.js +10 -0
  305. package/dist/lib/ui/spinner.js.map +1 -0
  306. package/dist/lib/ui/status.d.ts +65 -0
  307. package/dist/lib/ui/status.d.ts.map +1 -0
  308. package/dist/lib/ui/status.js +100 -0
  309. package/dist/lib/ui/status.js.map +1 -0
  310. package/dist/lib/ui/status.test.d.ts +2 -0
  311. package/dist/lib/ui/status.test.d.ts.map +1 -0
  312. package/dist/lib/ui/status.test.js +158 -0
  313. package/dist/lib/ui/status.test.js.map +1 -0
  314. package/dist/lib/ui/table.d.ts +39 -0
  315. package/dist/lib/ui/table.d.ts.map +1 -0
  316. package/dist/lib/ui/table.js +45 -0
  317. package/dist/lib/ui/table.js.map +1 -0
  318. package/dist/lib/ui/table.test.d.ts +2 -0
  319. package/dist/lib/ui/table.test.d.ts.map +1 -0
  320. package/dist/lib/ui/table.test.js +115 -0
  321. package/dist/lib/ui/table.test.js.map +1 -0
  322. package/dist/lib/ui/theme.d.ts +34 -0
  323. package/dist/lib/ui/theme.d.ts.map +1 -0
  324. package/dist/lib/ui/theme.js +37 -0
  325. package/dist/lib/ui/theme.js.map +1 -0
  326. package/dist/lib/ui/theme.test.d.ts +2 -0
  327. package/dist/lib/ui/theme.test.d.ts.map +1 -0
  328. package/dist/lib/ui/theme.test.js +76 -0
  329. package/dist/lib/ui/theme.test.js.map +1 -0
  330. package/dist/lib/wtconfig/environment.d.ts +18 -1
  331. package/dist/lib/wtconfig/environment.d.ts.map +1 -1
  332. package/dist/lib/wtconfig/environment.js +60 -24
  333. package/dist/lib/wtconfig/environment.js.map +1 -1
  334. package/dist/lib/wtconfig/environment.test.js +45 -1
  335. package/dist/lib/wtconfig/environment.test.js.map +1 -1
  336. package/dist/lib/wtlink/config-manifest.test.js +26 -0
  337. package/dist/lib/wtlink/config-manifest.test.js.map +1 -1
  338. package/dist/lib/wtlink/link-configs.js +7 -7
  339. package/dist/lib/wtlink/link-configs.js.map +1 -1
  340. package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -1
  341. package/dist/lib/wtlink/validate-manifest.js +5 -5
  342. package/dist/lib/wtlink/validate-manifest.js.map +1 -1
  343. package/dist/lib/wtstate/args.d.ts.map +1 -1
  344. package/dist/lib/wtstate/args.js +2 -0
  345. package/dist/lib/wtstate/args.js.map +1 -1
  346. package/dist/mcp/server.d.ts +2 -1
  347. package/dist/mcp/server.d.ts.map +1 -1
  348. package/dist/mcp/server.js +264 -44
  349. package/dist/mcp/server.js.map +1 -1
  350. package/dist/mcp/server.test.js +111 -0
  351. package/dist/mcp/server.test.js.map +1 -1
  352. package/package.json +3 -1
  353. package/schemas/worktreerc.schema.json +23 -0
@@ -1,12 +1,180 @@
1
1
  /**
2
2
  * Tests for wt unified command handlers
3
3
  *
4
- * These are thin wrappers around spawnSync that delegate to the underlying CLI tools.
5
- * We test that each handler correctly builds the argument array and spawns the right tool.
4
+ * All commands are migrated to direct library calls (list, state, clean, new, link)
5
+ * and tested by mocking the library modules they call.
6
6
  */
7
7
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
8
  import { spawnSync } from 'child_process';
9
9
  import yargs from 'yargs';
10
+ vi.mock('child_process', () => ({
11
+ spawnSync: vi.fn(() => ({ status: 0 })),
12
+ execSync: vi.fn(),
13
+ }));
14
+ // Mock newpr.ts for new command (direct library call)
15
+ vi.mock('../newpr.js', () => ({
16
+ runNewprHandler: vi.fn().mockResolvedValue(undefined),
17
+ }));
18
+ // Mock library dependencies for list command (direct library calls)
19
+ vi.mock('../../lib/lswt/index.js', () => ({
20
+ gatherWorktreeInfo: vi.fn().mockResolvedValue([]),
21
+ createDefaultDeps: vi.fn().mockReturnValue({}),
22
+ formatJsonOutput: vi.fn().mockReturnValue('[]'),
23
+ runInteractiveMode: vi.fn().mockResolvedValue(undefined),
24
+ printWorktreeTable: vi.fn(),
25
+ parseArgs: vi.fn(),
26
+ getHelpText: vi.fn(),
27
+ formatTypeLabel: vi.fn(),
28
+ getDisplayPath: vi.fn(),
29
+ sortWorktrees: vi.fn(),
30
+ extractPrNumber: vi.fn(),
31
+ isMainWorktree: vi.fn(),
32
+ }));
33
+ // Mock library dependencies for state command (direct library calls)
34
+ vi.mock('../../lib/wtstate/index.js', () => ({
35
+ analyzeState: vi.fn().mockReturnValue({
36
+ scenario: 'main_clean_same',
37
+ scenarioDescription: 'On main branch, same as origin/main, no changes',
38
+ currentBranch: 'main',
39
+ baseBranch: 'main',
40
+ worktreeType: 'main_worktree',
41
+ hasChanges: false,
42
+ hasStagedChanges: false,
43
+ hasUnstagedChanges: false,
44
+ localCommits: [],
45
+ stagedFiles: [],
46
+ unstagedFiles: [],
47
+ availableActions: [],
48
+ recommendedAction: null,
49
+ }),
50
+ formatText: vi.fn().mockReturnValue('State: main_clean_same'),
51
+ parseArgs: vi.fn(),
52
+ getHelpText: vi.fn(),
53
+ getDefaultOptions: vi.fn(),
54
+ }));
55
+ // Mock library dependencies for clean command (direct library calls)
56
+ vi.mock('../../lib/cleanpr/index.js', () => ({
57
+ gatherPrWorktreeInfo: vi.fn().mockResolvedValue([]),
58
+ createDefaultDeps: vi.fn().mockReturnValue({}),
59
+ groupWorktreesByState: vi.fn().mockReturnValue({ merged: [], closed: [], open: [], unknown: [] }),
60
+ getCleanableWorktrees: vi.fn().mockReturnValue([]),
61
+ findWorktreeByPrNumber: vi.fn().mockReturnValue(null),
62
+ cleanWorktree: vi.fn().mockReturnValue({
63
+ success: true,
64
+ prNumber: 42,
65
+ message: 'Cleaned',
66
+ localBranchDeleted: true,
67
+ remoteBranchDeleted: false,
68
+ }),
69
+ summarizeResults: vi.fn().mockReturnValue({ cleaned: 0, total: 0, failed: 0 }),
70
+ }));
71
+ // Mock config module for clean command
72
+ vi.mock('../../lib/config.js', () => ({
73
+ loadConfig: vi.fn().mockReturnValue({ worktreePattern: '{repo}.pr{number}', baseBranch: 'main' }),
74
+ loadConfigWithValidation: vi.fn().mockReturnValue({ config: {}, validation: null }),
75
+ getDefaultConfig: vi.fn().mockReturnValue({}),
76
+ getConfigPath: vi.fn().mockReturnValue(null),
77
+ }));
78
+ // Mock logger for clean command
79
+ vi.mock('../../lib/logger.js', () => ({
80
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
81
+ }));
82
+ // Mock prompts module for clean command
83
+ vi.mock('../../lib/prompts.js', () => ({
84
+ withSpinner: vi.fn((_msg, fn) => fn()),
85
+ promptChoice: vi.fn(),
86
+ promptConfirm: vi.fn(),
87
+ }));
88
+ // Mock colors module for clean command
89
+ vi.mock('../../lib/colors.js', () => ({
90
+ error: vi.fn((s) => s),
91
+ dim: vi.fn((s) => s),
92
+ success: vi.fn((s) => s),
93
+ info: vi.fn((s) => s),
94
+ cyan: vi.fn((s) => s),
95
+ yellow: vi.fn((s) => s),
96
+ red: vi.fn((s) => s),
97
+ green: vi.fn((s) => s),
98
+ bold: vi.fn((s) => s),
99
+ warning: vi.fn((s) => s),
100
+ }));
101
+ // Mock config-editor for config command
102
+ vi.mock('../../lib/config-editor.js', () => ({
103
+ runConfigEditor: vi.fn().mockResolvedValue({ saved: false }),
104
+ quickEditConfig: vi.fn().mockResolvedValue({ saved: false }),
105
+ }));
106
+ // Mock config-validation for config command
107
+ vi.mock('../../lib/config-validation.js', () => ({
108
+ formatValidationErrors: vi.fn().mockReturnValue('formatted errors'),
109
+ }));
110
+ // Mock global-config for config command
111
+ vi.mock('../../lib/global-config.js', () => ({
112
+ getSchemaUrl: vi.fn().mockReturnValue('https://example.com/schema.json'),
113
+ }));
114
+ // Mock library dependencies for link command (direct library calls)
115
+ vi.mock('../../lib/wtlink/manage-manifest.js', () => ({
116
+ run: vi.fn().mockResolvedValue(undefined),
117
+ }));
118
+ vi.mock('../../lib/wtlink/link-configs.js', () => ({
119
+ run: vi.fn().mockResolvedValue(undefined),
120
+ }));
121
+ vi.mock('../../lib/wtlink/validate-manifest.js', () => ({
122
+ run: vi.fn(),
123
+ }));
124
+ vi.mock('../../lib/wtlink/main-menu.js', () => ({
125
+ showMainMenu: vi.fn().mockResolvedValue(undefined),
126
+ }));
127
+ vi.mock('../../lib/wtlink/config-manifest.js', () => ({
128
+ hasLegacyManifest: vi.fn().mockReturnValue(false),
129
+ }));
130
+ vi.mock('../../lib/config-migration/index.js', () => ({
131
+ detectMigrationIssues: vi.fn().mockReturnValue({ issues: [] }),
132
+ runMigration: vi.fn().mockResolvedValue({ success: true, errors: [] }),
133
+ formatMigrationReport: vi.fn().mockReturnValue(''),
134
+ }));
135
+ vi.mock('../../lib/errors.js', () => ({
136
+ ManifestError: class ManifestError extends Error {
137
+ issues;
138
+ constructor(message, issues) {
139
+ super(message);
140
+ this.name = 'ManifestError';
141
+ this.issues = issues;
142
+ }
143
+ },
144
+ }));
145
+ // Mock git module for list/state/clean/link handlers
146
+ vi.mock('../../lib/git.js', () => ({
147
+ getRepoRoot: vi.fn().mockReturnValue('/fake/repo'),
148
+ getMainWorktreeRoot: vi.fn().mockReturnValue('/fake/repo'),
149
+ removeWorktree: vi.fn(),
150
+ pruneWorktrees: vi.fn(),
151
+ }));
152
+ // Mock github module for list/clean handlers
153
+ vi.mock('../../lib/github.js', () => ({
154
+ isGhInstalled: vi.fn().mockReturnValue(true),
155
+ }));
156
+ // Mock UI module for all handlers
157
+ vi.mock('../../lib/ui/index.js', () => ({
158
+ setJsonMode: vi.fn(),
159
+ isJsonMode: vi.fn().mockReturnValue(false),
160
+ printStatus: vi.fn(),
161
+ printDim: vi.fn(),
162
+ printError: vi.fn(),
163
+ printHeader: vi.fn(),
164
+ printNextSteps: vi.fn(),
165
+ changeIndicator: vi.fn().mockReturnValue(''),
166
+ errorToDisplay: vi.fn().mockReturnValue({ title: 'error' }),
167
+ }));
168
+ // Mock json-output module (partial mock to preserve exports for all command handlers)
169
+ vi.mock('../../lib/json-output.js', async (importOriginal) => {
170
+ const actual = await importOriginal();
171
+ return {
172
+ ...actual,
173
+ createSuccessResult: vi.fn().mockReturnValue({ success: true }),
174
+ createErrorResult: vi.fn().mockReturnValue({ success: false }),
175
+ formatJsonResult: vi.fn().mockReturnValue('{}'),
176
+ };
177
+ });
10
178
  // Import all commands statically so coverage is tracked
11
179
  import { newCommand } from './new.js';
12
180
  import { listCommand } from './list.js';
@@ -14,15 +182,24 @@ import { cleanCommand } from './clean.js';
14
182
  import { stateCommand } from './state.js';
15
183
  import { configCommand } from './config.js';
16
184
  import { linkCommand } from './link.js';
17
- vi.mock('child_process', () => ({
18
- spawnSync: vi.fn(() => ({ status: 0 })),
19
- }));
185
+ // Import mocked modules for assertions
186
+ import { gatherWorktreeInfo, printWorktreeTable, formatJsonOutput } from '../../lib/lswt/index.js';
187
+ import { analyzeState, formatText } from '../../lib/wtstate/index.js';
188
+ import { gatherPrWorktreeInfo, getCleanableWorktrees } from '../../lib/cleanpr/index.js';
189
+ import { setJsonMode, printError } from '../../lib/ui/index.js';
190
+ import { createSuccessResult, formatJsonResult } from '../../lib/json-output.js';
191
+ import { runNewprHandler } from '../newpr.js';
192
+ import { run as manageRun } from '../../lib/wtlink/manage-manifest.js';
193
+ import { run as linkRun } from '../../lib/wtlink/link-configs.js';
194
+ import { run as validateRun } from '../../lib/wtlink/validate-manifest.js';
195
+ import { showMainMenu } from '../../lib/wtlink/main-menu.js';
196
+ import * as git from '../../lib/git.js';
197
+ import * as github from '../../lib/github.js';
20
198
  // Mock process.exit to prevent test from exiting
21
199
  const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
22
200
  // Helper to invoke builder for coverage
23
- function invokeBuilder(
24
201
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- command, args) {
202
+ function invokeBuilder(command, args) {
26
203
  const parser = yargs(args);
27
204
  if (typeof command.builder === 'function') {
28
205
  command.builder(parser);
@@ -42,154 +219,195 @@ describe('wt subcommand handlers', () => {
42
219
  });
43
220
  it('builder registers all expected options', () => {
44
221
  invokeBuilder(newCommand, []);
45
- expect(true).toBe(true); // Builder executed for coverage
46
- // Verify builder returns a yargs instance with registered options
47
- // The builder function is invoked for coverage
222
+ expect(true).toBe(true);
48
223
  });
49
- it('passes description to newpr', () => {
50
- newCommand.handler({
224
+ it('calls runNewprHandler with description', async () => {
225
+ await newCommand.handler({
51
226
  description: 'Add dark mode',
52
227
  json: false,
53
228
  'non-interactive': false,
54
- 'stash-untracked': false,
229
+ draft: false,
55
230
  });
56
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['Add dark mode']), expect.any(Object));
57
- expect(mockExit).toHaveBeenCalledWith(0);
231
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
232
+ mode: 'new',
233
+ description: 'Add dark mode',
234
+ }));
58
235
  });
59
- it('passes --pr flag to newpr', () => {
60
- newCommand.handler({
236
+ it('calls runNewprHandler with pr mode for --pr flag', async () => {
237
+ await newCommand.handler({
61
238
  pr: 42,
62
239
  json: false,
63
240
  'non-interactive': false,
64
- 'stash-untracked': false,
241
+ draft: false,
65
242
  });
66
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--pr', '42']), expect.any(Object));
243
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
244
+ mode: 'pr',
245
+ prNumber: 42,
246
+ }));
67
247
  });
68
- it('passes --ready flag to newpr', () => {
69
- newCommand.handler({
248
+ it('maps --ready flag to draft=false and draftExplicitlySet=true', async () => {
249
+ await newCommand.handler({
70
250
  ready: true,
71
251
  json: false,
72
252
  'non-interactive': false,
73
- 'stash-untracked': false,
253
+ draft: false,
74
254
  install: false,
75
255
  code: false,
76
256
  'no-wtlink': false,
77
257
  'no-hooks': false,
78
258
  });
79
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--ready']), expect.any(Object));
259
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
260
+ draft: false,
261
+ draftExplicitlySet: true,
262
+ }));
80
263
  });
81
- it('passes --base flag to newpr', () => {
82
- newCommand.handler({
264
+ it('maps --base flag to baseBranch option', async () => {
265
+ await newCommand.handler({
83
266
  base: 'develop',
84
267
  json: false,
85
268
  'non-interactive': false,
86
- 'stash-untracked': false,
269
+ draft: false,
87
270
  install: false,
88
271
  code: false,
89
272
  ready: false,
90
273
  'no-wtlink': false,
91
274
  'no-hooks': false,
92
275
  });
93
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--base', 'develop']), expect.any(Object));
276
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ baseBranch: 'develop' }));
94
277
  });
95
- it('passes --branch flag to newpr', () => {
96
- newCommand.handler({
278
+ it('maps --branch flag to branch mode', async () => {
279
+ await newCommand.handler({
97
280
  branch: 'feat/my-feature',
98
281
  json: false,
99
282
  'non-interactive': false,
100
- 'stash-untracked': false,
283
+ draft: false,
101
284
  install: false,
102
285
  code: false,
103
286
  ready: false,
104
287
  'no-wtlink': false,
105
288
  'no-hooks': false,
106
289
  });
107
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--branch', 'feat/my-feature']), expect.any(Object));
290
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
291
+ mode: 'branch',
292
+ branchName: 'feat/my-feature',
293
+ }));
108
294
  });
109
- it('passes --install flag to newpr', () => {
110
- newCommand.handler({
295
+ it('maps --install flag to installDeps option', async () => {
296
+ await newCommand.handler({
111
297
  install: true,
112
298
  json: false,
113
299
  'non-interactive': false,
114
- 'stash-untracked': false,
300
+ draft: false,
115
301
  code: false,
116
302
  ready: false,
117
303
  'no-wtlink': false,
118
304
  'no-hooks': false,
119
305
  });
120
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--install']), expect.any(Object));
306
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ installDeps: true }));
121
307
  });
122
- it('passes --code flag to newpr', () => {
123
- newCommand.handler({
308
+ it('maps --code flag to openEditor option', async () => {
309
+ await newCommand.handler({
124
310
  code: true,
125
311
  json: false,
126
312
  'non-interactive': false,
127
- 'stash-untracked': false,
313
+ draft: false,
128
314
  install: false,
129
315
  ready: false,
130
316
  'no-wtlink': false,
131
317
  'no-hooks': false,
132
318
  });
133
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--code']), expect.any(Object));
319
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ openEditor: true }));
134
320
  });
135
- it('passes --no-wtlink flag to newpr', () => {
136
- newCommand.handler({
321
+ it('maps --no-wtlink flag to runWtlink=false option', async () => {
322
+ await newCommand.handler({
137
323
  'no-wtlink': true,
138
324
  json: false,
139
325
  'non-interactive': false,
140
- 'stash-untracked': false,
326
+ draft: false,
141
327
  install: false,
142
328
  code: false,
143
329
  ready: false,
144
330
  'no-hooks': false,
145
331
  });
146
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--no-wtlink']), expect.any(Object));
332
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ runWtlink: false }));
147
333
  });
148
- it('passes --no-hooks flag to newpr', () => {
149
- newCommand.handler({
334
+ it('maps --no-hooks flag to noHooks option', async () => {
335
+ await newCommand.handler({
150
336
  'no-hooks': true,
151
337
  json: false,
152
338
  'non-interactive': false,
153
- 'stash-untracked': false,
339
+ draft: false,
154
340
  install: false,
155
341
  code: false,
156
342
  ready: false,
157
343
  'no-wtlink': false,
158
344
  });
159
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--no-hooks']), expect.any(Object));
345
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ noHooks: true }));
160
346
  });
161
- it('passes --json flag to newpr', () => {
162
- newCommand.handler({
347
+ it('maps --json flag to json option', async () => {
348
+ await newCommand.handler({
163
349
  json: true,
164
350
  'non-interactive': false,
165
- 'stash-untracked': false,
351
+ draft: false,
166
352
  });
167
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--json']), expect.any(Object));
353
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ json: true }));
354
+ expect(setJsonMode).toHaveBeenCalledWith(true);
168
355
  });
169
- it('passes --non-interactive flag to newpr', () => {
170
- newCommand.handler({
356
+ it('maps --non-interactive flag to nonInteractive option', async () => {
357
+ await newCommand.handler({
171
358
  json: false,
172
359
  'non-interactive': true,
173
- 'stash-untracked': false,
360
+ draft: false,
174
361
  });
175
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--non-interactive']), expect.any(Object));
362
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ nonInteractive: true }));
176
363
  });
177
- it('passes --action flag to newpr', () => {
178
- newCommand.handler({
364
+ it('maps --action flag to action option', async () => {
365
+ await newCommand.handler({
179
366
  action: 'commit_all',
180
367
  json: false,
181
368
  'non-interactive': false,
182
- 'stash-untracked': false,
369
+ draft: false,
370
+ });
371
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ action: 'commit_all' }));
372
+ });
373
+ it('maps --draft flag to draft=true and draftExplicitlySet=true', async () => {
374
+ await newCommand.handler({
375
+ json: false,
376
+ 'non-interactive': false,
377
+ draft: true,
378
+ });
379
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
380
+ draft: true,
381
+ draftExplicitlySet: true,
382
+ }));
383
+ });
384
+ it('maps --plan flag to generatePlan option', async () => {
385
+ await newCommand.handler({
386
+ json: false,
387
+ 'non-interactive': false,
388
+ draft: false,
389
+ plan: true,
390
+ });
391
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ generatePlan: true }));
392
+ });
393
+ it('maps --confirm-hooks flag to confirmHooks option', async () => {
394
+ await newCommand.handler({
395
+ json: false,
396
+ 'non-interactive': false,
397
+ draft: false,
398
+ 'confirm-hooks': true,
183
399
  });
184
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--action', 'commit_all']), expect.any(Object));
400
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({ confirmHooks: true }));
185
401
  });
186
- it('passes --stash-untracked flag to newpr', () => {
187
- newCommand.handler({
402
+ it('does not spawn a child process', async () => {
403
+ vi.mocked(spawnSync).mockClear();
404
+ await newCommand.handler({
405
+ description: 'Test',
188
406
  json: false,
189
407
  'non-interactive': false,
190
- 'stash-untracked': true,
408
+ draft: false,
191
409
  });
192
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--stash-untracked']), expect.any(Object));
410
+ expect(spawnSync).not.toHaveBeenCalled();
193
411
  });
194
412
  });
195
413
  describe('listCommand', () => {
@@ -199,53 +417,39 @@ describe('wt subcommand handlers', () => {
199
417
  });
200
418
  it('builder registers all expected options', () => {
201
419
  invokeBuilder(listCommand, []);
202
- expect(true).toBe(true); // Builder executed for coverage
420
+ expect(true).toBe(true);
203
421
  });
204
- it('passes --verbose flag to lswt', () => {
205
- listCommand.handler({
206
- verbose: true,
207
- json: false,
208
- 'no-interactive': false,
209
- status: false,
210
- });
211
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--verbose']), expect.any(Object));
422
+ it('calls gatherWorktreeInfo with verbose option', async () => {
423
+ await listCommand.handler({ verbose: true, json: false, status: false });
424
+ expect(gatherWorktreeInfo).toHaveBeenCalledWith('/fake/repo', expect.objectContaining({ verbose: true }), expect.any(Object));
212
425
  });
213
- it('passes --json flag to lswt', () => {
214
- listCommand.handler({
215
- json: true,
216
- verbose: false,
217
- 'no-interactive': false,
218
- status: false,
219
- });
220
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--json']), expect.any(Object));
426
+ it('calls setJsonMode and formatJsonOutput for --json', async () => {
427
+ await listCommand.handler({ json: true, verbose: false, status: false });
428
+ expect(setJsonMode).toHaveBeenCalledWith(true);
429
+ expect(formatJsonOutput).toHaveBeenCalled();
221
430
  });
222
- it('passes --no-interactive flag to lswt', () => {
223
- listCommand.handler({
224
- 'no-interactive': true,
431
+ it('calls printWorktreeTable for non-interactive non-json output', async () => {
432
+ await listCommand.handler({
225
433
  json: false,
226
434
  verbose: false,
227
435
  status: false,
436
+ interactive: false,
228
437
  });
229
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--no-interactive']), expect.any(Object));
438
+ expect(printWorktreeTable).toHaveBeenCalled();
230
439
  });
231
- it('passes --interactive flag to lswt', () => {
232
- listCommand.handler({
233
- interactive: true,
234
- json: false,
235
- verbose: false,
236
- status: false,
237
- 'no-interactive': false,
238
- });
239
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--interactive']), expect.any(Object));
440
+ it('passes status option to gatherWorktreeInfo', async () => {
441
+ await listCommand.handler({ status: true, json: false, verbose: false });
442
+ expect(gatherWorktreeInfo).toHaveBeenCalledWith('/fake/repo', expect.objectContaining({ showStatus: true }), expect.any(Object));
240
443
  });
241
- it('passes --status flag to lswt', () => {
242
- listCommand.handler({
243
- status: true,
444
+ it('does not spawn a child process', async () => {
445
+ vi.mocked(spawnSync).mockClear();
446
+ await listCommand.handler({
244
447
  json: false,
245
448
  verbose: false,
246
- 'no-interactive': false,
449
+ status: false,
450
+ interactive: false,
247
451
  });
248
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--status']), expect.any(Object));
452
+ expect(spawnSync).not.toHaveBeenCalled();
249
453
  });
250
454
  });
251
455
  describe('cleanCommand', () => {
@@ -255,64 +459,58 @@ describe('wt subcommand handlers', () => {
255
459
  });
256
460
  it('builder registers all expected options', () => {
257
461
  invokeBuilder(cleanCommand, []);
258
- expect(true).toBe(true); // Builder executed for coverage
462
+ expect(true).toBe(true);
259
463
  });
260
- it('passes pr-number to cleanpr', () => {
261
- cleanCommand.handler({
262
- prNumber: 42,
263
- all: false,
464
+ it('calls gatherPrWorktreeInfo for --all mode', async () => {
465
+ await cleanCommand.handler({
466
+ all: true,
264
467
  'dry-run': false,
265
468
  force: false,
266
469
  json: false,
267
470
  });
268
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['42']), expect.any(Object));
471
+ expect(gatherPrWorktreeInfo).toHaveBeenCalledWith('/fake/repo', '{repo}.pr{number}', expect.any(Object));
472
+ expect(getCleanableWorktrees).toHaveBeenCalled();
269
473
  });
270
- it('passes --all flag to cleanpr', () => {
271
- cleanCommand.handler({
474
+ it('calls setJsonMode when --json is passed', async () => {
475
+ await cleanCommand.handler({
272
476
  all: true,
273
477
  'dry-run': false,
274
478
  force: false,
275
- json: false,
479
+ json: true,
276
480
  });
277
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--all']), expect.any(Object));
481
+ expect(setJsonMode).toHaveBeenCalledWith(true);
278
482
  });
279
- it('passes --dry-run flag to cleanpr', () => {
280
- cleanCommand.handler({
281
- 'dry-run': true,
483
+ it('handles gh not installed error', async () => {
484
+ vi.mocked(github.isGhInstalled).mockReturnValueOnce(false);
485
+ await cleanCommand.handler({
282
486
  all: false,
487
+ 'dry-run': false,
283
488
  force: false,
284
489
  json: false,
285
490
  });
286
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--dry-run']), expect.any(Object));
491
+ expect(printError).toHaveBeenCalledWith(expect.objectContaining({ title: expect.stringContaining('GitHub CLI') }));
492
+ expect(mockExit).toHaveBeenCalledWith(1);
287
493
  });
288
- it('passes --force flag to cleanpr', () => {
289
- cleanCommand.handler({
290
- force: true,
494
+ it('handles not in git repo error', async () => {
495
+ vi.mocked(git.getRepoRoot).mockReturnValueOnce(null);
496
+ await cleanCommand.handler({
291
497
  all: false,
292
498
  'dry-run': false,
499
+ force: false,
293
500
  json: false,
294
501
  });
295
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--force']), expect.any(Object));
502
+ expect(printError).toHaveBeenCalledWith(expect.objectContaining({ title: expect.stringContaining('git repository') }));
503
+ expect(mockExit).toHaveBeenCalledWith(1);
296
504
  });
297
- it('passes --json flag to cleanpr', () => {
298
- cleanCommand.handler({
299
- json: true,
300
- all: false,
505
+ it('does not spawn a child process', async () => {
506
+ vi.mocked(spawnSync).mockClear();
507
+ await cleanCommand.handler({
508
+ all: true,
301
509
  'dry-run': false,
302
510
  force: false,
303
- remote: false,
304
- });
305
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--json']), expect.any(Object));
306
- });
307
- it('passes --remote flag to cleanpr', () => {
308
- cleanCommand.handler({
309
- remote: true,
310
511
  json: false,
311
- all: false,
312
- 'dry-run': false,
313
- force: false,
314
512
  });
315
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--remote']), expect.any(Object));
513
+ expect(spawnSync).not.toHaveBeenCalled();
316
514
  });
317
515
  });
318
516
  describe('stateCommand', () => {
@@ -322,21 +520,31 @@ describe('wt subcommand handlers', () => {
322
520
  });
323
521
  it('builder registers all expected options', () => {
324
522
  invokeBuilder(stateCommand, []);
325
- expect(true).toBe(true); // Builder executed for coverage
523
+ expect(true).toBe(true);
326
524
  });
327
- it('passes --json flag to wtstate', () => {
328
- stateCommand.handler({
329
- json: true,
330
- verbose: false,
331
- });
332
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--json']), expect.any(Object));
525
+ it('calls analyzeState and formatJsonResult for --json', async () => {
526
+ await stateCommand.handler({ json: true, verbose: false });
527
+ expect(analyzeState).toHaveBeenCalledWith(expect.objectContaining({ json: true, verbose: false }));
528
+ expect(createSuccessResult).toHaveBeenCalledWith('wtstate', expect.any(Object));
529
+ expect(formatJsonResult).toHaveBeenCalled();
333
530
  });
334
- it('passes --verbose flag to wtstate', () => {
335
- stateCommand.handler({
336
- verbose: true,
531
+ it('calls analyzeState and formatText for text output', async () => {
532
+ await stateCommand.handler({ verbose: true, json: false });
533
+ expect(analyzeState).toHaveBeenCalledWith(expect.objectContaining({ verbose: true, json: false }));
534
+ expect(formatText).toHaveBeenCalled();
535
+ });
536
+ it('passes base-branch option to analyzeState', async () => {
537
+ await stateCommand.handler({
538
+ verbose: false,
337
539
  json: false,
540
+ 'base-branch': 'develop',
338
541
  });
339
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--verbose']), expect.any(Object));
542
+ expect(analyzeState).toHaveBeenCalledWith(expect.objectContaining({ baseBranch: 'develop' }));
543
+ });
544
+ it('does not spawn a child process', async () => {
545
+ vi.mocked(spawnSync).mockClear();
546
+ await stateCommand.handler({ json: false, verbose: false });
547
+ expect(spawnSync).not.toHaveBeenCalled();
340
548
  });
341
549
  });
342
550
  describe('configCommand', () => {
@@ -346,32 +554,10 @@ describe('wt subcommand handlers', () => {
346
554
  });
347
555
  it('builder registers positional args', () => {
348
556
  invokeBuilder(configCommand, []);
349
- expect(true).toBe(true); // Builder executed for coverage
350
- });
351
- it('passes subcommand to wtconfig', () => {
352
- configCommand.handler({
353
- subcommand: 'show',
354
- args: [],
355
- });
356
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['show']), expect.any(Object));
357
- });
358
- it('passes subcommand and args to wtconfig', () => {
359
- configCommand.handler({
360
- subcommand: 'set',
361
- args: ['baseBranch', 'develop'],
362
- });
363
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['set', 'baseBranch', 'develop']), expect.any(Object));
557
+ expect(true).toBe(true);
364
558
  });
365
559
  it('handles no subcommand (defaults to interactive)', async () => {
366
- // The interactive mode calls git.getRepoRoot() first
367
- // Since we're not in a git repo in tests, it will error out
368
- // We just verify the handler runs without throwing
369
- const handler = configCommand.handler({
370
- subcommand: undefined,
371
- args: [],
372
- });
373
- // Handler is now async; it will call process.exit
374
- // We just check it returns a promise
560
+ const handler = configCommand.handler({ subcommand: undefined, args: [] });
375
561
  expect(handler).toBeInstanceOf(Promise);
376
562
  });
377
563
  });
@@ -382,10 +568,10 @@ describe('wt subcommand handlers', () => {
382
568
  });
383
569
  it('builder registers all expected options', () => {
384
570
  invokeBuilder(linkCommand, []);
385
- expect(true).toBe(true); // Builder executed for coverage
571
+ expect(true).toBe(true);
386
572
  });
387
- it('passes subcommand and args to wtlink', () => {
388
- linkCommand.handler({
573
+ it('calls link.run with source and destination for link subcommand', async () => {
574
+ await linkCommand.handler({
389
575
  subcommand: 'link',
390
576
  args: ['source', 'dest'],
391
577
  'dry-run': false,
@@ -394,65 +580,66 @@ describe('wt subcommand handlers', () => {
394
580
  json: false,
395
581
  verbose: false,
396
582
  });
397
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['link', 'source', 'dest']), expect.any(Object));
583
+ expect(linkRun).toHaveBeenCalledWith(expect.objectContaining({
584
+ source: 'source',
585
+ destination: 'dest',
586
+ dryRun: false,
587
+ yes: false,
588
+ }));
398
589
  });
399
- it('passes --dry-run flag to wtlink', () => {
400
- linkCommand.handler({
401
- 'dry-run': true,
590
+ it('passes --dry-run to link.run', async () => {
591
+ await linkCommand.handler({
592
+ subcommand: 'link',
402
593
  args: [],
594
+ 'dry-run': true,
403
595
  yes: false,
404
596
  'non-interactive': false,
405
597
  json: false,
406
598
  verbose: false,
407
599
  });
408
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--dry-run']), expect.any(Object));
600
+ expect(linkRun).toHaveBeenCalledWith(expect.objectContaining({ dryRun: true }));
409
601
  });
410
- it('passes --yes flag to wtlink', () => {
411
- linkCommand.handler({
412
- yes: true,
602
+ it('passes --yes to link.run', async () => {
603
+ await linkCommand.handler({
604
+ subcommand: 'link',
413
605
  args: [],
414
606
  'dry-run': false,
607
+ yes: true,
415
608
  'non-interactive': false,
416
609
  json: false,
417
610
  verbose: false,
418
611
  });
419
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--yes']), expect.any(Object));
420
- });
421
- it('passes --non-interactive flag to wtlink', () => {
422
- linkCommand.handler({
423
- 'non-interactive': true,
424
- args: [],
425
- 'dry-run': false,
426
- yes: false,
427
- json: false,
428
- verbose: false,
429
- });
430
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--non-interactive']), expect.any(Object));
612
+ expect(linkRun).toHaveBeenCalledWith(expect.objectContaining({ yes: true }));
431
613
  });
432
- it('passes --json flag to wtlink', () => {
433
- linkCommand.handler({
434
- json: true,
614
+ it('calls setJsonMode when --json is passed', async () => {
615
+ await linkCommand.handler({
616
+ subcommand: 'link',
435
617
  args: [],
436
618
  'dry-run': false,
437
619
  yes: false,
438
620
  'non-interactive': false,
621
+ json: true,
439
622
  verbose: false,
440
623
  });
441
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--json']), expect.any(Object));
624
+ expect(setJsonMode).toHaveBeenCalledWith(true);
442
625
  });
443
- it('passes --verbose flag to wtlink', () => {
444
- linkCommand.handler({
445
- verbose: true,
626
+ it('calls manage.run with verbose option for manage subcommand', async () => {
627
+ await linkCommand.handler({
628
+ subcommand: 'manage',
446
629
  args: [],
447
630
  'dry-run': false,
448
631
  yes: false,
449
632
  'non-interactive': false,
450
633
  json: false,
634
+ verbose: true,
635
+ clean: false,
636
+ backup: false,
451
637
  });
452
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--verbose']), expect.any(Object));
638
+ expect(manageRun).toHaveBeenCalledWith(expect.objectContaining({ verbose: true }));
453
639
  });
454
- it('passes --manifest-file flag to wtlink', () => {
455
- linkCommand.handler({
640
+ it('passes --manifest-file to manage.run', async () => {
641
+ await linkCommand.handler({
642
+ subcommand: 'manage',
456
643
  'manifest-file': '.custom-manifest',
457
644
  args: [],
458
645
  'dry-run': false,
@@ -460,11 +647,13 @@ describe('wt subcommand handlers', () => {
460
647
  'non-interactive': false,
461
648
  json: false,
462
649
  verbose: false,
650
+ clean: false,
651
+ backup: false,
463
652
  });
464
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--manifest-file', '.custom-manifest']), expect.any(Object));
653
+ expect(manageRun).toHaveBeenCalledWith(expect.objectContaining({ manifestFile: '.custom-manifest' }));
465
654
  });
466
- it('handles no subcommand (defaults to interactive)', () => {
467
- linkCommand.handler({
655
+ it('shows interactive menu when no subcommand provided', async () => {
656
+ await linkCommand.handler({
468
657
  args: [],
469
658
  'dry-run': false,
470
659
  yes: false,
@@ -474,10 +663,11 @@ describe('wt subcommand handlers', () => {
474
663
  clean: false,
475
664
  backup: false,
476
665
  });
477
- expect(spawnSync).toHaveBeenCalled();
666
+ expect(showMainMenu).toHaveBeenCalled();
478
667
  });
479
- it('passes --clean flag to wtlink', () => {
480
- linkCommand.handler({
668
+ it('passes --clean to manage.run', async () => {
669
+ await linkCommand.handler({
670
+ subcommand: 'manage',
481
671
  clean: true,
482
672
  args: [],
483
673
  'dry-run': false,
@@ -487,10 +677,11 @@ describe('wt subcommand handlers', () => {
487
677
  verbose: false,
488
678
  backup: false,
489
679
  });
490
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--clean']), expect.any(Object));
680
+ expect(manageRun).toHaveBeenCalledWith(expect.objectContaining({ clean: true }));
491
681
  });
492
- it('passes --backup flag to wtlink', () => {
493
- linkCommand.handler({
682
+ it('passes --backup to manage.run', async () => {
683
+ await linkCommand.handler({
684
+ subcommand: 'manage',
494
685
  backup: true,
495
686
  args: [],
496
687
  'dry-run': false,
@@ -500,10 +691,11 @@ describe('wt subcommand handlers', () => {
500
691
  verbose: false,
501
692
  clean: false,
502
693
  });
503
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--backup']), expect.any(Object));
694
+ expect(manageRun).toHaveBeenCalledWith(expect.objectContaining({ backup: true }));
504
695
  });
505
- it('passes --type flag to wtlink', () => {
506
- linkCommand.handler({
696
+ it('passes --type to link.run', async () => {
697
+ await linkCommand.handler({
698
+ subcommand: 'link',
507
699
  type: 'symbolic',
508
700
  args: [],
509
701
  'dry-run': false,
@@ -514,7 +706,33 @@ describe('wt subcommand handlers', () => {
514
706
  clean: false,
515
707
  backup: false,
516
708
  });
517
- expect(spawnSync).toHaveBeenCalledWith(process.execPath, expect.arrayContaining(['--type', 'symbolic']), expect.any(Object));
709
+ expect(linkRun).toHaveBeenCalledWith(expect.objectContaining({ type: 'symbolic' }));
710
+ });
711
+ it('calls validate.run for validate subcommand', async () => {
712
+ await linkCommand.handler({
713
+ subcommand: 'validate',
714
+ args: [],
715
+ 'dry-run': false,
716
+ yes: false,
717
+ 'non-interactive': false,
718
+ json: false,
719
+ verbose: false,
720
+ });
721
+ expect(validateRun).toHaveBeenCalled();
722
+ });
723
+ it('does not spawn a child process', async () => {
724
+ vi.mocked(spawnSync).mockClear();
725
+ await linkCommand.handler({
726
+ args: [],
727
+ 'dry-run': false,
728
+ yes: false,
729
+ 'non-interactive': false,
730
+ json: false,
731
+ verbose: false,
732
+ clean: false,
733
+ backup: false,
734
+ });
735
+ expect(spawnSync).not.toHaveBeenCalled();
518
736
  });
519
737
  });
520
738
  });