@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
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * These tests verify that each menu flow:
5
5
  * 1. Gathers the correct user inputs
6
- * 2. Passes the correct arguments to subcommands
7
- * 3. Handles cancellation and back navigation correctly
6
+ * 2. Calls the correct library functions with proper arguments
7
+ * 3. Returns to menu after operation execution (not exit)
8
+ * 4. Handles cancellation and back navigation correctly
9
+ * 5. Uses direct library calls (no subprocess spawning)
8
10
  */
9
11
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
12
  // Mock modules before importing the module under test
@@ -23,14 +25,9 @@ vi.mock('../../lib/prompts.js', () => {
23
25
  UserNavigatedBack: MockUserNavigatedBack,
24
26
  };
25
27
  });
26
- vi.mock('./run-command.js', () => ({
27
- runSubcommand: vi.fn(() => {
28
- // Mock never returns - simulate process.exit
29
- throw new Error('process.exit called');
30
- }),
31
- }));
32
28
  vi.mock('../../lib/config.js', () => ({
33
29
  loadConfig: vi.fn(() => ({
30
+ configVersion: 1,
34
31
  sharedRepos: [],
35
32
  baseBranch: 'main',
36
33
  draftPr: true,
@@ -62,12 +59,90 @@ vi.mock('../../lib/config.js', () => ({
62
59
  vi.mock('../../lib/git.js', () => ({
63
60
  getRepoRoot: vi.fn(() => '/mock/repo'),
64
61
  listLocalBranches: vi.fn(() => ['feat/existing-branch', 'fix/bug-fix', 'main', 'develop']),
62
+ removeWorktree: vi.fn(),
63
+ pruneWorktrees: vi.fn(),
64
+ }));
65
+ vi.mock('../../lib/wtlink/config-manifest.js', () => ({
66
+ loadManifestData: vi.fn(() => ({
67
+ enabled: ['.env', '.env.local'],
68
+ disabled: ['config.json'],
69
+ source: 'config',
70
+ })),
71
+ saveManifestData: vi.fn(),
72
+ }));
73
+ // Mock direct library imports
74
+ vi.mock('../../lib/lswt/index.js', () => ({
75
+ gatherWorktreeInfo: vi.fn(async () => []),
76
+ createDefaultDeps: vi.fn(() => ({})),
77
+ runInteractiveMode: vi.fn(async () => { }),
78
+ }));
79
+ vi.mock('../../lib/prs/command.js', () => ({
80
+ runPrsCommand: vi.fn(async () => { }),
81
+ }));
82
+ vi.mock('../newpr.js', () => ({
83
+ runNewprHandler: vi.fn(async () => { }),
84
+ }));
85
+ vi.mock('../../lib/cleanpr/index.js', () => ({
86
+ gatherPrWorktreeInfo: vi.fn(async () => []),
87
+ createDefaultDeps: vi.fn(() => ({})),
88
+ getCleanableWorktrees: vi.fn(() => []),
89
+ cleanWorktree: vi.fn(() => ({ success: true, message: 'Cleaned', prNumber: 42 })),
90
+ findWorktreeByPrNumber: vi.fn(() => null),
91
+ summarizeResults: vi.fn(() => ({ cleaned: 0, total: 0 })),
92
+ }));
93
+ vi.mock('../../lib/wtstate/index.js', () => ({
94
+ analyzeState: vi.fn(() => ({
95
+ scenario: 'main_clean_same',
96
+ scenarioDescription: 'On main, clean, same as origin',
97
+ currentBranch: 'main',
98
+ baseBranch: 'main',
99
+ worktreeType: 'main_worktree',
100
+ hasChanges: false,
101
+ hasStagedChanges: false,
102
+ hasUnstagedChanges: false,
103
+ localCommits: 0,
104
+ stagedFiles: [],
105
+ unstagedFiles: [],
106
+ availableActions: [],
107
+ recommendedAction: null,
108
+ })),
109
+ formatText: vi.fn(() => 'State: main_clean_same'),
110
+ }));
111
+ vi.mock('../../lib/wtlink/link-configs.js', () => ({
112
+ run: vi.fn(async () => { }),
113
+ }));
114
+ vi.mock('../../lib/wtlink/validate-manifest.js', () => ({
115
+ run: vi.fn(() => { }),
116
+ }));
117
+ vi.mock('../../lib/wtconfig/index.js', () => ({
118
+ formatConfigDisplay: vi.fn(() => '{ baseBranch: "main" }'),
119
+ setConfigValue: vi.fn((config, _key, _value) => config),
120
+ loadRepoConfig: vi.fn(() => ({})),
121
+ saveRepoConfig: vi.fn(),
122
+ validateConfig: vi.fn(() => ({ valid: true, errors: [], warnings: [] })),
123
+ }));
124
+ vi.mock('../../lib/constants.js', () => ({
125
+ DEFAULT_MANIFEST_FILE: '.wtlinkrc',
126
+ }));
127
+ vi.mock('../../lib/ui/index.js', () => ({
128
+ printStatus: vi.fn(),
129
+ }));
130
+ vi.mock('child_process', () => ({
131
+ execSync: vi.fn(),
65
132
  }));
66
133
  // Import mocked modules
67
134
  import { promptChoice, promptInput, promptConfirm } from '../../lib/prompts.js';
68
- import { runSubcommand } from './run-command.js';
69
135
  import { loadConfig } from '../../lib/config.js';
70
136
  import * as git from '../../lib/git.js';
137
+ import { loadManifestData, saveManifestData } from '../../lib/wtlink/config-manifest.js';
138
+ import { gatherWorktreeInfo, runInteractiveMode } from '../../lib/lswt/index.js';
139
+ import { runPrsCommand } from '../../lib/prs/command.js';
140
+ import { runNewprHandler } from '../newpr.js';
141
+ import { gatherPrWorktreeInfo, getCleanableWorktrees, } from '../../lib/cleanpr/index.js';
142
+ import { analyzeState, formatText } from '../../lib/wtstate/index.js';
143
+ import { run as runWtlinkLink } from '../../lib/wtlink/link-configs.js';
144
+ import { run as runWtlinkValidate } from '../../lib/wtlink/validate-manifest.js';
145
+ import { formatConfigDisplay, setConfigValue, loadRepoConfig, saveRepoConfig, } from '../../lib/wtconfig/index.js';
71
146
  // Import flows after mocks are set up
72
147
  import { flows, showMainMenu } from './interactive-menu.js';
73
148
  // Mock console.log to keep test output clean
@@ -80,36 +155,41 @@ describe('Interactive Menu Flows', () => {
80
155
  consoleSpy.mockClear();
81
156
  });
82
157
  describe('handleListWorktrees', () => {
83
- it('calls lswt subcommand with no args', async () => {
84
- try {
85
- await flows.handleListWorktrees();
86
- }
87
- catch {
88
- // Expected - runSubcommand throws
89
- }
90
- expect(runSubcommand).toHaveBeenCalledWith('lswt', []);
158
+ it('calls gatherWorktreeInfo and runInteractiveMode and returns to menu', async () => {
159
+ const result = await flows.handleListWorktrees();
160
+ expect(gatherWorktreeInfo).toHaveBeenCalledWith('/mock/repo', { verbose: false, json: false, showStatus: false }, expect.anything());
161
+ expect(runInteractiveMode).toHaveBeenCalled();
162
+ expect(result).toEqual({ completed: true, returnToMenu: true });
163
+ });
164
+ it('returns to menu with error message when library call fails', async () => {
165
+ vi.mocked(gatherWorktreeInfo).mockRejectedValueOnce(new Error('git error'));
166
+ const result = await flows.handleListWorktrees();
167
+ expect(result).toEqual({ completed: true, returnToMenu: true });
168
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('git error'));
91
169
  });
92
170
  });
93
171
  describe('handleBrowsePRs', () => {
94
- it('calls prs subcommand with no args', async () => {
95
- try {
96
- await flows.handleBrowsePRs();
97
- }
98
- catch {
99
- // Expected - runSubcommand throws
100
- }
101
- expect(runSubcommand).toHaveBeenCalledWith('prs', []);
172
+ it('calls runPrsCommand and returns to menu', async () => {
173
+ const result = await flows.handleBrowsePRs();
174
+ expect(runPrsCommand).toHaveBeenCalledWith({
175
+ state: 'open',
176
+ limit: 50,
177
+ json: false,
178
+ noInteractive: false,
179
+ });
180
+ expect(result).toEqual({ completed: true, returnToMenu: true });
102
181
  });
103
182
  });
104
183
  describe('handleShowState', () => {
105
- it('calls wtstate subcommand with no args', async () => {
106
- try {
107
- await flows.handleShowState();
108
- }
109
- catch {
110
- // Expected - runSubcommand throws
111
- }
112
- expect(runSubcommand).toHaveBeenCalledWith('wtstate', []);
184
+ it('calls analyzeState and formatText and returns to menu', async () => {
185
+ const result = await flows.handleShowState();
186
+ expect(analyzeState).toHaveBeenCalledWith({
187
+ verbose: false,
188
+ json: false,
189
+ baseBranch: 'main',
190
+ });
191
+ expect(formatText).toHaveBeenCalled();
192
+ expect(result).toEqual({ completed: true, returnToMenu: true });
113
193
  });
114
194
  });
115
195
  describe('handleNewPR', () => {
@@ -117,7 +197,7 @@ describe('Interactive Menu Flows', () => {
117
197
  vi.mocked(promptChoice).mockResolvedValueOnce('back');
118
198
  const result = await flows.handleNewPR();
119
199
  expect(result).toEqual({ completed: false, returnToMenu: true });
120
- expect(runSubcommand).not.toHaveBeenCalled();
200
+ expect(runNewprHandler).not.toHaveBeenCalled();
121
201
  });
122
202
  it('handles user cancellation (Ctrl+C)', async () => {
123
203
  vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
@@ -125,7 +205,7 @@ describe('Interactive Menu Flows', () => {
125
205
  expect(result).toEqual({ completed: false, returnToMenu: true });
126
206
  });
127
207
  describe('from-description flow', () => {
128
- it('gathers all inputs and calls newpr with correct args', async () => {
208
+ it('gathers all inputs and calls runNewprHandler with correct Options', async () => {
129
209
  vi.mocked(promptChoice)
130
210
  .mockResolvedValueOnce('from-description') // New PR sub-menu
131
211
  .mockResolvedValueOnce(true); // Draft PR selection
@@ -135,15 +215,18 @@ describe('Interactive Menu Flows', () => {
135
215
  vi.mocked(promptConfirm)
136
216
  .mockResolvedValueOnce(false) // Install deps
137
217
  .mockResolvedValueOnce(false); // Open VS Code
138
- try {
139
- await flows.handleNewPR();
140
- }
141
- catch {
142
- // Expected
143
- }
144
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add dark mode support']);
145
- });
146
- it('passes --ready flag when not draft', async () => {
218
+ const result = await flows.handleNewPR();
219
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
220
+ mode: 'new',
221
+ description: 'Add dark mode support',
222
+ baseBranch: 'main',
223
+ draft: true,
224
+ installDeps: false,
225
+ openEditor: false,
226
+ }));
227
+ expect(result).toEqual({ completed: true, returnToMenu: true });
228
+ });
229
+ it('passes ready flag when not draft', async () => {
147
230
  vi.mocked(promptChoice)
148
231
  .mockResolvedValueOnce('from-description')
149
232
  .mockResolvedValueOnce(false); // Ready for review (not draft)
@@ -151,15 +234,15 @@ describe('Interactive Menu Flows', () => {
151
234
  .mockResolvedValueOnce('Fix critical bug')
152
235
  .mockResolvedValueOnce('main');
153
236
  vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
154
- try {
155
- await flows.handleNewPR();
156
- }
157
- catch {
158
- // Expected
159
- }
160
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Fix critical bug', '--ready']);
161
- });
162
- it('passes --base flag when not main', async () => {
237
+ const result = await flows.handleNewPR();
238
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
239
+ mode: 'new',
240
+ description: 'Fix critical bug',
241
+ draft: false,
242
+ }));
243
+ expect(result).toEqual({ completed: true, returnToMenu: true });
244
+ });
245
+ it('passes non-main base branch', async () => {
163
246
  vi.mocked(promptChoice)
164
247
  .mockResolvedValueOnce('from-description')
165
248
  .mockResolvedValueOnce(true);
@@ -167,15 +250,13 @@ describe('Interactive Menu Flows', () => {
167
250
  .mockResolvedValueOnce('Feature work')
168
251
  .mockResolvedValueOnce('develop'); // Non-main base branch
169
252
  vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
170
- try {
171
- await flows.handleNewPR();
172
- }
173
- catch {
174
- // Expected
175
- }
176
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Feature work', '--base', 'develop']);
177
- });
178
- it('passes --install flag when requested', async () => {
253
+ const result = await flows.handleNewPR();
254
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
255
+ baseBranch: 'develop',
256
+ }));
257
+ expect(result).toEqual({ completed: true, returnToMenu: true });
258
+ });
259
+ it('passes install flag when requested', async () => {
179
260
  vi.mocked(promptChoice)
180
261
  .mockResolvedValueOnce('from-description')
181
262
  .mockResolvedValueOnce(true);
@@ -183,27 +264,23 @@ describe('Interactive Menu Flows', () => {
183
264
  vi.mocked(promptConfirm)
184
265
  .mockResolvedValueOnce(true) // Install deps
185
266
  .mockResolvedValueOnce(false);
186
- try {
187
- await flows.handleNewPR();
188
- }
189
- catch {
190
- // Expected
191
- }
192
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add feature', '--install']);
193
- });
194
- it('passes --code flag when requested', async () => {
267
+ const result = await flows.handleNewPR();
268
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
269
+ installDeps: true,
270
+ }));
271
+ expect(result).toEqual({ completed: true, returnToMenu: true });
272
+ });
273
+ it('passes code flag when requested', async () => {
195
274
  vi.mocked(promptChoice)
196
275
  .mockResolvedValueOnce('from-description')
197
276
  .mockResolvedValueOnce(true);
198
277
  vi.mocked(promptInput).mockResolvedValueOnce('Add feature').mockResolvedValueOnce('main');
199
278
  vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(true); // Open VS Code
200
- try {
201
- await flows.handleNewPR();
202
- }
203
- catch {
204
- // Expected
205
- }
206
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Add feature', '--code']);
279
+ const result = await flows.handleNewPR();
280
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
281
+ openEditor: true,
282
+ }));
283
+ expect(result).toEqual({ completed: true, returnToMenu: true });
207
284
  });
208
285
  it('passes all optional flags together', async () => {
209
286
  vi.mocked(promptChoice)
@@ -215,27 +292,23 @@ describe('Interactive Menu Flows', () => {
215
292
  vi.mocked(promptConfirm)
216
293
  .mockResolvedValueOnce(true) // Install
217
294
  .mockResolvedValueOnce(true); // VS Code
218
- try {
219
- await flows.handleNewPR();
220
- }
221
- catch {
222
- // Expected
223
- }
224
- expect(runSubcommand).toHaveBeenCalledWith('newpr', [
225
- 'Full feature',
226
- '--base',
227
- 'develop',
228
- '--ready',
229
- '--install',
230
- '--code',
231
- ]);
295
+ const result = await flows.handleNewPR();
296
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
297
+ mode: 'new',
298
+ description: 'Full feature',
299
+ baseBranch: 'develop',
300
+ draft: false,
301
+ installDeps: true,
302
+ openEditor: true,
303
+ }));
304
+ expect(result).toEqual({ completed: true, returnToMenu: true });
232
305
  });
233
306
  it('returns CANCELLED when description is empty', async () => {
234
307
  vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
235
308
  vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty description
236
309
  const result = await flows.handleNewPR();
237
310
  expect(result).toEqual({ completed: false, returnToMenu: true });
238
- expect(runSubcommand).not.toHaveBeenCalled();
311
+ expect(runNewprHandler).not.toHaveBeenCalled();
239
312
  });
240
313
  it('handles user cancellation during input', async () => {
241
314
  vi.mocked(promptChoice).mockResolvedValueOnce('from-description');
@@ -245,59 +318,59 @@ describe('Interactive Menu Flows', () => {
245
318
  });
246
319
  });
247
320
  describe('from-pr flow', () => {
248
- it('gathers PR number and calls newpr with --pr flag', async () => {
321
+ it('gathers PR number and calls runNewprHandler with mode pr', async () => {
249
322
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
250
323
  vi.mocked(promptInput).mockResolvedValueOnce('42');
251
324
  vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
252
- try {
253
- await flows.handleNewPR();
254
- }
255
- catch {
256
- // Expected
257
- }
258
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--pr', '42']);
259
- });
260
- it('passes --install and --code flags', async () => {
325
+ const result = await flows.handleNewPR();
326
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
327
+ mode: 'pr',
328
+ prNumber: 42,
329
+ }));
330
+ expect(result).toEqual({ completed: true, returnToMenu: true });
331
+ });
332
+ it('passes install and code flags', async () => {
261
333
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
262
334
  vi.mocked(promptInput).mockResolvedValueOnce('123');
263
335
  vi.mocked(promptConfirm)
264
336
  .mockResolvedValueOnce(true) // Install
265
337
  .mockResolvedValueOnce(true); // VS Code
266
- try {
267
- await flows.handleNewPR();
268
- }
269
- catch {
270
- // Expected
271
- }
272
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--pr', '123', '--install', '--code']);
338
+ const result = await flows.handleNewPR();
339
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
340
+ mode: 'pr',
341
+ prNumber: 123,
342
+ installDeps: true,
343
+ openEditor: true,
344
+ }));
345
+ expect(result).toEqual({ completed: true, returnToMenu: true });
273
346
  });
274
347
  it('returns CANCELLED when PR number is empty', async () => {
275
348
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
276
349
  vi.mocked(promptInput).mockResolvedValueOnce('');
277
350
  const result = await flows.handleNewPR();
278
351
  expect(result).toEqual({ completed: false, returnToMenu: true });
279
- expect(runSubcommand).not.toHaveBeenCalled();
352
+ expect(runNewprHandler).not.toHaveBeenCalled();
280
353
  });
281
354
  it('returns CANCELLED when PR number is invalid', async () => {
282
355
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
283
356
  vi.mocked(promptInput).mockResolvedValueOnce('not-a-number');
284
357
  const result = await flows.handleNewPR();
285
358
  expect(result).toEqual({ completed: false, returnToMenu: true });
286
- expect(runSubcommand).not.toHaveBeenCalled();
359
+ expect(runNewprHandler).not.toHaveBeenCalled();
287
360
  });
288
361
  it('returns CANCELLED when PR number is zero', async () => {
289
362
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
290
363
  vi.mocked(promptInput).mockResolvedValueOnce('0');
291
364
  const result = await flows.handleNewPR();
292
365
  expect(result).toEqual({ completed: false, returnToMenu: true });
293
- expect(runSubcommand).not.toHaveBeenCalled();
366
+ expect(runNewprHandler).not.toHaveBeenCalled();
294
367
  });
295
368
  it('returns CANCELLED when PR number is negative', async () => {
296
369
  vi.mocked(promptChoice).mockResolvedValueOnce('from-pr');
297
370
  vi.mocked(promptInput).mockResolvedValueOnce('-5');
298
371
  const result = await flows.handleNewPR();
299
372
  expect(result).toEqual({ completed: false, returnToMenu: true });
300
- expect(runSubcommand).not.toHaveBeenCalled();
373
+ expect(runNewprHandler).not.toHaveBeenCalled();
301
374
  });
302
375
  });
303
376
  describe('from-branch flow', () => {
@@ -307,13 +380,12 @@ describe('Interactive Menu Flows', () => {
307
380
  .mockResolvedValueOnce('feat/existing-branch') // Select branch
308
381
  .mockResolvedValueOnce(true); // Draft PR
309
382
  vi.mocked(promptInput).mockResolvedValueOnce('main');
310
- try {
311
- await flows.handleNewPR();
312
- }
313
- catch {
314
- // Expected
315
- }
316
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--branch', 'feat/existing-branch']);
383
+ const result = await flows.handleNewPR();
384
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
385
+ mode: 'branch',
386
+ branchName: 'feat/existing-branch',
387
+ }));
388
+ expect(result).toEqual({ completed: true, returnToMenu: true });
317
389
  });
318
390
  it('allows typing custom branch name', async () => {
319
391
  vi.mocked(promptChoice)
@@ -323,33 +395,27 @@ describe('Interactive Menu Flows', () => {
323
395
  vi.mocked(promptInput)
324
396
  .mockResolvedValueOnce('feat/my-new-branch') // Custom branch name
325
397
  .mockResolvedValueOnce('main');
326
- try {
327
- await flows.handleNewPR();
328
- }
329
- catch {
330
- // Expected
331
- }
332
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['--branch', 'feat/my-new-branch']);
333
- });
334
- it('passes --base and --ready flags', async () => {
398
+ const result = await flows.handleNewPR();
399
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
400
+ mode: 'branch',
401
+ branchName: 'feat/my-new-branch',
402
+ }));
403
+ expect(result).toEqual({ completed: true, returnToMenu: true });
404
+ });
405
+ it('passes non-main base branch and ready flag', async () => {
335
406
  vi.mocked(promptChoice)
336
407
  .mockResolvedValueOnce('from-branch')
337
408
  .mockResolvedValueOnce('fix/bug-fix')
338
409
  .mockResolvedValueOnce(false); // Ready for review
339
410
  vi.mocked(promptInput).mockResolvedValueOnce('develop');
340
- try {
341
- await flows.handleNewPR();
342
- }
343
- catch {
344
- // Expected
345
- }
346
- expect(runSubcommand).toHaveBeenCalledWith('newpr', [
347
- '--branch',
348
- 'fix/bug-fix',
349
- '--base',
350
- 'develop',
351
- '--ready',
352
- ]);
411
+ const result = await flows.handleNewPR();
412
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
413
+ mode: 'branch',
414
+ branchName: 'fix/bug-fix',
415
+ baseBranch: 'develop',
416
+ draft: false,
417
+ }));
418
+ expect(result).toEqual({ completed: true, returnToMenu: true });
353
419
  });
354
420
  it('returns CANCELLED when branch name is empty', async () => {
355
421
  vi.mocked(promptChoice)
@@ -358,7 +424,7 @@ describe('Interactive Menu Flows', () => {
358
424
  vi.mocked(promptInput).mockResolvedValueOnce(''); // Empty branch name
359
425
  const result = await flows.handleNewPR();
360
426
  expect(result).toEqual({ completed: false, returnToMenu: true });
361
- expect(runSubcommand).not.toHaveBeenCalled();
427
+ expect(runNewprHandler).not.toHaveBeenCalled();
362
428
  });
363
429
  it('handles empty branch list gracefully', async () => {
364
430
  // Mock empty branch list
@@ -368,14 +434,10 @@ describe('Interactive Menu Flows', () => {
368
434
  .mockResolvedValueOnce('feat/new-branch') // Manual branch input
369
435
  .mockResolvedValueOnce('main');
370
436
  vi.mocked(promptChoice).mockResolvedValueOnce(true); // Draft
371
- try {
372
- await flows.handleNewPR();
373
- }
374
- catch {
375
- // Expected
376
- }
437
+ const result = await flows.handleNewPR();
377
438
  // Should have prompted for branch name directly
378
439
  expect(promptInput).toHaveBeenCalledWith('Branch name');
440
+ expect(result).toEqual({ completed: true, returnToMenu: true });
379
441
  });
380
442
  });
381
443
  });
@@ -384,39 +446,31 @@ describe('Interactive Menu Flows', () => {
384
446
  vi.mocked(promptChoice).mockResolvedValueOnce('back');
385
447
  const result = await flows.handleCleanPRs();
386
448
  expect(result).toEqual({ completed: false, returnToMenu: true });
387
- expect(runSubcommand).not.toHaveBeenCalled();
449
+ expect(gatherPrWorktreeInfo).not.toHaveBeenCalled();
388
450
  });
389
451
  describe('clean-all', () => {
390
- it('calls cleanpr with --all after confirmation', async () => {
452
+ it('calls cleanpr library after confirmation and returns to menu', async () => {
391
453
  vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
392
454
  vi.mocked(promptConfirm).mockResolvedValueOnce(true);
393
- try {
394
- await flows.handleCleanPRs();
395
- }
396
- catch {
397
- // Expected
398
- }
399
- expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--all']);
455
+ const result = await flows.handleCleanPRs();
456
+ expect(gatherPrWorktreeInfo).toHaveBeenCalled();
457
+ expect(result).toEqual({ completed: true, returnToMenu: true });
400
458
  });
401
459
  it('returns CANCELLED when not confirmed', async () => {
402
460
  vi.mocked(promptChoice).mockResolvedValueOnce('clean-all');
403
461
  vi.mocked(promptConfirm).mockResolvedValueOnce(false);
404
462
  const result = await flows.handleCleanPRs();
405
463
  expect(result).toEqual({ completed: false, returnToMenu: true });
406
- expect(runSubcommand).not.toHaveBeenCalled();
464
+ expect(gatherPrWorktreeInfo).not.toHaveBeenCalled();
407
465
  });
408
466
  });
409
467
  describe('clean-specific', () => {
410
- it('calls cleanpr with PR number', async () => {
468
+ it('calls cleanpr with PR number and returns to menu', async () => {
411
469
  vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
412
470
  vi.mocked(promptInput).mockResolvedValueOnce('42');
413
- try {
414
- await flows.handleCleanPRs();
415
- }
416
- catch {
417
- // Expected
418
- }
419
- expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['42']);
471
+ const result = await flows.handleCleanPRs();
472
+ expect(gatherPrWorktreeInfo).toHaveBeenCalled();
473
+ expect(result).toEqual({ completed: true, returnToMenu: true });
420
474
  });
421
475
  it('returns CANCELLED when PR number is empty', async () => {
422
476
  vi.mocked(promptChoice).mockResolvedValueOnce('clean-specific');
@@ -432,15 +486,12 @@ describe('Interactive Menu Flows', () => {
432
486
  });
433
487
  });
434
488
  describe('dry-run', () => {
435
- it('calls cleanpr with --dry-run', async () => {
489
+ it('calls cleanpr with dry-run and returns to menu', async () => {
436
490
  vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
437
- try {
438
- await flows.handleCleanPRs();
439
- }
440
- catch {
441
- // Expected
442
- }
443
- expect(runSubcommand).toHaveBeenCalledWith('cleanpr', ['--dry-run']);
491
+ const result = await flows.handleCleanPRs();
492
+ expect(gatherPrWorktreeInfo).toHaveBeenCalled();
493
+ expect(getCleanableWorktrees).toHaveBeenCalled();
494
+ expect(result).toEqual({ completed: true, returnToMenu: true });
444
495
  });
445
496
  });
446
497
  it('handles user cancellation', async () => {
@@ -454,72 +505,110 @@ describe('Interactive Menu Flows', () => {
454
505
  vi.mocked(promptChoice).mockResolvedValueOnce('back');
455
506
  const result = await flows.handleLinkConfig();
456
507
  expect(result).toEqual({ completed: false, returnToMenu: true });
457
- expect(runSubcommand).not.toHaveBeenCalled();
458
508
  });
459
- it('view calls wtlink list', async () => {
460
- vi.mocked(promptChoice).mockResolvedValueOnce('view');
461
- try {
462
- await flows.handleLinkConfig();
463
- }
464
- catch {
465
- // Expected
466
- }
467
- expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['list']);
468
- });
469
- it('sync calls wtlink sync', async () => {
470
- vi.mocked(promptChoice).mockResolvedValueOnce('sync');
471
- try {
472
- await flows.handleLinkConfig();
473
- }
474
- catch {
475
- // Expected
476
- }
477
- expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['sync']);
478
- });
479
- it('add calls wtlink add with file path', async () => {
480
- vi.mocked(promptChoice).mockResolvedValueOnce('add');
481
- vi.mocked(promptInput).mockResolvedValueOnce('.env');
482
- try {
483
- await flows.handleLinkConfig();
484
- }
485
- catch {
486
- // Expected
487
- }
488
- expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['add', '.env']);
489
- });
490
- it('add returns CANCELLED when file path is empty', async () => {
491
- vi.mocked(promptChoice).mockResolvedValueOnce('add');
492
- vi.mocked(promptInput).mockResolvedValueOnce('');
493
- const result = await flows.handleLinkConfig();
494
- expect(result).toEqual({ completed: false, returnToMenu: true });
495
- expect(runSubcommand).not.toHaveBeenCalled();
496
- });
497
- it('remove calls wtlink remove with file path', async () => {
498
- vi.mocked(promptChoice).mockResolvedValueOnce('remove');
499
- vi.mocked(promptInput).mockResolvedValueOnce('.env.local');
500
- try {
501
- await flows.handleLinkConfig();
502
- }
503
- catch {
504
- // Expected
505
- }
506
- expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['remove', '.env.local']);
507
- });
508
- it('remove returns CANCELLED when file path is empty', async () => {
509
- vi.mocked(promptChoice).mockResolvedValueOnce('remove');
510
- vi.mocked(promptInput).mockResolvedValueOnce('');
511
- const result = await flows.handleLinkConfig();
512
- expect(result).toEqual({ completed: false, returnToMenu: true });
509
+ describe('view via library', () => {
510
+ it('displays manifest contents from loadManifestData', async () => {
511
+ vi.mocked(promptChoice).mockResolvedValueOnce('view');
512
+ const result = await flows.handleLinkConfig();
513
+ expect(loadManifestData).toHaveBeenCalledWith('/mock/repo');
514
+ expect(result).toEqual({ completed: true, returnToMenu: true });
515
+ });
516
+ it('shows empty message when manifest has no files', async () => {
517
+ vi.mocked(loadManifestData).mockReturnValueOnce({
518
+ enabled: [],
519
+ disabled: [],
520
+ source: 'empty',
521
+ });
522
+ vi.mocked(promptChoice).mockResolvedValueOnce('view');
523
+ const result = await flows.handleLinkConfig();
524
+ expect(result).toEqual({ completed: true, returnToMenu: true });
525
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No files'));
526
+ });
527
+ it('displays enabled and disabled files', async () => {
528
+ vi.mocked(promptChoice).mockResolvedValueOnce('view');
529
+ const result = await flows.handleLinkConfig();
530
+ expect(result).toEqual({ completed: true, returnToMenu: true });
531
+ // Check enabled files are shown
532
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Enabled'));
533
+ expect(consoleSpy).toHaveBeenCalledWith(' .env');
534
+ expect(consoleSpy).toHaveBeenCalledWith(' .env.local');
535
+ // Check disabled files are shown
536
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Disabled'));
537
+ });
513
538
  });
514
- it('validate calls wtlink validate', async () => {
515
- vi.mocked(promptChoice).mockResolvedValueOnce('validate');
516
- try {
517
- await flows.handleLinkConfig();
518
- }
519
- catch {
520
- // Expected
521
- }
522
- expect(runSubcommand).toHaveBeenCalledWith('wtlink', ['validate']);
539
+ describe('sync via wtlink link', () => {
540
+ it('calls wtlink link library function', async () => {
541
+ vi.mocked(promptChoice).mockResolvedValueOnce('sync');
542
+ const result = await flows.handleLinkConfig();
543
+ expect(runWtlinkLink).toHaveBeenCalledWith(expect.objectContaining({
544
+ manifestFile: '.wtlinkrc',
545
+ dryRun: false,
546
+ type: 'hard',
547
+ }));
548
+ expect(result).toEqual({ completed: true, returnToMenu: true });
549
+ });
550
+ it('shows error when sync fails', async () => {
551
+ vi.mocked(runWtlinkLink).mockRejectedValueOnce(new Error('Link failed'));
552
+ vi.mocked(promptChoice).mockResolvedValueOnce('sync');
553
+ const result = await flows.handleLinkConfig();
554
+ expect(result).toEqual({ completed: true, returnToMenu: true });
555
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Link failed'));
556
+ });
557
+ });
558
+ describe('add via library', () => {
559
+ it('adds file to manifest via saveManifestData', async () => {
560
+ vi.mocked(promptChoice).mockResolvedValueOnce('add');
561
+ vi.mocked(promptInput).mockResolvedValueOnce('.npmrc');
562
+ const result = await flows.handleLinkConfig();
563
+ expect(saveManifestData).toHaveBeenCalledWith('/mock/repo', ['.env', '.env.local', '.npmrc'], ['config.json']);
564
+ expect(result).toEqual({ completed: true, returnToMenu: true });
565
+ });
566
+ it('skips duplicate files', async () => {
567
+ vi.mocked(promptChoice).mockResolvedValueOnce('add');
568
+ vi.mocked(promptInput).mockResolvedValueOnce('.env');
569
+ const result = await flows.handleLinkConfig();
570
+ expect(saveManifestData).not.toHaveBeenCalled();
571
+ expect(result).toEqual({ completed: true, returnToMenu: true });
572
+ });
573
+ it('returns CANCELLED when file path is empty', async () => {
574
+ vi.mocked(promptChoice).mockResolvedValueOnce('add');
575
+ vi.mocked(promptInput).mockResolvedValueOnce('');
576
+ const result = await flows.handleLinkConfig();
577
+ expect(result).toEqual({ completed: false, returnToMenu: true });
578
+ expect(saveManifestData).not.toHaveBeenCalled();
579
+ });
580
+ });
581
+ describe('remove via library', () => {
582
+ it('removes file from manifest via saveManifestData', async () => {
583
+ vi.mocked(promptChoice).mockResolvedValueOnce('remove');
584
+ vi.mocked(promptInput).mockResolvedValueOnce('.env');
585
+ const result = await flows.handleLinkConfig();
586
+ expect(saveManifestData).toHaveBeenCalledWith('/mock/repo', ['.env.local'], ['config.json']);
587
+ expect(result).toEqual({ completed: true, returnToMenu: true });
588
+ });
589
+ it('handles file not in manifest', async () => {
590
+ vi.mocked(promptChoice).mockResolvedValueOnce('remove');
591
+ vi.mocked(promptInput).mockResolvedValueOnce('nonexistent.txt');
592
+ const result = await flows.handleLinkConfig();
593
+ expect(saveManifestData).not.toHaveBeenCalled();
594
+ expect(result).toEqual({ completed: true, returnToMenu: true });
595
+ });
596
+ it('returns CANCELLED when file path is empty', async () => {
597
+ vi.mocked(promptChoice).mockResolvedValueOnce('remove');
598
+ vi.mocked(promptInput).mockResolvedValueOnce('');
599
+ const result = await flows.handleLinkConfig();
600
+ expect(result).toEqual({ completed: false, returnToMenu: true });
601
+ });
602
+ });
603
+ describe('validate', () => {
604
+ it('calls wtlink validate and returns to menu', async () => {
605
+ vi.mocked(promptChoice).mockResolvedValueOnce('validate');
606
+ const result = await flows.handleLinkConfig();
607
+ expect(runWtlinkValidate).toHaveBeenCalledWith(expect.objectContaining({
608
+ manifestFile: '.wtlinkrc',
609
+ }));
610
+ expect(result).toEqual({ completed: true, returnToMenu: true });
611
+ });
523
612
  });
524
613
  it('handles user cancellation', async () => {
525
614
  vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
@@ -532,53 +621,41 @@ describe('Interactive Menu Flows', () => {
532
621
  vi.mocked(promptChoice).mockResolvedValueOnce('back');
533
622
  const result = await flows.handleConfigure();
534
623
  expect(result).toEqual({ completed: false, returnToMenu: true });
535
- expect(runSubcommand).not.toHaveBeenCalled();
536
624
  });
537
- it('view calls wtconfig show', async () => {
625
+ it('view calls formatConfigDisplay and returns to menu', async () => {
538
626
  vi.mocked(promptChoice).mockResolvedValueOnce('view');
539
- try {
540
- await flows.handleConfigure();
541
- }
542
- catch {
543
- // Expected
544
- }
545
- expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['show']);
546
- });
547
- it('init calls wtconfig init after confirmation', async () => {
627
+ const result = await flows.handleConfigure();
628
+ expect(loadRepoConfig).toHaveBeenCalledWith('/mock/repo');
629
+ expect(formatConfigDisplay).toHaveBeenCalled();
630
+ expect(result).toEqual({ completed: true, returnToMenu: true });
631
+ });
632
+ it('init shows redirect message after confirmation and returns to menu', async () => {
548
633
  vi.mocked(promptChoice).mockResolvedValueOnce('init');
549
634
  vi.mocked(promptConfirm).mockResolvedValueOnce(true);
550
- try {
551
- await flows.handleConfigure();
552
- }
553
- catch {
554
- // Expected
555
- }
556
- expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['init']);
635
+ const result = await flows.handleConfigure();
636
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('wt init'));
637
+ expect(result).toEqual({ completed: true, returnToMenu: true });
557
638
  });
558
639
  it('init returns CANCELLED when not confirmed', async () => {
559
640
  vi.mocked(promptChoice).mockResolvedValueOnce('init');
560
641
  vi.mocked(promptConfirm).mockResolvedValueOnce(false);
561
642
  const result = await flows.handleConfigure();
562
643
  expect(result).toEqual({ completed: false, returnToMenu: true });
563
- expect(runSubcommand).not.toHaveBeenCalled();
564
644
  });
565
- it('edit calls wtconfig set with setting and value', async () => {
645
+ it('edit calls setConfigValue and saveRepoConfig with setting and value', async () => {
566
646
  vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('baseBranch');
567
647
  vi.mocked(promptInput).mockResolvedValueOnce('develop');
568
- try {
569
- await flows.handleConfigure();
570
- }
571
- catch {
572
- // Expected
573
- }
574
- expect(runSubcommand).toHaveBeenCalledWith('wtconfig', ['set', 'baseBranch', 'develop']);
648
+ const result = await flows.handleConfigure();
649
+ expect(setConfigValue).toHaveBeenCalledWith({}, 'baseBranch', 'develop');
650
+ expect(saveRepoConfig).toHaveBeenCalled();
651
+ expect(result).toEqual({ completed: true, returnToMenu: true });
575
652
  });
576
653
  it('edit returns CANCELLED when value is empty', async () => {
577
654
  vi.mocked(promptChoice).mockResolvedValueOnce('edit').mockResolvedValueOnce('branchPrefix');
578
655
  vi.mocked(promptInput).mockResolvedValueOnce('');
579
656
  const result = await flows.handleConfigure();
580
657
  expect(result).toEqual({ completed: false, returnToMenu: true });
581
- expect(runSubcommand).not.toHaveBeenCalled();
658
+ expect(saveRepoConfig).not.toHaveBeenCalled();
582
659
  });
583
660
  it('handles user cancellation', async () => {
584
661
  vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
@@ -590,12 +667,12 @@ describe('Interactive Menu Flows', () => {
590
667
  it('exits on exit selection', async () => {
591
668
  vi.mocked(promptChoice).mockResolvedValueOnce('exit');
592
669
  await showMainMenu();
593
- expect(runSubcommand).not.toHaveBeenCalled();
670
+ expect(runNewprHandler).not.toHaveBeenCalled();
594
671
  });
595
672
  it('exits on user cancellation', async () => {
596
673
  vi.mocked(promptChoice).mockRejectedValueOnce(new Error('User cancelled'));
597
674
  await showMainMenu();
598
- expect(runSubcommand).not.toHaveBeenCalled();
675
+ expect(runNewprHandler).not.toHaveBeenCalled();
599
676
  });
600
677
  it('re-throws non-cancellation errors', async () => {
601
678
  vi.mocked(promptChoice).mockRejectedValueOnce(new Error('Some other error'));
@@ -610,35 +687,29 @@ describe('Interactive Menu Flows', () => {
610
687
  // Should have called promptChoice 3 times (menu -> sub-menu -> back to menu -> exit)
611
688
  expect(promptChoice).toHaveBeenCalledTimes(3);
612
689
  });
613
- it('handles list worktrees selection', async () => {
614
- vi.mocked(promptChoice).mockResolvedValueOnce('list');
615
- try {
616
- await showMainMenu();
617
- }
618
- catch {
619
- // Expected - runSubcommand throws
620
- }
621
- expect(runSubcommand).toHaveBeenCalledWith('lswt', []);
622
- });
623
- it('handles browse PRs selection', async () => {
624
- vi.mocked(promptChoice).mockResolvedValueOnce('browse-prs');
625
- try {
626
- await showMainMenu();
627
- }
628
- catch {
629
- // Expected - runSubcommand throws
630
- }
631
- expect(runSubcommand).toHaveBeenCalledWith('prs', []);
632
- });
633
- it('handles show state selection', async () => {
634
- vi.mocked(promptChoice).mockResolvedValueOnce('state');
635
- try {
636
- await showMainMenu();
637
- }
638
- catch {
639
- // Expected
640
- }
641
- expect(runSubcommand).toHaveBeenCalledWith('wtstate', []);
690
+ it('handles list worktrees and returns to menu', async () => {
691
+ vi.mocked(promptChoice)
692
+ .mockResolvedValueOnce('list') // Select list
693
+ .mockResolvedValueOnce('exit'); // Then exit
694
+ await showMainMenu();
695
+ expect(gatherWorktreeInfo).toHaveBeenCalled();
696
+ expect(promptChoice).toHaveBeenCalledTimes(2);
697
+ });
698
+ it('handles browse PRs and returns to menu', async () => {
699
+ vi.mocked(promptChoice)
700
+ .mockResolvedValueOnce('browse-prs') // Select browse-prs
701
+ .mockResolvedValueOnce('exit'); // Then exit
702
+ await showMainMenu();
703
+ expect(runPrsCommand).toHaveBeenCalled();
704
+ expect(promptChoice).toHaveBeenCalledTimes(2);
705
+ });
706
+ it('handles show state and returns to menu', async () => {
707
+ vi.mocked(promptChoice)
708
+ .mockResolvedValueOnce('state') // Select state
709
+ .mockResolvedValueOnce('exit'); // Then exit
710
+ await showMainMenu();
711
+ expect(analyzeState).toHaveBeenCalled();
712
+ expect(promptChoice).toHaveBeenCalledTimes(2);
642
713
  });
643
714
  });
644
715
  describe('FlowResult types', () => {
@@ -648,17 +719,10 @@ describe('Interactive Menu Flows', () => {
648
719
  expect(result.completed).toBe(false);
649
720
  expect(result.returnToMenu).toBe(true);
650
721
  });
651
- it('flows that run subcommands return COMPLETED_EXIT', async () => {
722
+ it('flows that run operations return completed with returnToMenu=true', async () => {
652
723
  vi.mocked(promptChoice).mockResolvedValueOnce('dry-run');
653
- // We can't test the actual return value since runSubcommand throws,
654
- // but we can verify the flow attempted to call the subcommand
655
- try {
656
- await flows.handleCleanPRs();
657
- }
658
- catch {
659
- // Expected
660
- }
661
- expect(runSubcommand).toHaveBeenCalled();
724
+ const result = await flows.handleCleanPRs();
725
+ expect(result).toEqual({ completed: true, returnToMenu: true });
662
726
  });
663
727
  });
664
728
  });
@@ -669,6 +733,7 @@ describe('Config loading in flows', () => {
669
733
  it('uses config default for base branch', async () => {
670
734
  // Set up config mock to return custom baseBranch
671
735
  vi.mocked(loadConfig).mockReturnValueOnce({
736
+ configVersion: 1,
672
737
  sharedRepos: [],
673
738
  baseBranch: 'develop',
674
739
  draftPr: true,
@@ -695,20 +760,19 @@ describe('Config loading in flows', () => {
695
760
  logging: { level: 'info', timestamps: true },
696
761
  global: { warnNotGlobal: true },
697
762
  wtlink: { enabled: [], disabled: [] },
763
+ linkConfigFiles: undefined,
698
764
  });
699
765
  vi.mocked(promptChoice).mockResolvedValueOnce('from-description').mockResolvedValueOnce(true);
700
766
  vi.mocked(promptInput).mockResolvedValueOnce('Test feature').mockResolvedValueOnce('develop'); // User accepts default
701
767
  vi.mocked(promptConfirm).mockResolvedValueOnce(false).mockResolvedValueOnce(false);
702
- try {
703
- await flows.handleNewPR();
704
- }
705
- catch {
706
- // Expected
707
- }
768
+ const result = await flows.handleNewPR();
708
769
  // Verify loadConfig was called
709
770
  expect(loadConfig).toHaveBeenCalled();
710
- // Since user entered 'develop' (matching config default), no --base flag
711
- expect(runSubcommand).toHaveBeenCalledWith('newpr', ['Test feature', '--base', 'develop']);
771
+ // Verify runNewprHandler was called with develop base branch
772
+ expect(runNewprHandler).toHaveBeenCalledWith(expect.objectContaining({
773
+ baseBranch: 'develop',
774
+ }));
775
+ expect(result).toEqual({ completed: true, returnToMenu: true });
712
776
  });
713
777
  });
714
778
  describe('Git branch listing in flows', () => {
@@ -723,14 +787,10 @@ describe('Git branch listing in flows', () => {
723
787
  .mockResolvedValueOnce('feat/existing-branch')
724
788
  .mockResolvedValueOnce(true);
725
789
  vi.mocked(promptInput).mockResolvedValueOnce('main');
726
- try {
727
- await flows.handleNewPR();
728
- }
729
- catch {
730
- // Expected
731
- }
790
+ const result = await flows.handleNewPR();
732
791
  // Check that listLocalBranches was called
733
792
  expect(git.listLocalBranches).toHaveBeenCalled();
793
+ expect(result).toEqual({ completed: true, returnToMenu: true });
734
794
  });
735
795
  });
736
796
  //# sourceMappingURL=interactive-menu.test.js.map