byterover-cli 2.6.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/.env.production +1 -0
  2. package/README.md +240 -14
  3. package/dist/agent/core/domain/knowledge/conflict-detector.d.ts +38 -0
  4. package/dist/agent/core/domain/knowledge/conflict-detector.js +71 -0
  5. package/dist/agent/core/domain/knowledge/conflict-resolver.d.ts +17 -0
  6. package/dist/agent/core/domain/knowledge/conflict-resolver.js +118 -0
  7. package/dist/agent/core/domain/knowledge/utils.d.ts +4 -0
  8. package/dist/agent/core/domain/knowledge/utils.js +6 -0
  9. package/dist/agent/core/interfaces/i-curate-service.d.ts +6 -0
  10. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +67 -34
  11. package/dist/agent/infra/tools/implementations/curate-tool.js +294 -47
  12. package/dist/agent/resources/prompts/system-prompt.yml +15 -8
  13. package/dist/agent/resources/tools/code_exec.txt +3 -0
  14. package/dist/agent/resources/tools/curate.txt +12 -3
  15. package/dist/oclif/commands/connectors/install.d.ts +2 -1
  16. package/dist/oclif/commands/connectors/install.js +38 -3
  17. package/dist/oclif/commands/curate/index.d.ts +18 -0
  18. package/dist/oclif/commands/curate/index.js +78 -1
  19. package/dist/oclif/commands/init.d.ts +12 -0
  20. package/dist/oclif/commands/init.js +75 -0
  21. package/dist/oclif/commands/locations.js +1 -1
  22. package/dist/oclif/commands/providers/connect.d.ts +31 -1
  23. package/dist/oclif/commands/providers/connect.js +307 -27
  24. package/dist/oclif/commands/pull.d.ts +1 -0
  25. package/dist/oclif/commands/pull.js +7 -0
  26. package/dist/oclif/commands/push.d.ts +1 -0
  27. package/dist/oclif/commands/push.js +8 -0
  28. package/dist/oclif/commands/review/approve.d.ts +17 -0
  29. package/dist/oclif/commands/review/approve.js +37 -0
  30. package/dist/oclif/commands/review/base-review-decision.d.ts +18 -0
  31. package/dist/oclif/commands/review/base-review-decision.js +71 -0
  32. package/dist/oclif/commands/review/pending.d.ts +13 -0
  33. package/dist/oclif/commands/review/pending.js +94 -0
  34. package/dist/oclif/commands/review/reject.d.ts +17 -0
  35. package/dist/oclif/commands/review/reject.js +38 -0
  36. package/dist/oclif/commands/space/list.d.ts +2 -2
  37. package/dist/oclif/commands/space/list.js +13 -35
  38. package/dist/oclif/commands/space/switch.d.ts +2 -7
  39. package/dist/oclif/commands/space/switch.js +13 -56
  40. package/dist/oclif/commands/status.d.ts +1 -0
  41. package/dist/oclif/commands/status.js +11 -1
  42. package/dist/oclif/commands/vc/add.d.ts +7 -0
  43. package/dist/oclif/commands/vc/add.js +29 -0
  44. package/dist/oclif/commands/vc/branch.d.ts +15 -0
  45. package/dist/oclif/commands/vc/branch.js +70 -0
  46. package/dist/oclif/commands/vc/checkout.d.ts +14 -0
  47. package/dist/oclif/commands/vc/checkout.js +47 -0
  48. package/dist/oclif/commands/vc/clone.d.ts +9 -0
  49. package/dist/oclif/commands/vc/clone.js +61 -0
  50. package/dist/oclif/commands/vc/commit.d.ts +10 -0
  51. package/dist/oclif/commands/vc/commit.js +32 -0
  52. package/dist/oclif/commands/vc/config.d.ts +10 -0
  53. package/dist/oclif/commands/vc/config.js +30 -0
  54. package/dist/oclif/commands/vc/fetch.d.ts +10 -0
  55. package/dist/oclif/commands/vc/fetch.js +42 -0
  56. package/dist/oclif/commands/vc/index.d.ts +6 -0
  57. package/dist/oclif/commands/vc/index.js +8 -0
  58. package/dist/oclif/commands/vc/init.d.ts +6 -0
  59. package/dist/oclif/commands/vc/init.js +25 -0
  60. package/dist/oclif/commands/vc/log.d.ts +13 -0
  61. package/dist/oclif/commands/vc/log.js +48 -0
  62. package/dist/oclif/commands/vc/merge.d.ts +19 -0
  63. package/dist/oclif/commands/vc/merge.js +130 -0
  64. package/dist/oclif/commands/vc/pull.d.ts +13 -0
  65. package/dist/oclif/commands/vc/pull.js +60 -0
  66. package/dist/oclif/commands/vc/push.d.ts +13 -0
  67. package/dist/oclif/commands/vc/push.js +60 -0
  68. package/dist/oclif/commands/vc/remote/add.d.ts +10 -0
  69. package/dist/oclif/commands/vc/remote/add.js +30 -0
  70. package/dist/oclif/commands/vc/remote/index.d.ts +6 -0
  71. package/dist/oclif/commands/vc/remote/index.js +16 -0
  72. package/dist/oclif/commands/vc/remote/set-url.d.ts +10 -0
  73. package/dist/oclif/commands/vc/remote/set-url.js +30 -0
  74. package/dist/oclif/commands/vc/reset.d.ts +13 -0
  75. package/dist/oclif/commands/vc/reset.js +62 -0
  76. package/dist/oclif/commands/vc/status.d.ts +8 -0
  77. package/dist/oclif/commands/vc/status.js +106 -0
  78. package/dist/oclif/hooks/init/validate-brv-config.d.ts +26 -0
  79. package/dist/oclif/hooks/init/validate-brv-config.js +62 -0
  80. package/dist/oclif/lib/daemon-client.d.ts +2 -0
  81. package/dist/oclif/lib/daemon-client.js +36 -10
  82. package/dist/oclif/lib/prompt-utils.d.ts +43 -0
  83. package/dist/oclif/lib/prompt-utils.js +84 -0
  84. package/dist/oclif/lib/spinner.d.ts +8 -0
  85. package/dist/oclif/lib/spinner.js +23 -0
  86. package/dist/oclif/lib/task-client.d.ts +5 -0
  87. package/dist/oclif/lib/task-client.js +15 -2
  88. package/dist/server/config/environment.d.ts +2 -0
  89. package/dist/server/config/environment.js +2 -0
  90. package/dist/server/constants.d.ts +3 -0
  91. package/dist/server/constants.js +9 -0
  92. package/dist/server/core/domain/entities/auth-token.d.ts +2 -0
  93. package/dist/server/core/domain/entities/auth-token.js +7 -1
  94. package/dist/server/core/domain/entities/curate-log-entry.d.ts +11 -0
  95. package/dist/server/core/domain/entities/space.d.ts +4 -0
  96. package/dist/server/core/domain/entities/space.js +8 -0
  97. package/dist/server/core/domain/entities/team.d.ts +2 -0
  98. package/dist/server/core/domain/entities/team.js +4 -0
  99. package/dist/server/core/domain/errors/git-error.d.ts +6 -0
  100. package/dist/server/core/domain/errors/git-error.js +12 -0
  101. package/dist/server/core/domain/errors/task-error.d.ts +4 -0
  102. package/dist/server/core/domain/errors/task-error.js +8 -0
  103. package/dist/server/core/domain/errors/vc-error.d.ts +5 -0
  104. package/dist/server/core/domain/errors/vc-error.js +8 -0
  105. package/dist/server/core/domain/knowledge/markdown-writer.d.ts +4 -1
  106. package/dist/server/core/domain/knowledge/markdown-writer.js +37 -7
  107. package/dist/server/core/domain/transport/schemas.d.ts +6 -6
  108. package/dist/server/core/interfaces/context-tree/i-context-tree-service.d.ts +11 -0
  109. package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +6 -0
  110. package/dist/server/core/interfaces/services/i-git-service.d.ts +234 -0
  111. package/dist/server/core/interfaces/services/i-git-service.js +1 -0
  112. package/dist/server/core/interfaces/storage/i-curate-log-store.d.ts +5 -0
  113. package/dist/server/core/interfaces/storage/i-review-backup-store.d.ts +19 -0
  114. package/dist/server/core/interfaces/storage/i-review-backup-store.js +1 -0
  115. package/dist/server/core/interfaces/vc/i-vc-git-config-store.d.ts +8 -0
  116. package/dist/server/core/interfaces/vc/i-vc-git-config-store.js +1 -0
  117. package/dist/server/infra/config/auto-init.d.ts +0 -2
  118. package/dist/server/infra/config/auto-init.js +0 -1
  119. package/dist/server/infra/context-tree/file-context-tree-service.d.ts +2 -0
  120. package/dist/server/infra/context-tree/file-context-tree-service.js +13 -0
  121. package/dist/server/infra/daemon/brv-server.js +23 -3
  122. package/dist/server/infra/git/cogit-url.d.ts +17 -0
  123. package/dist/server/infra/git/cogit-url.js +39 -0
  124. package/dist/server/infra/git/git-http-wrapper.d.ts +20 -0
  125. package/dist/server/infra/git/git-http-wrapper.js +334 -0
  126. package/dist/server/infra/git/isomorphic-git-service.d.ts +78 -0
  127. package/dist/server/infra/git/isomorphic-git-service.js +983 -0
  128. package/dist/server/infra/http/review-api-handler.d.ts +13 -0
  129. package/dist/server/infra/http/review-api-handler.js +286 -0
  130. package/dist/server/infra/http/review-ui.d.ts +7 -0
  131. package/dist/server/infra/http/review-ui.js +606 -0
  132. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -2
  133. package/dist/server/infra/process/curate-log-handler.d.ts +18 -2
  134. package/dist/server/infra/process/curate-log-handler.js +50 -13
  135. package/dist/server/infra/process/feature-handlers.js +41 -1
  136. package/dist/server/infra/process/task-router.js +16 -0
  137. package/dist/server/infra/space/http-space-service.js +2 -0
  138. package/dist/server/infra/storage/file-curate-log-store.d.ts +10 -0
  139. package/dist/server/infra/storage/file-curate-log-store.js +35 -0
  140. package/dist/server/infra/storage/file-review-backup-store.d.ts +29 -0
  141. package/dist/server/infra/storage/file-review-backup-store.js +121 -0
  142. package/dist/server/infra/transport/handlers/auth-handler.js +9 -5
  143. package/dist/server/infra/transport/handlers/handler-types.d.ts +9 -0
  144. package/dist/server/infra/transport/handlers/handler-types.js +11 -0
  145. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  146. package/dist/server/infra/transport/handlers/index.js +2 -0
  147. package/dist/server/infra/transport/handlers/init-handler.d.ts +1 -0
  148. package/dist/server/infra/transport/handlers/init-handler.js +13 -1
  149. package/dist/server/infra/transport/handlers/pull-handler.d.ts +3 -0
  150. package/dist/server/infra/transport/handlers/pull-handler.js +5 -1
  151. package/dist/server/infra/transport/handlers/push-handler.d.ts +20 -0
  152. package/dist/server/infra/transport/handlers/push-handler.js +116 -14
  153. package/dist/server/infra/transport/handlers/reset-handler.d.ts +11 -0
  154. package/dist/server/infra/transport/handlers/reset-handler.js +37 -1
  155. package/dist/server/infra/transport/handlers/review-handler.d.ts +35 -0
  156. package/dist/server/infra/transport/handlers/review-handler.js +162 -0
  157. package/dist/server/infra/transport/handlers/space-handler.d.ts +3 -0
  158. package/dist/server/infra/transport/handlers/space-handler.js +4 -1
  159. package/dist/server/infra/transport/handlers/status-handler.d.ts +5 -0
  160. package/dist/server/infra/transport/handlers/status-handler.js +51 -16
  161. package/dist/server/infra/transport/handlers/vc-handler.d.ts +100 -0
  162. package/dist/server/infra/transport/handlers/vc-handler.js +1050 -0
  163. package/dist/server/infra/transport/socket-io-transport-server.d.ts +7 -0
  164. package/dist/server/infra/transport/socket-io-transport-server.js +12 -1
  165. package/dist/server/infra/transport/transport-connector.d.ts +1 -1
  166. package/dist/server/infra/transport/transport-connector.js +2 -1
  167. package/dist/server/infra/vc/file-vc-git-config-store.d.ts +11 -0
  168. package/dist/server/infra/vc/file-vc-git-config-store.js +43 -0
  169. package/dist/server/templates/skill/SKILL.md +167 -33
  170. package/dist/server/utils/curate-result-parser.d.ts +64 -0
  171. package/dist/server/utils/curate-result-parser.js +8 -0
  172. package/dist/server/utils/gitignore.d.ts +9 -0
  173. package/dist/server/utils/gitignore.js +47 -0
  174. package/dist/shared/transport/events/index.d.ts +6 -0
  175. package/dist/shared/transport/events/index.js +3 -0
  176. package/dist/shared/transport/events/init-events.d.ts +8 -0
  177. package/dist/shared/transport/events/init-events.js +1 -0
  178. package/dist/shared/transport/events/push-events.d.ts +6 -0
  179. package/dist/shared/transport/events/review-events.d.ts +41 -0
  180. package/dist/shared/transport/events/review-events.js +5 -0
  181. package/dist/shared/transport/events/vc-events.d.ts +257 -0
  182. package/dist/shared/transport/events/vc-events.js +67 -0
  183. package/dist/shared/transport/types/dto.d.ts +6 -1
  184. package/dist/tui/app/pages/init-project-page.d.ts +9 -0
  185. package/dist/tui/app/pages/init-project-page.js +54 -0
  186. package/dist/tui/app/pages/protected-routes.js +14 -6
  187. package/dist/tui/components/index.d.ts +0 -2
  188. package/dist/tui/components/index.js +0 -1
  189. package/dist/tui/features/activity/hooks/use-activity-logs.js +7 -1
  190. package/dist/tui/features/commands/definitions/index.js +3 -0
  191. package/dist/tui/features/commands/definitions/space-list.js +9 -18
  192. package/dist/tui/features/commands/definitions/space-switch.js +10 -6
  193. package/dist/tui/features/commands/definitions/vc-add.d.ts +2 -0
  194. package/dist/tui/features/commands/definitions/vc-add.js +15 -0
  195. package/dist/tui/features/commands/definitions/vc-branch.d.ts +2 -0
  196. package/dist/tui/features/commands/definitions/vc-branch.js +33 -0
  197. package/dist/tui/features/commands/definitions/vc-checkout.d.ts +2 -0
  198. package/dist/tui/features/commands/definitions/vc-checkout.js +32 -0
  199. package/dist/tui/features/commands/definitions/vc-clone.d.ts +2 -0
  200. package/dist/tui/features/commands/definitions/vc-clone.js +18 -0
  201. package/dist/tui/features/commands/definitions/vc-commit.d.ts +2 -0
  202. package/dist/tui/features/commands/definitions/vc-commit.js +32 -0
  203. package/dist/tui/features/commands/definitions/vc-config.d.ts +2 -0
  204. package/dist/tui/features/commands/definitions/vc-config.js +40 -0
  205. package/dist/tui/features/commands/definitions/vc-fetch.d.ts +2 -0
  206. package/dist/tui/features/commands/definitions/vc-fetch.js +37 -0
  207. package/dist/tui/features/commands/definitions/vc-init.d.ts +2 -0
  208. package/dist/tui/features/commands/definitions/vc-init.js +11 -0
  209. package/dist/tui/features/commands/definitions/vc-log.d.ts +2 -0
  210. package/dist/tui/features/commands/definitions/vc-log.js +25 -0
  211. package/dist/tui/features/commands/definitions/vc-merge.d.ts +2 -0
  212. package/dist/tui/features/commands/definitions/vc-merge.js +48 -0
  213. package/dist/tui/features/commands/definitions/vc-pull.d.ts +2 -0
  214. package/dist/tui/features/commands/definitions/vc-pull.js +42 -0
  215. package/dist/tui/features/commands/definitions/vc-push.d.ts +2 -0
  216. package/dist/tui/features/commands/definitions/vc-push.js +38 -0
  217. package/dist/tui/features/commands/definitions/vc-remote.d.ts +2 -0
  218. package/dist/tui/features/commands/definitions/vc-remote.js +57 -0
  219. package/dist/tui/features/commands/definitions/vc-reset.d.ts +2 -0
  220. package/dist/tui/features/commands/definitions/vc-reset.js +35 -0
  221. package/dist/tui/features/commands/definitions/vc-status.d.ts +2 -0
  222. package/dist/tui/features/commands/definitions/vc-status.js +11 -0
  223. package/dist/tui/features/commands/definitions/vc.d.ts +2 -0
  224. package/dist/tui/features/commands/definitions/vc.js +36 -0
  225. package/dist/tui/features/commands/hooks/use-slash-command-processor.js +5 -5
  226. package/dist/tui/features/log/api/execute-log.d.ts +8 -0
  227. package/dist/tui/features/log/api/execute-log.js +13 -0
  228. package/dist/tui/features/log/components/log-flow.d.ts +14 -0
  229. package/dist/tui/features/log/components/log-flow.js +29 -0
  230. package/dist/tui/features/log/utils/format-log.d.ts +3 -0
  231. package/dist/tui/features/log/utils/format-log.js +42 -0
  232. package/dist/tui/features/onboarding/hooks/use-app-view-mode.d.ts +9 -5
  233. package/dist/tui/features/onboarding/hooks/use-app-view-mode.js +12 -5
  234. package/dist/tui/features/push/components/push-flow.js +9 -2
  235. package/dist/tui/features/reset/components/reset-flow.js +2 -1
  236. package/dist/tui/features/status/components/status-view.js +2 -1
  237. package/dist/tui/features/status/utils/format-status.js +9 -0
  238. package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +11 -0
  239. package/dist/tui/features/tasks/stores/tasks-store.d.ts +10 -0
  240. package/dist/tui/features/tasks/stores/tasks-store.js +16 -0
  241. package/dist/tui/features/vc/add/api/execute-vc-add.d.ts +8 -0
  242. package/dist/tui/features/vc/add/api/execute-vc-add.js +13 -0
  243. package/dist/tui/features/vc/add/components/vc-add-flow.d.ts +7 -0
  244. package/dist/tui/features/vc/add/components/vc-add-flow.js +35 -0
  245. package/dist/tui/features/vc/branch/api/execute-vc-branch.d.ts +8 -0
  246. package/dist/tui/features/vc/branch/api/execute-vc-branch.js +13 -0
  247. package/dist/tui/features/vc/branch/components/vc-branch-flow.d.ts +8 -0
  248. package/dist/tui/features/vc/branch/components/vc-branch-flow.js +53 -0
  249. package/dist/tui/features/vc/branch/utils/format-branch.d.ts +4 -0
  250. package/dist/tui/features/vc/branch/utils/format-branch.js +12 -0
  251. package/dist/tui/features/vc/checkout/api/execute-vc-checkout.d.ts +8 -0
  252. package/dist/tui/features/vc/checkout/api/execute-vc-checkout.js +13 -0
  253. package/dist/tui/features/vc/checkout/components/vc-checkout-flow.d.ts +8 -0
  254. package/dist/tui/features/vc/checkout/components/vc-checkout-flow.js +33 -0
  255. package/dist/tui/features/vc/clone/api/execute-vc-clone.d.ts +8 -0
  256. package/dist/tui/features/vc/clone/api/execute-vc-clone.js +13 -0
  257. package/dist/tui/features/vc/clone/components/vc-clone-flow.d.ts +7 -0
  258. package/dist/tui/features/vc/clone/components/vc-clone-flow.js +79 -0
  259. package/dist/tui/features/vc/commit/api/execute-vc-commit.d.ts +8 -0
  260. package/dist/tui/features/vc/commit/api/execute-vc-commit.js +13 -0
  261. package/dist/tui/features/vc/commit/components/vc-commit-flow.d.ts +7 -0
  262. package/dist/tui/features/vc/commit/components/vc-commit-flow.js +29 -0
  263. package/dist/tui/features/vc/config/api/execute-vc-config.d.ts +8 -0
  264. package/dist/tui/features/vc/config/api/execute-vc-config.js +13 -0
  265. package/dist/tui/features/vc/config/components/vc-config-flow.d.ts +9 -0
  266. package/dist/tui/features/vc/config/components/vc-config-flow.js +30 -0
  267. package/dist/tui/features/vc/fetch/api/execute-vc-fetch.d.ts +8 -0
  268. package/dist/tui/features/vc/fetch/api/execute-vc-fetch.js +13 -0
  269. package/dist/tui/features/vc/fetch/components/vc-fetch-flow.d.ts +8 -0
  270. package/dist/tui/features/vc/fetch/components/vc-fetch-flow.js +75 -0
  271. package/dist/tui/features/vc/init/api/execute-vc-init.d.ts +8 -0
  272. package/dist/tui/features/vc/init/api/execute-vc-init.js +13 -0
  273. package/dist/tui/features/vc/init/components/vc-init-flow.d.ts +10 -0
  274. package/dist/tui/features/vc/init/components/vc-init-flow.js +37 -0
  275. package/dist/tui/features/vc/merge/api/execute-vc-merge.d.ts +8 -0
  276. package/dist/tui/features/vc/merge/api/execute-vc-merge.js +13 -0
  277. package/dist/tui/features/vc/merge/components/vc-merge-flow.d.ts +11 -0
  278. package/dist/tui/features/vc/merge/components/vc-merge-flow.js +72 -0
  279. package/dist/tui/features/vc/pull/api/execute-vc-pull.d.ts +8 -0
  280. package/dist/tui/features/vc/pull/api/execute-vc-pull.js +13 -0
  281. package/dist/tui/features/vc/pull/components/vc-pull-flow.d.ts +9 -0
  282. package/dist/tui/features/vc/pull/components/vc-pull-flow.js +83 -0
  283. package/dist/tui/features/vc/push/api/execute-vc-push.d.ts +8 -0
  284. package/dist/tui/features/vc/push/api/execute-vc-push.js +13 -0
  285. package/dist/tui/features/vc/push/components/vc-push-flow.d.ts +8 -0
  286. package/dist/tui/features/vc/push/components/vc-push-flow.js +83 -0
  287. package/dist/tui/features/vc/remote/api/execute-vc-remote.d.ts +8 -0
  288. package/dist/tui/features/vc/remote/api/execute-vc-remote.js +13 -0
  289. package/dist/tui/features/vc/remote/components/vc-remote-flow.d.ts +9 -0
  290. package/dist/tui/features/vc/remote/components/vc-remote-flow.js +42 -0
  291. package/dist/tui/features/vc/reset/api/execute-vc-reset.d.ts +8 -0
  292. package/dist/tui/features/vc/reset/api/execute-vc-reset.js +13 -0
  293. package/dist/tui/features/vc/reset/components/vc-reset-flow.d.ts +10 -0
  294. package/dist/tui/features/vc/reset/components/vc-reset-flow.js +63 -0
  295. package/dist/tui/features/vc/status/api/execute-vc-status.d.ts +8 -0
  296. package/dist/tui/features/vc/status/api/execute-vc-status.js +13 -0
  297. package/dist/tui/features/vc/status/components/vc-status-flow.d.ts +10 -0
  298. package/dist/tui/features/vc/status/components/vc-status-flow.js +133 -0
  299. package/dist/tui/lib/environment.d.ts +8 -0
  300. package/dist/tui/lib/environment.js +8 -0
  301. package/dist/tui/utils/error-messages.d.ts +5 -1
  302. package/dist/tui/utils/error-messages.js +32 -3
  303. package/oclif.manifest.json +1018 -98
  304. package/package.json +9 -3
  305. package/dist/oclif/hooks/prerun/validate-brv-config-version.d.ts +0 -33
  306. package/dist/oclif/hooks/prerun/validate-brv-config-version.js +0 -86
  307. package/dist/tui/components/init.d.ts +0 -33
  308. package/dist/tui/components/init.js +0 -234
  309. package/dist/tui/features/space/api/get-spaces.d.ts +0 -16
  310. package/dist/tui/features/space/api/get-spaces.js +0 -17
  311. package/dist/tui/features/space/api/switch-space.d.ts +0 -11
  312. package/dist/tui/features/space/api/switch-space.js +0 -24
  313. package/dist/tui/features/space/components/space-list-view.d.ts +0 -12
  314. package/dist/tui/features/space/components/space-list-view.js +0 -56
  315. package/dist/tui/features/space/components/space-switch-flow.d.ts +0 -13
  316. package/dist/tui/features/space/components/space-switch-flow.js +0 -97
@@ -0,0 +1,1050 @@
1
+ import fs from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { VcErrorCode, VcEvents, } from '../../../../shared/transport/events/vc-events.js';
4
+ import { CONTEXT_TREE_GITIGNORE } from '../../../constants.js';
5
+ import { BrvConfig } from '../../../core/domain/entities/brv-config.js';
6
+ import { Space } from '../../../core/domain/entities/space.js';
7
+ import { GitAuthError, GitError } from '../../../core/domain/errors/git-error.js';
8
+ import { NotAuthenticatedError } from '../../../core/domain/errors/task-error.js';
9
+ import { VcError } from '../../../core/domain/errors/vc-error.js';
10
+ import { ensureGitignoreEntries } from '../../../utils/gitignore.js';
11
+ import { buildCogitRemoteUrl, isValidBranchName, parseUserFacingUrl } from '../../git/cogit-url.js';
12
+ import { resolveRequiredProjectPath } from './handler-types.js';
13
+ /**
14
+ * Classify a raw isomorphic-git error into a specific VcError by its `.code` property.
15
+ * Returns undefined if the error is not a recognized isomorphic-git error.
16
+ */
17
+ function classifyIsomorphicGitError(error, notFoundCode) {
18
+ if (!(error instanceof Error) || !('code' in error))
19
+ return undefined;
20
+ const { code } = error;
21
+ if (code === 'HttpError' || code === 'SmartHttpError') {
22
+ return new VcError(error.message, VcErrorCode.NETWORK_ERROR);
23
+ }
24
+ if (code === 'NotFoundError') {
25
+ return new VcError(error.message, notFoundCode);
26
+ }
27
+ if (code === 'UrlParseError') {
28
+ return new VcError(error.message, VcErrorCode.INVALID_REMOTE_URL);
29
+ }
30
+ return undefined;
31
+ }
32
+ const FIELD_MAP = {
33
+ 'user.email': 'email',
34
+ 'user.name': 'name',
35
+ };
36
+ /**
37
+ * Handles vc:* events (Version Control commands).
38
+ */
39
+ export class VcHandler {
40
+ broadcastToProject;
41
+ contextTreeService;
42
+ gitRemoteBaseUrl;
43
+ gitService;
44
+ projectConfigStore;
45
+ resolveProjectPath;
46
+ spaceService;
47
+ teamService;
48
+ tokenStore;
49
+ transport;
50
+ vcGitConfigStore;
51
+ webAppUrl;
52
+ constructor(deps) {
53
+ this.broadcastToProject = deps.broadcastToProject;
54
+ this.gitRemoteBaseUrl = deps.gitRemoteBaseUrl;
55
+ this.contextTreeService = deps.contextTreeService;
56
+ this.gitService = deps.gitService;
57
+ this.projectConfigStore = deps.projectConfigStore;
58
+ this.resolveProjectPath = deps.resolveProjectPath;
59
+ this.spaceService = deps.spaceService;
60
+ this.teamService = deps.teamService;
61
+ this.tokenStore = deps.tokenStore;
62
+ this.transport = deps.transport;
63
+ this.vcGitConfigStore = deps.vcGitConfigStore;
64
+ this.webAppUrl = deps.webAppUrl;
65
+ }
66
+ setup() {
67
+ this.transport.onRequest(VcEvents.BRANCH, (data, clientId) => this.handleBranch(data, clientId));
68
+ this.transport.onRequest(VcEvents.CHECKOUT, (data, clientId) => this.handleCheckout(data, clientId));
69
+ this.transport.onRequest(VcEvents.CLONE, (data, clientId) => this.handleClone(data, clientId));
70
+ this.transport.onRequest(VcEvents.ADD, (data, clientId) => this.handleAdd(data, clientId));
71
+ this.transport.onRequest(VcEvents.COMMIT, (data, clientId) => this.handleCommit(data, clientId));
72
+ this.transport.onRequest(VcEvents.CONFIG, (data, clientId) => this.handleConfig(data, clientId));
73
+ this.transport.onRequest(VcEvents.FETCH, (data, clientId) => this.handleFetch(data, clientId));
74
+ this.transport.onRequest(VcEvents.INIT, (_data, clientId) => this.handleInit(clientId));
75
+ this.transport.onRequest(VcEvents.LOG, (data, clientId) => this.handleLog(data, clientId));
76
+ this.transport.onRequest(VcEvents.MERGE, (data, clientId) => this.handleMerge(data, clientId));
77
+ this.transport.onRequest(VcEvents.PULL, (data, clientId) => this.handlePull(data, clientId));
78
+ this.transport.onRequest(VcEvents.PUSH, (data, clientId) => this.handlePush(data, clientId));
79
+ this.transport.onRequest(VcEvents.REMOTE, (data, clientId) => this.handleRemote(data, clientId));
80
+ this.transport.onRequest(VcEvents.RESET, (data, clientId) => this.handleReset(data, clientId));
81
+ this.transport.onRequest(VcEvents.STATUS, (_data, clientId) => this.handleStatus(clientId));
82
+ }
83
+ async buildAuthorHint(existing) {
84
+ try {
85
+ const token = await this.tokenStore.load();
86
+ if (token?.isValid()) {
87
+ const email = existing?.email ?? token.userEmail;
88
+ const name = existing?.name ?? token.userName ?? token.userEmail;
89
+ return `Run: brv vc config user.name '${name}' and brv vc config user.email '${email}'.`;
90
+ }
91
+ }
92
+ catch {
93
+ // not logged in
94
+ }
95
+ return 'Run: brv vc config user.name <value> and brv vc config user.email <value>.';
96
+ }
97
+ buildNoRemoteMessage(nextStep) {
98
+ return (`No remote configured.\n\nTo connect to cloud:\n` +
99
+ ` 1. Go to ${this.webAppUrl} → create or open a Space\n` +
100
+ ` 2. Copy the remote URL\n` +
101
+ ` 3. Run: brv vc remote add origin <url>\n` +
102
+ ` 4. Then: ${nextStep}`);
103
+ }
104
+ /**
105
+ * Writes a .gitignore to the context-tree directory only if one does not already exist.
106
+ */
107
+ async ensureGitignore(contextTreeDir) {
108
+ const gitignorePath = join(contextTreeDir, '.gitignore');
109
+ try {
110
+ await fs.promises.access(gitignorePath);
111
+ }
112
+ catch {
113
+ await fs.promises.writeFile(gitignorePath, CONTEXT_TREE_GITIGNORE, 'utf8');
114
+ }
115
+ }
116
+ /**
117
+ * When force is NOT set, checks for uncommitted changes and throws
118
+ * VcError(UNCOMMITTED_CHANGES) if the working tree is dirty.
119
+ * When force IS set, skips the check entirely (changes will be discarded).
120
+ */
121
+ async guardUncommittedChanges(force, directory) {
122
+ if (force)
123
+ return;
124
+ const status = await this.gitService.status({ directory });
125
+ const hasTrackedChanges = status.files.some((f) => f.status !== 'untracked');
126
+ if (hasTrackedChanges) {
127
+ throw new VcError('You have uncommitted changes that would be overwritten. Commit your changes or use --force to discard them.', VcErrorCode.UNCOMMITTED_CHANGES);
128
+ }
129
+ }
130
+ async handleAdd(data, clientId) {
131
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
132
+ const directory = this.contextTreeService.resolvePath(projectPath);
133
+ const gitInitialized = await this.gitService.isInitialized({ directory });
134
+ if (!gitInitialized) {
135
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
136
+ }
137
+ const statusBefore = await this.gitService.status({ directory });
138
+ const stagedBefore = new Set(statusBefore.files.filter((f) => f.staged).map((f) => f.path));
139
+ const hadUnstagedBefore = new Set(statusBefore.files.filter((f) => !f.staged).map((f) => f.path));
140
+ await this.gitService.add({ directory, filePaths: data.filePaths ?? ['.'] });
141
+ const statusAfter = await this.gitService.status({ directory });
142
+ const count = statusAfter.files.filter((f) => f.staged && (!stagedBefore.has(f.path) || hadUnstagedBefore.has(f.path))).length;
143
+ return { count };
144
+ }
145
+ async handleBranch(data, clientId) {
146
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
147
+ const directory = this.contextTreeService.resolvePath(projectPath);
148
+ const gitInitialized = await this.gitService.isInitialized({ directory });
149
+ if (!gitInitialized) {
150
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
151
+ }
152
+ if (data.action === 'list')
153
+ return this.handleBranchList(directory, data.all);
154
+ // Runtime guard: `name` is guaranteed by the discriminated union at compile time,
155
+ // but transport payloads are untrusted — validate at the boundary.
156
+ if (data.action === 'create' || data.action === 'delete') {
157
+ if (!data.name)
158
+ throw new VcError('Branch name is required.', VcErrorCode.INVALID_BRANCH_NAME);
159
+ if (data.action === 'create')
160
+ return this.handleBranchCreate(directory, data.name);
161
+ return this.handleBranchDelete(directory, data.name);
162
+ }
163
+ if (data.action === 'set-upstream') {
164
+ return this.handleBranchSetUpstream(directory, data.upstream);
165
+ }
166
+ throw new VcError(`Unknown branch action.`, VcErrorCode.INVALID_ACTION);
167
+ }
168
+ async handleBranchCreate(directory, name) {
169
+ if (!isValidBranchName(name)) {
170
+ throw new VcError(`Invalid branch name: '${name}'.`, VcErrorCode.INVALID_BRANCH_NAME);
171
+ }
172
+ const existing = await this.gitService.listBranches({ directory });
173
+ if (existing.some((b) => b.name === name)) {
174
+ throw new VcError(`Branch '${name}' already exists.`, VcErrorCode.BRANCH_ALREADY_EXISTS);
175
+ }
176
+ // Block branch creation on empty repo (no commits yet) — matches native git behavior
177
+ const commits = await this.gitService.log({ depth: 1, directory });
178
+ if (commits.length === 0) {
179
+ throw new VcError('You must make an initial commit before creating branches.', VcErrorCode.NO_COMMITS);
180
+ }
181
+ await this.gitService.createBranch({ branch: name, directory });
182
+ return { action: 'create', created: name };
183
+ }
184
+ async handleBranchDelete(directory, name) {
185
+ const current = await this.gitService.getCurrentBranch({ directory });
186
+ if (name === current) {
187
+ throw new VcError(`Cannot delete current branch '${name}'.`, VcErrorCode.CANNOT_DELETE_CURRENT_BRANCH);
188
+ }
189
+ const localBranches = await this.gitService.listBranches({ directory });
190
+ if (!localBranches.some((b) => b.name === name)) {
191
+ throw new VcError(`Branch '${name}' not found.`, VcErrorCode.BRANCH_NOT_FOUND);
192
+ }
193
+ // Safe delete: verify branch is fully merged into current branch
194
+ if (current) {
195
+ const isMerged = await this.gitService.isAncestor({
196
+ ancestor: `refs/heads/${name}`,
197
+ commit: `refs/heads/${current}`,
198
+ directory,
199
+ });
200
+ if (!isMerged) {
201
+ throw new VcError(`The branch '${name}' is not fully merged.`, VcErrorCode.BRANCH_NOT_MERGED);
202
+ }
203
+ }
204
+ await this.gitService.deleteBranch({ branch: name, directory });
205
+ return { action: 'delete', deleted: name };
206
+ }
207
+ async handleBranchList(directory, all) {
208
+ const branches = await this.gitService.listBranches({
209
+ directory,
210
+ remote: all ? 'origin' : undefined,
211
+ });
212
+ return {
213
+ action: 'list',
214
+ branches: branches.map((b) => ({
215
+ isCurrent: b.isCurrent,
216
+ isRemote: b.isRemote,
217
+ name: b.name,
218
+ })),
219
+ };
220
+ }
221
+ async handleBranchSetUpstream(directory, upstream) {
222
+ const slashIndex = upstream.indexOf('/');
223
+ if (slashIndex <= 0) {
224
+ throw new VcError(`Invalid upstream format '${upstream}'. Expected <remote>/<branch> (e.g. origin/main).`, VcErrorCode.INVALID_BRANCH_NAME);
225
+ }
226
+ const remote = upstream.slice(0, slashIndex);
227
+ const remoteBranch = upstream.slice(slashIndex + 1);
228
+ if (!remoteBranch) {
229
+ throw new VcError(`Invalid upstream format '${upstream}'. Expected <remote>/<branch> (e.g. origin/main).`, VcErrorCode.INVALID_BRANCH_NAME);
230
+ }
231
+ const currentBranch = await this.gitService.getCurrentBranch({ directory });
232
+ if (!currentBranch) {
233
+ throw new VcError('Cannot set upstream in detached HEAD state.', VcErrorCode.INVALID_BRANCH_NAME);
234
+ }
235
+ // Validate the remote exists
236
+ const remotes = await this.gitService.listRemotes({ directory });
237
+ if (!remotes.some((r) => r.remote === remote)) {
238
+ throw new VcError(`Remote '${remote}' not found.`, VcErrorCode.NO_REMOTE);
239
+ }
240
+ // Validate the remote-tracking branch exists
241
+ const remoteBranches = await this.gitService.listBranches({ directory, remote });
242
+ if (!remoteBranches.some((b) => b.isRemote && b.name === `${remote}/${remoteBranch}`)) {
243
+ throw new VcError(`The requested upstream branch '${upstream}' does not exist.`, VcErrorCode.BRANCH_NOT_FOUND);
244
+ }
245
+ await this.gitService.setTrackingBranch({ branch: currentBranch, directory, remote, remoteBranch });
246
+ return { action: 'set-upstream', branch: currentBranch, upstream };
247
+ }
248
+ async handleCheckout(data, clientId) {
249
+ // ── Phase 1: Resolve project and validate inputs ──
250
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
251
+ const directory = this.contextTreeService.resolvePath(projectPath);
252
+ const gitInitialized = await this.gitService.isInitialized({ directory });
253
+ if (!gitInitialized) {
254
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
255
+ }
256
+ this.validateBranchName(data.branch);
257
+ // ── Phase 2: Resolve current branch ──
258
+ const previousBranch = await this.gitService.getCurrentBranch({ directory });
259
+ // ── Phase 3: Create or switch ──
260
+ if (data.create) {
261
+ const branches = await this.gitService.listBranches({ directory });
262
+ if (branches.some((b) => b.name === data.branch)) {
263
+ throw new VcError(`Branch '${data.branch}' already exists.`, VcErrorCode.BRANCH_ALREADY_EXISTS);
264
+ }
265
+ await this.gitService.createBranch({ branch: data.branch, checkout: true, directory });
266
+ return { branch: data.branch, created: true, previousBranch };
267
+ }
268
+ try {
269
+ await this.gitService.checkout({ directory, force: data.force, ref: data.branch });
270
+ // Clear merge state after force checkout (like git checkout --force)
271
+ if (data.force) {
272
+ const mergeHeadPath = join(directory, '.git', 'MERGE_HEAD');
273
+ const mergeMsgPath = join(directory, '.git', 'MERGE_MSG');
274
+ await fs.promises.rm(mergeHeadPath, { force: true }).catch(() => { });
275
+ await fs.promises.rm(mergeMsgPath, { force: true }).catch(() => { });
276
+ }
277
+ }
278
+ catch (error) {
279
+ // Dirty files that conflict with target branch (matches native git behavior)
280
+ if (error instanceof GitError && error.message.includes('would be overwritten')) {
281
+ throw new VcError(error.message, VcErrorCode.UNCOMMITTED_CHANGES);
282
+ }
283
+ if (error instanceof Error && 'code' in error && error.code === 'NotFoundError') {
284
+ // Distinguish empty repo from branch-not-found
285
+ const commits = await this.gitService.log({ depth: 1, directory });
286
+ if (commits.length === 0) {
287
+ throw new VcError(`Your current branch does not have any commits yet. Run 'brv vc add' and 'brv vc commit' first.`, VcErrorCode.NO_COMMITS);
288
+ }
289
+ throw new VcError(`Branch '${data.branch}' not found. Use 'brv vc checkout -b ${data.branch}' to create it.`, VcErrorCode.BRANCH_NOT_FOUND);
290
+ }
291
+ throw error;
292
+ }
293
+ return { branch: data.branch, created: false, previousBranch };
294
+ }
295
+ async handleClone(data, clientId) {
296
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
297
+ const contextTreeDir = this.contextTreeService.resolvePath(projectPath);
298
+ if (await this.gitService.isInitialized({ directory: contextTreeDir })) {
299
+ const isEmpty = await this.gitService.isEmptyRepository({ directory: contextTreeDir });
300
+ if (!isEmpty) {
301
+ throw new VcError('Already initialized. Use brv vc pull to sync.', VcErrorCode.ALREADY_INITIALIZED);
302
+ }
303
+ // Fresh auto-init — remove .git and .gitignore so clone starts clean
304
+ await fs.promises.rm(join(contextTreeDir, '.git'), { force: true, recursive: true });
305
+ await fs.promises.rm(join(contextTreeDir, '.gitignore'), { force: true }).catch(() => { });
306
+ }
307
+ const { spaceId, spaceName, spaceSlug, teamId, teamName, teamSlug, url: cloneUrl } = await this.resolveCloneInput(data);
308
+ const label = teamName && spaceName ? `${teamName}/${spaceName}` : 'repository';
309
+ try {
310
+ await this.contextTreeService.initialize(projectPath);
311
+ this.broadcastToProject(projectPath, VcEvents.CLONE_PROGRESS, {
312
+ message: `Remote: ${data.url ?? label}`,
313
+ step: 'cloning',
314
+ });
315
+ this.broadcastToProject(projectPath, VcEvents.CLONE_PROGRESS, {
316
+ message: `Cloning from ${label}...`,
317
+ step: 'cloning',
318
+ });
319
+ let lastPhase = '';
320
+ await this.gitService.clone({
321
+ directory: contextTreeDir,
322
+ onProgress: ({ phase, total }) => {
323
+ if (phase !== lastPhase) {
324
+ lastPhase = phase;
325
+ const totalStr = total === undefined ? '' : ` (${total})`;
326
+ this.broadcastToProject(projectPath, VcEvents.CLONE_PROGRESS, {
327
+ message: `${phase}${totalStr}...`,
328
+ step: 'cloning',
329
+ });
330
+ }
331
+ },
332
+ url: cloneUrl,
333
+ });
334
+ this.broadcastToProject(projectPath, VcEvents.CLONE_PROGRESS, {
335
+ message: 'Saving configuration...',
336
+ step: 'saving',
337
+ });
338
+ if (spaceId && spaceName && teamId && teamName) {
339
+ const space = new Space({
340
+ id: spaceId,
341
+ isDefault: false,
342
+ name: spaceName,
343
+ slug: spaceSlug,
344
+ teamId,
345
+ teamName,
346
+ teamSlug,
347
+ });
348
+ const existing = await this.projectConfigStore.read(projectPath);
349
+ const updated = existing ? existing.withSpace(space) : BrvConfig.partialFromSpace({ space });
350
+ await this.projectConfigStore.write(updated, projectPath);
351
+ }
352
+ // Ensure .gitignore exists (remote may not have one)
353
+ await this.ensureGitignore(contextTreeDir);
354
+ // Add .brv entries to project .gitignore (prevents `git add .` fatal error from nested .git)
355
+ await ensureGitignoreEntries(projectPath);
356
+ }
357
+ catch (error) {
358
+ // Rollback partial .git — keep context tree intact
359
+ await fs.promises.rm(join(contextTreeDir, '.git'), { force: true, recursive: true }).catch(() => { });
360
+ if (error instanceof GitAuthError) {
361
+ throw new VcError('Authentication failed. Run brv login.', VcErrorCode.AUTH_FAILED);
362
+ }
363
+ const classified = classifyIsomorphicGitError(error, VcErrorCode.INVALID_REMOTE_URL);
364
+ if (classified)
365
+ throw classified;
366
+ const msg = error instanceof Error ? error.message : String(error);
367
+ throw new VcError(`Clone failed: ${msg}`, VcErrorCode.CLONE_FAILED);
368
+ }
369
+ return {
370
+ gitDir: join(contextTreeDir, '.git'),
371
+ spaceName,
372
+ teamName,
373
+ };
374
+ }
375
+ async handleCommit(data, clientId) {
376
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
377
+ const directory = this.contextTreeService.resolvePath(projectPath);
378
+ const gitInitialized = await this.gitService.isInitialized({ directory });
379
+ if (!gitInitialized) {
380
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
381
+ }
382
+ const status = await this.gitService.status({ directory });
383
+ const hasStagedFiles = status.files.some((f) => f.staged);
384
+ if (!hasStagedFiles) {
385
+ throw new VcError('Nothing staged.', VcErrorCode.NOTHING_STAGED);
386
+ }
387
+ const config = await this.vcGitConfigStore.get(projectPath);
388
+ if (!config?.name || !config.email) {
389
+ const hint = await this.buildAuthorHint(config);
390
+ throw new VcError(`Commit author not configured. ${hint}`, VcErrorCode.USER_NOT_CONFIGURED);
391
+ }
392
+ const commit = await this.gitService.commit({
393
+ author: { email: config.email, name: config.name },
394
+ directory,
395
+ message: data.message,
396
+ });
397
+ return { message: commit.message, sha: commit.sha };
398
+ }
399
+ async handleConfig(data, clientId) {
400
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
401
+ const field = FIELD_MAP[data.key];
402
+ if (!field) {
403
+ throw new VcError(`Unknown key '${data.key}'. Allowed: user.name, user.email.`, VcErrorCode.INVALID_CONFIG_KEY);
404
+ }
405
+ if (data.value !== undefined) {
406
+ // SET: read existing → merge single field → write back
407
+ const existing = (await this.vcGitConfigStore.get(projectPath)) ?? {};
408
+ const merged = { ...existing, [field]: data.value };
409
+ await this.vcGitConfigStore.set(projectPath, merged);
410
+ return { key: data.key, value: data.value };
411
+ }
412
+ // GET
413
+ const config = await this.vcGitConfigStore.get(projectPath);
414
+ const value = config?.[field];
415
+ if (value === undefined) {
416
+ throw new VcError(`'${data.key}' is not set.`, VcErrorCode.CONFIG_KEY_NOT_SET);
417
+ }
418
+ return { key: data.key, value };
419
+ }
420
+ async handleFetch(data, clientId) {
421
+ const token = await this.tokenStore.load();
422
+ if (!token?.isValid())
423
+ throw new NotAuthenticatedError();
424
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
425
+ const directory = this.contextTreeService.resolvePath(projectPath);
426
+ const gitInitialized = await this.gitService.isInitialized({ directory });
427
+ if (!gitInitialized) {
428
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
429
+ }
430
+ const remotes = await this.gitService.listRemotes({ directory });
431
+ if (remotes.length === 0) {
432
+ throw new VcError(this.buildNoRemoteMessage('brv vc fetch'), VcErrorCode.NO_REMOTE);
433
+ }
434
+ const remote = data.remote ?? 'origin';
435
+ try {
436
+ await this.gitService.fetch({ directory, ref: data.ref, remote });
437
+ }
438
+ catch (error) {
439
+ if (error instanceof GitAuthError) {
440
+ throw new VcError('Authentication failed. Run brv login.', VcErrorCode.AUTH_FAILED);
441
+ }
442
+ const classified = classifyIsomorphicGitError(error, VcErrorCode.INVALID_REF);
443
+ if (classified)
444
+ throw classified;
445
+ const message = error instanceof Error ? error.message : 'Fetch failed.';
446
+ throw new VcError(message, VcErrorCode.FETCH_FAILED);
447
+ }
448
+ return { remote };
449
+ }
450
+ async handleInit(clientId) {
451
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
452
+ // 1. Ensure context tree directory exists
453
+ const contextTreeDir = await this.contextTreeService.initialize(projectPath);
454
+ // 2. Git init — always call (idempotent, like real `git init`).
455
+ // Check beforehand to determine whether this is a fresh init or a reinit.
456
+ const reinitialized = await this.gitService.isInitialized({ directory: contextTreeDir });
457
+ await this.gitService.init({ defaultBranch: 'main', directory: contextTreeDir });
458
+ // 3. Ensure .gitignore exists with correct content (idempotent)
459
+ await this.ensureGitignore(contextTreeDir);
460
+ // 4. Add .brv entries to project .gitignore (prevents `git add .` fatal error from nested .git)
461
+ await ensureGitignoreEntries(projectPath);
462
+ return {
463
+ gitDir: join(contextTreeDir, '.git'),
464
+ reinitialized,
465
+ };
466
+ }
467
+ async handleLog(data, clientId) {
468
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
469
+ const contextTreeDir = this.contextTreeService.resolvePath(projectPath);
470
+ const gitInitialized = await this.gitService.isInitialized({ directory: contextTreeDir });
471
+ if (!gitInitialized) {
472
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
473
+ }
474
+ const hasCommits = await this.gitService.log({ depth: 1, directory: contextTreeDir }).then((c) => c.length > 0);
475
+ if (!hasCommits) {
476
+ const branch = await this.gitService.getCurrentBranch({ directory: contextTreeDir });
477
+ throw new VcError(`Your current branch '${branch ?? 'main'}' does not have any commits yet.`, VcErrorCode.NO_COMMITS);
478
+ }
479
+ const { commits, displayBranch } = await this.resolveLogResult(data, contextTreeDir);
480
+ return {
481
+ commits: commits.map((c) => ({
482
+ author: c.author,
483
+ message: c.message,
484
+ sha: c.sha,
485
+ timestamp: c.timestamp.toISOString(),
486
+ })),
487
+ currentBranch: displayBranch,
488
+ };
489
+ }
490
+ async handleMerge(data, clientId) {
491
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
492
+ const directory = this.contextTreeService.resolvePath(projectPath);
493
+ const gitInitialized = await this.gitService.isInitialized({ directory });
494
+ if (!gitInitialized) {
495
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
496
+ }
497
+ const mergeHeadPath = join(directory, '.git', 'MERGE_HEAD');
498
+ const mergeMsgPath = join(directory, '.git', 'MERGE_MSG');
499
+ const hasMergeHead = await fs.promises
500
+ .access(mergeHeadPath)
501
+ .then(() => true)
502
+ .catch(() => false);
503
+ if (data.action === 'abort') {
504
+ if (!hasMergeHead) {
505
+ throw new VcError('There is no merge to abort (MERGE_HEAD missing).', VcErrorCode.NO_MERGE_IN_PROGRESS);
506
+ }
507
+ await this.gitService.abortMerge({ directory });
508
+ return { action: 'abort' };
509
+ }
510
+ if (data.action === 'continue') {
511
+ if (!hasMergeHead) {
512
+ throw new VcError('There is no merge in progress (MERGE_HEAD missing).', VcErrorCode.NO_MERGE_IN_PROGRESS);
513
+ }
514
+ if (!data.message) {
515
+ // Return default message so oclif can open editor; TUI uses it directly
516
+ const defaultMessage = await fs.promises
517
+ .readFile(mergeMsgPath, 'utf8')
518
+ .then((content) => content.trim())
519
+ .catch(() => 'Merge commit');
520
+ return { action: 'continue', defaultMessage };
521
+ }
522
+ // Check for unresolved conflicts before committing
523
+ const conflicts = await this.gitService.getConflicts({ directory });
524
+ if (conflicts.length > 0) {
525
+ throw new VcError('Committing is not possible because you have unmerged files.', VcErrorCode.MERGE_CONFLICT);
526
+ }
527
+ const config = await this.vcGitConfigStore.get(projectPath);
528
+ if (!config?.name || !config.email) {
529
+ const hint = await this.buildAuthorHint(config);
530
+ throw new VcError(`Commit author not configured. ${hint}`, VcErrorCode.USER_NOT_CONFIGURED);
531
+ }
532
+ await this.gitService.commit({
533
+ author: { email: config.email, name: config.name },
534
+ directory,
535
+ message: data.message,
536
+ });
537
+ return { action: 'continue' };
538
+ }
539
+ // action: 'merge'
540
+ if (!data.branch) {
541
+ throw new VcError('Branch name is required for merge.', VcErrorCode.INVALID_BRANCH_NAME);
542
+ }
543
+ if (!isValidBranchName(data.branch)) {
544
+ throw new VcError(`Invalid branch name: '${data.branch}'.`, VcErrorCode.INVALID_BRANCH_NAME);
545
+ }
546
+ if (hasMergeHead) {
547
+ throw new VcError('You have not concluded your merge (MERGE_HEAD exists).', VcErrorCode.MERGE_IN_PROGRESS);
548
+ }
549
+ await this.guardUncommittedChanges(false, directory);
550
+ // Self-merge check
551
+ const currentBranch = await this.gitService.getCurrentBranch({ directory });
552
+ if (currentBranch && data.branch === currentBranch) {
553
+ return { action: 'merge', alreadyUpToDate: true, branch: data.branch };
554
+ }
555
+ // Validate branch exists (check both local and remote-tracking branches)
556
+ const branches = await this.gitService.listBranches({ directory, remote: 'origin' });
557
+ if (!branches.some((b) => b.name === data.branch)) {
558
+ throw new VcError(`merge: ${data.branch} - not something we can merge`, VcErrorCode.BRANCH_NOT_FOUND);
559
+ }
560
+ const config = await this.vcGitConfigStore.get(projectPath);
561
+ if (!config?.name || !config.email) {
562
+ const hint = await this.buildAuthorHint(config);
563
+ throw new VcError(`Commit author not configured. ${hint}`, VcErrorCode.USER_NOT_CONFIGURED);
564
+ }
565
+ const result = await this.gitService.merge({
566
+ allowUnrelatedHistories: data.allowUnrelatedHistories,
567
+ author: { email: config.email, name: config.name },
568
+ branch: data.branch,
569
+ directory,
570
+ message: data.message,
571
+ });
572
+ if (!result.success) {
573
+ return {
574
+ action: 'merge',
575
+ branch: data.branch,
576
+ conflicts: result.conflicts.map((c) => ({ path: c.path, type: c.type })),
577
+ };
578
+ }
579
+ if (result.alreadyUpToDate) {
580
+ return { action: 'merge', alreadyUpToDate: true, branch: data.branch };
581
+ }
582
+ return { action: 'merge', branch: data.branch };
583
+ }
584
+ async handlePull(data, clientId) {
585
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
586
+ const directory = this.contextTreeService.resolvePath(projectPath);
587
+ const gitInitialized = await this.gitService.isInitialized({ directory });
588
+ if (!gitInitialized) {
589
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
590
+ }
591
+ const token = await this.tokenStore.load();
592
+ if (!token?.isValid())
593
+ throw new NotAuthenticatedError();
594
+ const remotes = await this.gitService.listRemotes({ directory });
595
+ if (remotes.length === 0) {
596
+ throw new VcError(this.buildNoRemoteMessage('brv vc pull origin main'), VcErrorCode.NO_REMOTE);
597
+ }
598
+ // Soft resolve author: use vc config if available, otherwise let pull() fallback to getAuthor() from auth token.
599
+ // Unlike commit/merge, pull only needs author when creating a merge commit (not for up-to-date or fast-forward).
600
+ const config = await this.vcGitConfigStore.get(projectPath);
601
+ const author = config?.name && config?.email ? { email: config.email, name: config.name } : undefined;
602
+ // If explicit branch provided, use it directly (skip tracking resolution)
603
+ const remote = data?.remote ?? 'origin';
604
+ const branch = data?.branch ?? (await this.resolvePullBranch(directory));
605
+ let alreadyUpToDate = false;
606
+ let conflicts;
607
+ try {
608
+ const result = await this.gitService.pull({
609
+ allowUnrelatedHistories: data?.allowUnrelatedHistories,
610
+ author,
611
+ branch,
612
+ directory,
613
+ remote,
614
+ });
615
+ if (!result.success) {
616
+ conflicts = result.conflicts.map((c) => ({ path: c.path, type: c.type }));
617
+ return { branch, conflicts };
618
+ }
619
+ alreadyUpToDate = result.alreadyUpToDate ?? false;
620
+ }
621
+ catch (error) {
622
+ if (error instanceof VcError)
623
+ throw error;
624
+ if (error instanceof GitAuthError) {
625
+ throw new VcError('Authentication failed. Run brv login.', VcErrorCode.AUTH_FAILED);
626
+ }
627
+ if (error instanceof GitError) {
628
+ if (error.message.includes('unresolved merge conflicts')) {
629
+ throw new VcError(error.message, VcErrorCode.MERGE_IN_PROGRESS);
630
+ }
631
+ if (error.message.includes('would be overwritten')) {
632
+ throw new VcError(error.message, VcErrorCode.UNCOMMITTED_CHANGES);
633
+ }
634
+ if (error.message.includes('unrelated histories')) {
635
+ throw new VcError(error.message, VcErrorCode.UNRELATED_HISTORIES);
636
+ }
637
+ }
638
+ const classified = classifyIsomorphicGitError(error, VcErrorCode.INVALID_REF);
639
+ if (classified)
640
+ throw classified;
641
+ const message = error instanceof Error ? error.message : 'Pull failed. Check your connection and try again.';
642
+ throw new VcError(message, VcErrorCode.PULL_FAILED);
643
+ }
644
+ return { alreadyUpToDate, branch };
645
+ }
646
+ async handlePush(data, clientId) {
647
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
648
+ const directory = this.contextTreeService.resolvePath(projectPath);
649
+ const gitInitialized = await this.gitService.isInitialized({ directory });
650
+ if (!gitInitialized) {
651
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
652
+ }
653
+ const token = await this.tokenStore.load();
654
+ if (!token?.isValid())
655
+ throw new NotAuthenticatedError();
656
+ const remotes = await this.gitService.listRemotes({ directory });
657
+ if (remotes.length === 0) {
658
+ throw new VcError(this.buildNoRemoteMessage('brv vc push -u origin main'), VcErrorCode.NO_REMOTE);
659
+ }
660
+ const commits = await this.gitService.log({ depth: 1, directory });
661
+ if (commits.length === 0) {
662
+ throw new VcError('No commits to push. Run brv vc add and brv vc commit first.', VcErrorCode.NOTHING_TO_PUSH);
663
+ }
664
+ // Block push while conflict markers remain in tracked files
665
+ const conflictFiles = await this.gitService.getFilesWithConflictMarkers({ directory });
666
+ if (conflictFiles.length > 0) {
667
+ throw new VcError(`Conflict markers detected in: ${conflictFiles.join(', ')}. Resolve conflicts before pushing.`, VcErrorCode.CONFLICT_MARKERS_PRESENT);
668
+ }
669
+ const branch = await this.resolveTargetBranch(data.branch, directory);
670
+ // Block push when no upstream tracking and no explicit branch — like git:
671
+ // git push → error if no tracking
672
+ // git push origin X → OK without tracking (explicit target)
673
+ // git push -u → OK (will set tracking)
674
+ const explicitBranch = Boolean(data.branch?.trim());
675
+ const existingTracking = await this.gitService.getTrackingBranch({ branch, directory });
676
+ if (!existingTracking && !explicitBranch && !data.setUpstream) {
677
+ throw new VcError(`The current branch '${branch}' has no upstream branch.\n` +
678
+ `To push the current branch and set the remote as upstream, use\n\n` +
679
+ ` brv vc push -u origin ${branch}`, VcErrorCode.NO_UPSTREAM);
680
+ }
681
+ // Set upstream tracking BEFORE push so pull works even if push fails with non_fast_forward
682
+ let upstreamSet = false;
683
+ if (data.setUpstream) {
684
+ await this.gitService.setTrackingBranch({ branch, directory, remote: 'origin', remoteBranch: branch });
685
+ upstreamSet = true;
686
+ }
687
+ let alreadyUpToDate = false;
688
+ try {
689
+ const result = await this.gitService.push({ branch, directory, remote: 'origin' });
690
+ if (!result.success && result.reason === 'non_fast_forward') {
691
+ throw new VcError('Remote has changes. Pull first with brv vc pull.', VcErrorCode.NON_FAST_FORWARD);
692
+ }
693
+ if (result.success)
694
+ alreadyUpToDate = result.alreadyUpToDate ?? false;
695
+ }
696
+ catch (error) {
697
+ if (error instanceof VcError)
698
+ throw error;
699
+ if (error instanceof GitAuthError) {
700
+ throw new VcError('Authentication failed. Run brv login.', VcErrorCode.AUTH_FAILED);
701
+ }
702
+ const classified = classifyIsomorphicGitError(error, VcErrorCode.INVALID_REF);
703
+ if (classified)
704
+ throw classified;
705
+ const message = error instanceof Error ? error.message : 'Push failed. Check your connection and try again.';
706
+ throw new VcError(message, VcErrorCode.PUSH_FAILED);
707
+ }
708
+ return { alreadyUpToDate, branch, upstreamSet };
709
+ }
710
+ async handleRemote(data, clientId) {
711
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
712
+ const directory = this.contextTreeService.resolvePath(projectPath);
713
+ const gitInitialized = await this.gitService.isInitialized({ directory });
714
+ if (!gitInitialized) {
715
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
716
+ }
717
+ if (data.subcommand === 'show') {
718
+ const url = await this.gitService.getRemoteUrl({ directory, remote: 'origin' });
719
+ return { action: 'show', url: url ? maskCredentialsInUrl(url) : undefined };
720
+ }
721
+ if (!data.url) {
722
+ throw new VcError('URL is required.', VcErrorCode.INVALID_REMOTE_URL);
723
+ }
724
+ // Check local state before hitting the server — fail fast for duplicate remote
725
+ if (data.subcommand === 'add') {
726
+ const existing = await this.gitService.getRemoteUrl({ directory, remote: 'origin' });
727
+ if (existing) {
728
+ throw new VcError("Remote 'origin' already exists. Use brv vc remote set-url <url> to update.", VcErrorCode.REMOTE_ALREADY_EXISTS);
729
+ }
730
+ }
731
+ const resolved = await this.resolveFullCogitUrl(data.url);
732
+ if (data.subcommand === 'add') {
733
+ await this.gitService.addRemote({ directory, remote: 'origin', url: resolved.url });
734
+ }
735
+ else {
736
+ // set-url
737
+ await this.gitService.removeRemote({ directory, remote: 'origin' }).catch(() => {
738
+ // ignore if remote doesn't exist
739
+ });
740
+ await this.gitService.addRemote({ directory, remote: 'origin', url: resolved.url });
741
+ }
742
+ // Persist space/team to config (same pattern as handleClone)
743
+ if (resolved.spaceId && resolved.spaceName && resolved.teamId && resolved.teamName) {
744
+ const space = new Space({
745
+ id: resolved.spaceId,
746
+ isDefault: false,
747
+ name: resolved.spaceName,
748
+ slug: resolved.spaceSlug,
749
+ teamId: resolved.teamId,
750
+ teamName: resolved.teamName,
751
+ teamSlug: resolved.teamSlug,
752
+ });
753
+ const existing = await this.projectConfigStore.read(projectPath);
754
+ const updated = existing ? existing.withSpace(space) : BrvConfig.partialFromSpace({ space });
755
+ await this.projectConfigStore.write(updated, projectPath);
756
+ }
757
+ return { action: data.subcommand === 'add' ? 'add' : 'set-url', url: resolved.url };
758
+ }
759
+ async handleReset(data, clientId) {
760
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
761
+ const directory = this.contextTreeService.resolvePath(projectPath);
762
+ const gitInitialized = await this.gitService.isInitialized({ directory });
763
+ if (!gitInitialized) {
764
+ throw new VcError('ByteRover version control not initialized.', VcErrorCode.GIT_NOT_INITIALIZED);
765
+ }
766
+ const mode = data.filePaths ? 'mixed' : (data.mode ?? 'mixed');
767
+ // Block soft/hard reset during active merge
768
+ if (mode !== 'mixed' || (data.ref && data.ref !== 'HEAD')) {
769
+ const hasMergeHead = await fs.promises
770
+ .access(join(directory, '.git', 'MERGE_HEAD'))
771
+ .then(() => true)
772
+ .catch(() => false);
773
+ if (hasMergeHead && mode !== 'hard') {
774
+ throw new VcError('Cannot reset while a merge is in progress. Abort or complete the merge first.', VcErrorCode.MERGE_IN_PROGRESS);
775
+ }
776
+ }
777
+ try {
778
+ const result = await this.gitService.reset({
779
+ directory,
780
+ filePaths: data.filePaths,
781
+ mode: data.filePaths ? undefined : mode,
782
+ ref: data.ref,
783
+ });
784
+ const isUnstage = Boolean(data.filePaths) || (mode === 'mixed' && (!data.ref || data.ref === 'HEAD'));
785
+ return {
786
+ filesUnstaged: isUnstage ? result.filesChanged : undefined,
787
+ headSha: isUnstage ? undefined : result.headSha,
788
+ mode,
789
+ };
790
+ }
791
+ catch (error) {
792
+ if (error instanceof GitError) {
793
+ if (error.message.includes('pathspec')) {
794
+ throw new VcError(error.message, VcErrorCode.FILE_NOT_FOUND);
795
+ }
796
+ if (error.message.includes('Cannot resolve')) {
797
+ throw new VcError(error.message, VcErrorCode.INVALID_REF);
798
+ }
799
+ if (error.message.includes('detached HEAD')) {
800
+ throw new VcError(error.message, VcErrorCode.INVALID_ACTION);
801
+ }
802
+ if (error.message.includes('No commits')) {
803
+ throw new VcError(error.message, VcErrorCode.NO_COMMITS);
804
+ }
805
+ }
806
+ throw error;
807
+ }
808
+ }
809
+ async handleStatus(clientId) {
810
+ const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
811
+ const contextTreeDir = this.contextTreeService.resolvePath(projectPath);
812
+ const gitInitialized = await this.gitService.isInitialized({ directory: contextTreeDir });
813
+ if (!gitInitialized) {
814
+ return {
815
+ initialized: false,
816
+ staged: { added: [], deleted: [], modified: [] },
817
+ unstaged: { deleted: [], modified: [] },
818
+ untracked: [],
819
+ };
820
+ }
821
+ const branch = await this.gitService.getCurrentBranch({ directory: contextTreeDir });
822
+ const gitStatus = await this.gitService.status({ directory: contextTreeDir });
823
+ // Detect empty repo (no commits yet)
824
+ const hasCommits = await this.gitService
825
+ .log({ depth: 1, directory: contextTreeDir })
826
+ .then((commits) => commits.length > 0);
827
+ // Check if a merge is in progress (MERGE_HEAD exists)
828
+ const mergeInProgress = await fs.promises
829
+ .access(join(contextTreeDir, '.git', 'MERGE_HEAD'))
830
+ .then(() => true)
831
+ .catch(() => false);
832
+ // Detect unresolved conflicts during merge
833
+ const unmerged = mergeInProgress
834
+ ? (await this.gitService.getConflicts({ directory: contextTreeDir })).map((c) => ({ path: c.path, type: c.type }))
835
+ : undefined;
836
+ // Detect files with conflict markers (regardless of merge state)
837
+ const conflictMarkerFiles = await this.gitService.getFilesWithConflictMarkers({ directory: contextTreeDir });
838
+ // Filter out unmerged paths from staged/unstaged — git native only shows them in "Unmerged paths" section
839
+ const unmergedPaths = unmerged ? new Set(unmerged.map((u) => u.path)) : new Set();
840
+ const staged = gitStatus.files.filter((f) => f.staged && !unmergedPaths.has(f.path));
841
+ const unstaged = gitStatus.files.filter((f) => !f.staged && f.status !== 'untracked' && !unmergedPaths.has(f.path));
842
+ // Resolve tracking branch and ahead/behind counts
843
+ let trackingBranch;
844
+ let ahead;
845
+ let behind;
846
+ if (branch) {
847
+ const tracking = await this.gitService.getTrackingBranch({ branch, directory: contextTreeDir });
848
+ if (tracking) {
849
+ trackingBranch = `${tracking.remote}/${tracking.remoteBranch}`;
850
+ const counts = await this.gitService.getAheadBehind({
851
+ directory: contextTreeDir,
852
+ localRef: `refs/heads/${branch}`,
853
+ remoteRef: `refs/remotes/${tracking.remote}/${tracking.remoteBranch}`,
854
+ });
855
+ ahead = counts.ahead;
856
+ behind = counts.behind;
857
+ }
858
+ }
859
+ return {
860
+ ahead,
861
+ behind,
862
+ branch,
863
+ conflictMarkerFiles: conflictMarkerFiles.length > 0 ? conflictMarkerFiles : undefined,
864
+ hasCommits,
865
+ initialized: true,
866
+ mergeInProgress,
867
+ staged: {
868
+ added: staged.filter((f) => f.status === 'added').map((f) => f.path),
869
+ deleted: staged.filter((f) => f.status === 'deleted').map((f) => f.path),
870
+ modified: staged.filter((f) => f.status === 'modified').map((f) => f.path),
871
+ },
872
+ trackingBranch,
873
+ unmerged,
874
+ unstaged: {
875
+ deleted: unstaged.filter((f) => f.status === 'deleted').map((f) => f.path),
876
+ modified: unstaged.filter((f) => f.status === 'modified').map((f) => f.path),
877
+ },
878
+ untracked: gitStatus.files.filter((f) => f.status === 'untracked').map((f) => f.path),
879
+ };
880
+ }
881
+ /**
882
+ * Resolve clone request data into a clean cogit URL + team/space info.
883
+ * Accepts either a URL or explicit teamName/spaceName.
884
+ * Auth is handled by IsomorphicGitService via headers, not URL credentials.
885
+ */
886
+ async resolveCloneInput(data) {
887
+ if (data.url) {
888
+ const resolved = await this.resolveFullCogitUrl(data.url);
889
+ return {
890
+ spaceId: resolved.spaceId ?? data.spaceId,
891
+ spaceName: resolved.spaceName ?? data.spaceName,
892
+ spaceSlug: resolved.spaceSlug,
893
+ teamId: resolved.teamId ?? data.teamId,
894
+ teamName: resolved.teamName ?? data.teamName,
895
+ teamSlug: resolved.teamSlug,
896
+ url: resolved.url,
897
+ };
898
+ }
899
+ if (data.teamName && data.spaceName) {
900
+ return {
901
+ spaceId: data.spaceId,
902
+ spaceName: data.spaceName,
903
+ teamId: data.teamId,
904
+ teamName: data.teamName,
905
+ url: buildCogitRemoteUrl(this.gitRemoteBaseUrl, data.teamName, data.spaceName),
906
+ };
907
+ }
908
+ throw new VcError('URL or space selection is required.', VcErrorCode.INVALID_REMOTE_URL);
909
+ }
910
+ /**
911
+ * Resolve a remote URL to a clean cogit URL + team/space info.
912
+ * Expected format: {domain}/{teamName}/{spaceName}.git
913
+ * Resolves names to IDs via API; rejects unknown formats.
914
+ *
915
+ * Auth is handled by IsomorphicGitService via headers, not URL credentials.
916
+ */
917
+ async resolveFullCogitUrl(url) {
918
+ this.validateRemoteUrlDomain(url);
919
+ const parsed = parseUserFacingUrl(url);
920
+ if (parsed) {
921
+ return this.resolveTeamSpaceNames(parsed.teamName, parsed.spaceName);
922
+ }
923
+ throw new VcError(`Invalid URL format. Use: ${this.gitRemoteBaseUrl}/<team>/<space>.git`, VcErrorCode.INVALID_REMOTE_URL);
924
+ }
925
+ async resolveLogResult(data, contextTreeDir) {
926
+ const currentBranch = await this.gitService.getCurrentBranch({ directory: contextTreeDir });
927
+ const all = data.all ?? false;
928
+ const limit = data.limit ?? 10;
929
+ if (all) {
930
+ const branches = await this.gitService.listBranches({ directory: contextTreeDir });
931
+ const commitsByBranch = await Promise.all(branches.map((branch) => this.gitService.log({ directory: contextTreeDir, ref: branch.name })));
932
+ const seen = new Set();
933
+ const commits = commitsByBranch
934
+ .flat()
935
+ .filter((c) => {
936
+ if (seen.has(c.sha))
937
+ return false;
938
+ seen.add(c.sha);
939
+ return true;
940
+ })
941
+ .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
942
+ .slice(0, limit);
943
+ return { commits, displayBranch: currentBranch };
944
+ }
945
+ if (data.ref !== undefined) {
946
+ const branches = await this.gitService.listBranches({ directory: contextTreeDir });
947
+ const branchExists = branches.some((b) => b.name === data.ref);
948
+ if (!branchExists) {
949
+ throw new VcError(`Branch '${data.ref}' not found.`, VcErrorCode.BRANCH_NOT_FOUND);
950
+ }
951
+ }
952
+ const commits = await this.gitService.log({ depth: limit, directory: contextTreeDir, ref: data.ref });
953
+ return { commits, displayBranch: data.ref ?? currentBranch };
954
+ }
955
+ /**
956
+ * Resolves pull target branch: explicit → tracking config → error.
957
+ * Mirrors native git: `git pull` without tracking config errors.
958
+ */
959
+ async resolvePullBranch(directory) {
960
+ const current = await this.gitService.getCurrentBranch({ directory });
961
+ const currentTrimmed = current?.trim();
962
+ if (currentTrimmed) {
963
+ const tracking = await this.gitService.getTrackingBranch({ branch: currentTrimmed, directory });
964
+ if (tracking)
965
+ return tracking.remoteBranch;
966
+ // No tracking configured — error like native git
967
+ throw new VcError(`There is no tracking information for the current branch '${currentTrimmed}'.\n` +
968
+ `To pull from remote, use:\n\n` +
969
+ ` brv vc pull origin ${currentTrimmed}\n\n` +
970
+ `Or set upstream tracking with:\n\n` +
971
+ ` brv vc branch --set-upstream-to origin/${currentTrimmed}`, VcErrorCode.NO_UPSTREAM);
972
+ }
973
+ throw new VcError('Cannot determine branch for pull. Check out a branch first.', VcErrorCode.NO_BRANCH_RESOLVED);
974
+ }
975
+ async resolveTargetBranch(requestedBranch, directory) {
976
+ const trimmed = requestedBranch?.trim();
977
+ if (trimmed) {
978
+ if (!isValidBranchName(trimmed)) {
979
+ throw new VcError(`Invalid branch name: '${trimmed}'.`, VcErrorCode.INVALID_BRANCH_NAME);
980
+ }
981
+ return trimmed;
982
+ }
983
+ const current = await this.gitService.getCurrentBranch({ directory });
984
+ const currentTrimmed = current?.trim();
985
+ if (currentTrimmed)
986
+ return currentTrimmed;
987
+ return 'main';
988
+ }
989
+ /**
990
+ * Resolve team/space names to IDs via API, build clean cogit URL.
991
+ */
992
+ async resolveTeamSpaceNames(teamSlug, spaceSlug) {
993
+ const token = await this.tokenStore.load();
994
+ if (!token?.isValid())
995
+ throw new NotAuthenticatedError();
996
+ const { teams } = await this.teamService.getTeams(token.sessionKey, { fetchAll: true });
997
+ const team = teams.find((t) => t.slug.toLowerCase() === teamSlug.toLowerCase());
998
+ if (!team) {
999
+ throw new VcError(`Team "${teamSlug}" not found. Check the URL and your access permissions.`, VcErrorCode.INVALID_REMOTE_URL);
1000
+ }
1001
+ const { spaces } = await this.spaceService.getSpaces(token.sessionKey, team.id, { fetchAll: true });
1002
+ const space = spaces.find((s) => s.slug.toLowerCase() === spaceSlug.toLowerCase());
1003
+ if (!space) {
1004
+ throw new VcError(`Space "${spaceSlug}" not found in team "${team.name}". Check the URL and your access permissions.`, VcErrorCode.INVALID_REMOTE_URL);
1005
+ }
1006
+ return {
1007
+ spaceId: space.id,
1008
+ spaceName: space.name,
1009
+ spaceSlug: space.slug,
1010
+ teamId: space.teamId,
1011
+ teamName: team.name,
1012
+ teamSlug: team.slug,
1013
+ url: buildCogitRemoteUrl(this.gitRemoteBaseUrl, team.slug, space.slug),
1014
+ };
1015
+ }
1016
+ validateBranchName(branch) {
1017
+ if (!branch) {
1018
+ throw new VcError('Branch name is required.', VcErrorCode.INVALID_BRANCH_NAME);
1019
+ }
1020
+ if (!isValidBranchName(branch)) {
1021
+ throw new VcError(`Invalid branch name: '${branch}'.`, VcErrorCode.INVALID_BRANCH_NAME);
1022
+ }
1023
+ }
1024
+ validateRemoteUrlDomain(url) {
1025
+ try {
1026
+ const parsed = new URL(url);
1027
+ const allowedHosts = [this.gitRemoteBaseUrl].map((u) => new URL(u).host);
1028
+ if (!allowedHosts.includes(parsed.host)) {
1029
+ throw new VcError(`Invalid remote URL. Use: ${this.gitRemoteBaseUrl}/<team>/<space>.git`, VcErrorCode.INVALID_REMOTE_URL);
1030
+ }
1031
+ }
1032
+ catch (error) {
1033
+ if (error instanceof VcError)
1034
+ throw error;
1035
+ throw new VcError(`Invalid remote URL. Use: ${this.gitRemoteBaseUrl}/<team>/<space>.git`, VcErrorCode.INVALID_REMOTE_URL);
1036
+ }
1037
+ }
1038
+ }
1039
+ function maskCredentialsInUrl(url) {
1040
+ try {
1041
+ const parsed = new URL(url);
1042
+ if (parsed.password) {
1043
+ parsed.password = '***';
1044
+ }
1045
+ return parsed.toString();
1046
+ }
1047
+ catch {
1048
+ return url;
1049
+ }
1050
+ }