devchain-cli 0.9.1 → 0.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/dist/cli.js +985 -194
  2. package/dist/drizzle/0042_nice_flatman.sql +10 -0
  3. package/dist/drizzle/0043_majestic_starhawk.sql +10 -0
  4. package/dist/drizzle/0044_supreme_joshua_kane.sql +57 -0
  5. package/dist/drizzle/0045_provider_auto_compact_threshold.sql +11 -0
  6. package/dist/drizzle/0046_worktrees_owner_project_id.sql +3 -0
  7. package/dist/drizzle/meta/0042_snapshot.json +4171 -0
  8. package/dist/drizzle/meta/0043_snapshot.json +4231 -0
  9. package/dist/drizzle/meta/0044_snapshot.json +4620 -0
  10. package/dist/drizzle/meta/_journal.json +35 -0
  11. package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts +18 -0
  12. package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts.map +1 -1
  13. package/dist/node_modules/@devchain/shared/schemas/export-schema.js +6 -0
  14. package/dist/node_modules/@devchain/shared/schemas/export-schema.js.map +1 -1
  15. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  16. package/dist/server/app.main.module.d.ts +4 -0
  17. package/dist/server/app.main.module.js +102 -0
  18. package/dist/server/app.main.module.js.map +1 -0
  19. package/dist/server/app.module.d.ts +1 -4
  20. package/dist/server/app.module.js +2 -78
  21. package/dist/server/app.module.js.map +1 -1
  22. package/dist/server/app.normal.module.d.ts +4 -0
  23. package/dist/server/app.normal.module.js +90 -0
  24. package/dist/server/app.normal.module.js.map +1 -0
  25. package/dist/server/common/config/env.config.d.ts +81 -1
  26. package/dist/server/common/config/env.config.js +52 -2
  27. package/dist/server/common/config/env.config.js.map +1 -1
  28. package/dist/server/common/templates-directory.d.ts +8 -0
  29. package/dist/server/common/templates-directory.js +56 -0
  30. package/dist/server/common/templates-directory.js.map +1 -0
  31. package/dist/server/main.js +58 -7
  32. package/dist/server/main.js.map +1 -1
  33. package/dist/server/modules/chat/dtos/chat.dto.d.ts +2 -2
  34. package/dist/server/modules/core/controllers/health.controller.d.ts +4 -0
  35. package/dist/server/modules/core/controllers/health.controller.js +22 -1
  36. package/dist/server/modules/core/controllers/health.controller.js.map +1 -1
  37. package/dist/server/modules/core/controllers/runtime.controller.d.ts +18 -0
  38. package/dist/server/modules/core/controllers/runtime.controller.js +130 -0
  39. package/dist/server/modules/core/controllers/runtime.controller.js.map +1 -0
  40. package/dist/server/modules/core/core-common.module.d.ts +2 -0
  41. package/dist/server/modules/core/core-common.module.js +24 -0
  42. package/dist/server/modules/core/core-common.module.js.map +1 -0
  43. package/dist/server/modules/core/core-main-health.module.d.ts +2 -0
  44. package/dist/server/modules/core/core-main-health.module.js +32 -0
  45. package/dist/server/modules/core/core-main-health.module.js.map +1 -0
  46. package/dist/server/modules/core/core-normal-health.module.d.ts +2 -0
  47. package/dist/server/modules/core/core-normal-health.module.js +29 -0
  48. package/dist/server/modules/core/core-normal-health.module.js.map +1 -0
  49. package/dist/server/modules/core/core-normal.module.d.ts +2 -0
  50. package/dist/server/modules/core/core-normal.module.js +28 -0
  51. package/dist/server/modules/core/core-normal.module.js.map +1 -0
  52. package/dist/server/modules/core/core.module.js +4 -11
  53. package/dist/server/modules/core/core.module.js.map +1 -1
  54. package/dist/server/modules/core/services/health.service.d.ts +13 -0
  55. package/dist/server/modules/core/services/health.service.js +39 -0
  56. package/dist/server/modules/core/services/health.service.js.map +1 -0
  57. package/dist/server/modules/core/services/main-readiness-checker.service.d.ts +14 -0
  58. package/dist/server/modules/core/services/main-readiness-checker.service.js +82 -0
  59. package/dist/server/modules/core/services/main-readiness-checker.service.js.map +1 -0
  60. package/dist/server/modules/core/services/normal-readiness-checker.service.d.ts +13 -0
  61. package/dist/server/modules/core/services/normal-readiness-checker.service.js +67 -0
  62. package/dist/server/modules/core/services/normal-readiness-checker.service.js.map +1 -0
  63. package/dist/server/modules/core/services/preflight.service.d.ts +1 -0
  64. package/dist/server/modules/core/services/preflight.service.js +18 -1
  65. package/dist/server/modules/core/services/preflight.service.js.map +1 -1
  66. package/dist/server/modules/core/services/provider-mcp-ensure.service.js +8 -0
  67. package/dist/server/modules/core/services/provider-mcp-ensure.service.js.map +1 -1
  68. package/dist/server/modules/epics/epics.module.js +2 -2
  69. package/dist/server/modules/epics/epics.module.js.map +1 -1
  70. package/dist/server/modules/events/catalog/claude.hooks.session.started.d.ts +39 -0
  71. package/dist/server/modules/events/catalog/claude.hooks.session.started.js +20 -0
  72. package/dist/server/modules/events/catalog/claude.hooks.session.started.js.map +1 -0
  73. package/dist/server/modules/events/catalog/epic.created.d.ts +2 -2
  74. package/dist/server/modules/events/catalog/index.d.ts +38 -4
  75. package/dist/server/modules/events/catalog/index.js +2 -0
  76. package/dist/server/modules/events/catalog/index.js.map +1 -1
  77. package/dist/server/modules/events/catalog/terminal.watcher.triggered.d.ts +2 -2
  78. package/dist/server/modules/events/controllers/event-log.controller.d.ts +1 -1
  79. package/dist/server/modules/events/controllers/event-log.controller.js +11 -9
  80. package/dist/server/modules/events/controllers/event-log.controller.js.map +1 -1
  81. package/dist/server/modules/events/dtos/event-log.dto.d.ts +1 -0
  82. package/dist/server/modules/events/events-domain.module.d.ts +2 -0
  83. package/dist/server/modules/events/events-domain.module.js +42 -0
  84. package/dist/server/modules/events/events-domain.module.js.map +1 -0
  85. package/dist/server/modules/events/events-infra.module.d.ts +2 -0
  86. package/dist/server/modules/events/events-infra.module.js +26 -0
  87. package/dist/server/modules/events/events-infra.module.js.map +1 -0
  88. package/dist/server/modules/events/events.module.js +4 -27
  89. package/dist/server/modules/events/events.module.js.map +1 -1
  90. package/dist/server/modules/events/index.d.ts +2 -0
  91. package/dist/server/modules/events/index.js +2 -0
  92. package/dist/server/modules/events/index.js.map +1 -1
  93. package/dist/server/modules/events/services/event-log.service.d.ts +8 -1
  94. package/dist/server/modules/events/services/event-log.service.js +41 -0
  95. package/dist/server/modules/events/services/event-log.service.js.map +1 -1
  96. package/dist/server/modules/events/subscribers/index.js +2 -0
  97. package/dist/server/modules/events/subscribers/index.js.map +1 -1
  98. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.d.ts +8 -0
  99. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js +48 -0
  100. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js.map +1 -0
  101. package/dist/server/modules/git/dtos/git.dto.d.ts +1 -1
  102. package/dist/server/modules/guests/guests.module.js +2 -2
  103. package/dist/server/modules/guests/guests.module.js.map +1 -1
  104. package/dist/server/modules/hooks/controllers/hooks.controller.d.ts +7 -0
  105. package/dist/server/modules/hooks/controllers/hooks.controller.js +49 -0
  106. package/dist/server/modules/hooks/controllers/hooks.controller.js.map +1 -0
  107. package/dist/server/modules/hooks/dtos/hook-event.dto.d.ts +41 -0
  108. package/dist/server/modules/hooks/dtos/hook-event.dto.js +19 -0
  109. package/dist/server/modules/hooks/dtos/hook-event.dto.js.map +1 -0
  110. package/dist/server/modules/hooks/hooks.module.d.ts +2 -0
  111. package/dist/server/modules/hooks/hooks.module.js +27 -0
  112. package/dist/server/modules/hooks/hooks.module.js.map +1 -0
  113. package/dist/server/modules/hooks/services/hooks-config.service.d.ts +5 -0
  114. package/dist/server/modules/hooks/services/hooks-config.service.js +215 -0
  115. package/dist/server/modules/hooks/services/hooks-config.service.js.map +1 -0
  116. package/dist/server/modules/hooks/services/hooks.service.d.ts +11 -0
  117. package/dist/server/modules/hooks/services/hooks.service.js +83 -0
  118. package/dist/server/modules/hooks/services/hooks.service.js.map +1 -0
  119. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +14 -14
  120. package/dist/server/modules/mcp/mcp.module.js +2 -2
  121. package/dist/server/modules/mcp/mcp.module.js.map +1 -1
  122. package/dist/server/modules/orchestrator/docker/docker.module.d.ts +2 -0
  123. package/dist/server/modules/orchestrator/docker/docker.module.js +22 -0
  124. package/dist/server/modules/orchestrator/docker/docker.module.js.map +1 -0
  125. package/dist/server/modules/orchestrator/docker/index.d.ts +3 -0
  126. package/dist/server/modules/orchestrator/docker/index.js +20 -0
  127. package/dist/server/modules/orchestrator/docker/index.js.map +1 -0
  128. package/dist/server/modules/orchestrator/docker/services/docker.service.d.ts +85 -0
  129. package/dist/server/modules/orchestrator/docker/services/docker.service.js +745 -0
  130. package/dist/server/modules/orchestrator/docker/services/docker.service.js.map +1 -0
  131. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.d.ts +11 -0
  132. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js +181 -0
  133. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js.map +1 -0
  134. package/dist/server/modules/orchestrator/git/controllers/git.controller.d.ts +8 -0
  135. package/dist/server/modules/orchestrator/git/controllers/git.controller.js +38 -0
  136. package/dist/server/modules/orchestrator/git/controllers/git.controller.js.map +1 -0
  137. package/dist/server/modules/orchestrator/git/git.module.d.ts +2 -0
  138. package/dist/server/modules/orchestrator/git/git.module.js +23 -0
  139. package/dist/server/modules/orchestrator/git/git.module.js.map +1 -0
  140. package/dist/server/modules/orchestrator/git/index.d.ts +3 -0
  141. package/dist/server/modules/orchestrator/git/index.js +20 -0
  142. package/dist/server/modules/orchestrator/git/index.js.map +1 -0
  143. package/dist/server/modules/orchestrator/git/services/git-worktree.service.d.ts +83 -0
  144. package/dist/server/modules/orchestrator/git/services/git-worktree.service.js +474 -0
  145. package/dist/server/modules/orchestrator/git/services/git-worktree.service.js.map +1 -0
  146. package/dist/server/modules/orchestrator/index.d.ts +6 -0
  147. package/dist/server/modules/orchestrator/index.js +23 -0
  148. package/dist/server/modules/orchestrator/index.js.map +1 -0
  149. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.d.ts +1 -0
  150. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js +18 -0
  151. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js.map +1 -0
  152. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.d.ts +5 -0
  153. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js +10 -0
  154. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js.map +1 -0
  155. package/dist/server/modules/orchestrator/orchestrator-storage/index.d.ts +2 -0
  156. package/dist/server/modules/orchestrator/orchestrator-storage/index.js +19 -0
  157. package/dist/server/modules/orchestrator/orchestrator-storage/index.js.map +1 -0
  158. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.d.ts +2 -0
  159. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js +23 -0
  160. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js.map +1 -0
  161. package/dist/server/modules/orchestrator/proxy/index.d.ts +2 -0
  162. package/dist/server/modules/orchestrator/proxy/index.js +19 -0
  163. package/dist/server/modules/orchestrator/proxy/index.js.map +1 -0
  164. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.d.ts +2 -0
  165. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js +22 -0
  166. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js.map +1 -0
  167. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.d.ts +18 -0
  168. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js +192 -0
  169. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js.map +1 -0
  170. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.d.ts +9 -0
  171. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js +66 -0
  172. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js.map +1 -0
  173. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.d.ts +52 -0
  174. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js +3 -0
  175. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js.map +1 -0
  176. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.d.ts +5 -0
  177. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js +3 -0
  178. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js.map +1 -0
  179. package/dist/server/modules/orchestrator/sync/events/task-merge.events.d.ts +4 -0
  180. package/dist/server/modules/orchestrator/sync/events/task-merge.events.js +5 -0
  181. package/dist/server/modules/orchestrator/sync/events/task-merge.events.js.map +1 -0
  182. package/dist/server/modules/orchestrator/sync/index.d.ts +7 -0
  183. package/dist/server/modules/orchestrator/sync/index.js +24 -0
  184. package/dist/server/modules/orchestrator/sync/index.js.map +1 -0
  185. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.d.ts +31 -0
  186. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js +410 -0
  187. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js.map +1 -0
  188. package/dist/server/modules/orchestrator/sync/services/task-merge.service.d.ts +44 -0
  189. package/dist/server/modules/orchestrator/sync/services/task-merge.service.js +730 -0
  190. package/dist/server/modules/orchestrator/sync/services/task-merge.service.js.map +1 -0
  191. package/dist/server/modules/orchestrator/sync/sync.module.d.ts +2 -0
  192. package/dist/server/modules/orchestrator/sync/sync.module.js +36 -0
  193. package/dist/server/modules/orchestrator/sync/sync.module.js.map +1 -0
  194. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.d.ts +118 -0
  195. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js +297 -0
  196. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js.map +1 -0
  197. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.d.ts +17 -0
  198. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js +752 -0
  199. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js.map +1 -0
  200. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.d.ts +15 -0
  201. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js +85 -0
  202. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js.map +1 -0
  203. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.d.ts +29 -0
  204. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js +272 -0
  205. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js.map +1 -0
  206. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.d.ts +109 -0
  207. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js +57 -0
  208. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js.map +1 -0
  209. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.d.ts +4 -0
  210. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js +5 -0
  211. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js.map +1 -0
  212. package/dist/server/modules/orchestrator/worktrees/index.d.ts +7 -0
  213. package/dist/server/modules/orchestrator/worktrees/index.js +24 -0
  214. package/dist/server/modules/orchestrator/worktrees/index.js.map +1 -0
  215. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.d.ts +18 -0
  216. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js +198 -0
  217. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js.map +1 -0
  218. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.d.ts +87 -0
  219. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js +1380 -0
  220. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js.map +1 -0
  221. package/dist/server/modules/orchestrator/worktrees/worktree-validation.d.ts +4 -0
  222. package/dist/server/modules/orchestrator/worktrees/worktree-validation.js +43 -0
  223. package/dist/server/modules/orchestrator/worktrees/worktree-validation.js.map +1 -0
  224. package/dist/server/modules/orchestrator/worktrees/worktrees.module.d.ts +2 -0
  225. package/dist/server/modules/orchestrator/worktrees/worktrees.module.js +44 -0
  226. package/dist/server/modules/orchestrator/worktrees/worktrees.module.js.map +1 -0
  227. package/dist/server/modules/orchestrator/worktrees/worktrees.store.d.ts +38 -0
  228. package/dist/server/modules/orchestrator/worktrees/worktrees.store.js +5 -0
  229. package/dist/server/modules/orchestrator/worktrees/worktrees.store.js.map +1 -0
  230. package/dist/server/modules/profiles/dto.d.ts +4 -4
  231. package/dist/server/modules/projects/controllers/projects.controller.d.ts +13 -3
  232. package/dist/server/modules/projects/controllers/projects.controller.js +55 -7
  233. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  234. package/dist/server/modules/projects/projects.module.js +3 -2
  235. package/dist/server/modules/projects/projects.module.js.map +1 -1
  236. package/dist/server/modules/projects/services/main-project-bootstrap.service.d.ts +11 -0
  237. package/dist/server/modules/projects/services/main-project-bootstrap.service.js +76 -0
  238. package/dist/server/modules/projects/services/main-project-bootstrap.service.js.map +1 -0
  239. package/dist/server/modules/projects/services/projects.service.d.ts +8 -0
  240. package/dist/server/modules/projects/services/projects.service.js +115 -37
  241. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  242. package/dist/server/modules/providers/adapters/gemini.adapter.d.ts +1 -1
  243. package/dist/server/modules/providers/adapters/gemini.adapter.js +6 -4
  244. package/dist/server/modules/providers/adapters/gemini.adapter.js.map +1 -1
  245. package/dist/server/modules/providers/controllers/providers.controller.d.ts +3 -0
  246. package/dist/server/modules/providers/controllers/providers.controller.js +28 -0
  247. package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
  248. package/dist/server/modules/providers/providers.module.js +2 -2
  249. package/dist/server/modules/providers/providers.module.js.map +1 -1
  250. package/dist/server/modules/registry/services/unified-template.service.js +6 -18
  251. package/dist/server/modules/registry/services/unified-template.service.js.map +1 -1
  252. package/dist/server/modules/reviews/dtos/review.dto.d.ts +4 -4
  253. package/dist/server/modules/reviews/reviews.module.js +2 -2
  254. package/dist/server/modules/reviews/reviews.module.js.map +1 -1
  255. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.d.ts +3 -0
  256. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js +89 -0
  257. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js.map +1 -0
  258. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.d.ts +3 -0
  259. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js +60 -0
  260. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js.map +1 -0
  261. package/dist/server/modules/seeders/services/data-seeder.service.js +4 -0
  262. package/dist/server/modules/seeders/services/data-seeder.service.js.map +1 -1
  263. package/dist/server/modules/sessions/services/sessions.service.d.ts +3 -1
  264. package/dist/server/modules/sessions/services/sessions.service.js +79 -22
  265. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  266. package/dist/server/modules/sessions/sessions.module.js +6 -4
  267. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  268. package/dist/server/modules/sessions/utils/claude-config.d.ts +6 -2
  269. package/dist/server/modules/sessions/utils/claude-config.js +19 -6
  270. package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
  271. package/dist/server/modules/settings/dtos/settings.dto.d.ts +4 -4
  272. package/dist/server/modules/skills/adapters/github-skill-source.base.d.ts +2 -5
  273. package/dist/server/modules/skills/adapters/github-skill-source.base.js +31 -100
  274. package/dist/server/modules/skills/adapters/github-skill-source.base.js.map +1 -1
  275. package/dist/server/modules/skills/adapters/local-skill-source.adapter.d.ts +16 -0
  276. package/dist/server/modules/skills/adapters/local-skill-source.adapter.js +265 -0
  277. package/dist/server/modules/skills/adapters/local-skill-source.adapter.js.map +1 -0
  278. package/dist/server/modules/skills/adapters/skill-parsing.utils.d.ts +32 -0
  279. package/dist/server/modules/skills/adapters/skill-parsing.utils.js +169 -0
  280. package/dist/server/modules/skills/adapters/skill-parsing.utils.js.map +1 -0
  281. package/dist/server/modules/skills/controllers/local-sources.controller.d.ts +12 -0
  282. package/dist/server/modules/skills/controllers/local-sources.controller.js +71 -0
  283. package/dist/server/modules/skills/controllers/local-sources.controller.js.map +1 -0
  284. package/dist/server/modules/skills/controllers/skills.controller.d.ts +11 -1
  285. package/dist/server/modules/skills/controllers/skills.controller.js +35 -3
  286. package/dist/server/modules/skills/controllers/skills.controller.js.map +1 -1
  287. package/dist/server/modules/skills/dtos/community-sources.dto.d.ts +2 -2
  288. package/dist/server/modules/skills/dtos/local-sources.dto.d.ts +42 -0
  289. package/dist/server/modules/skills/dtos/local-sources.dto.js +30 -0
  290. package/dist/server/modules/skills/dtos/local-sources.dto.js.map +1 -0
  291. package/dist/server/modules/skills/dtos/skill.dto.d.ts +23 -7
  292. package/dist/server/modules/skills/dtos/skill.dto.js +7 -1
  293. package/dist/server/modules/skills/dtos/skill.dto.js.map +1 -1
  294. package/dist/server/modules/skills/services/community-sources.service.d.ts +8 -1
  295. package/dist/server/modules/skills/services/community-sources.service.js +62 -3
  296. package/dist/server/modules/skills/services/community-sources.service.js.map +1 -1
  297. package/dist/server/modules/skills/services/local-sources.service.d.ts +20 -0
  298. package/dist/server/modules/skills/services/local-sources.service.js +206 -0
  299. package/dist/server/modules/skills/services/local-sources.service.js.map +1 -0
  300. package/dist/server/modules/skills/services/skill-source-registry.service.d.ts +11 -0
  301. package/dist/server/modules/skills/services/skill-source-registry.service.js +99 -3
  302. package/dist/server/modules/skills/services/skill-source-registry.service.js.map +1 -1
  303. package/dist/server/modules/skills/services/skill-sync.service.d.ts +4 -0
  304. package/dist/server/modules/skills/services/skill-sync.service.js +93 -0
  305. package/dist/server/modules/skills/services/skill-sync.service.js.map +1 -1
  306. package/dist/server/modules/skills/services/skills.service.d.ts +14 -2
  307. package/dist/server/modules/skills/services/skills.service.js +125 -23
  308. package/dist/server/modules/skills/services/skills.service.js.map +1 -1
  309. package/dist/server/modules/skills/skills.module.js +4 -1
  310. package/dist/server/modules/skills/skills.module.js.map +1 -1
  311. package/dist/server/modules/storage/db/schema.d.ts +1115 -102
  312. package/dist/server/modules/storage/db/schema.js +81 -1
  313. package/dist/server/modules/storage/db/schema.js.map +1 -1
  314. package/dist/server/modules/storage/db/sqlite-json.d.ts +2 -0
  315. package/dist/server/modules/storage/db/sqlite-json.js +8 -0
  316. package/dist/server/modules/storage/db/sqlite-json.js.map +1 -0
  317. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +17 -2
  318. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  319. package/dist/server/modules/storage/local/local-storage.service.d.ts +19 -2
  320. package/dist/server/modules/storage/local/local-storage.service.js +313 -5
  321. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  322. package/dist/server/modules/storage/models/domain.models.d.ts +10 -0
  323. package/dist/server/modules/subscribers/dtos/subscriber.dto.d.ts +16 -16
  324. package/dist/server/modules/subscribers/events/event-fields-catalog.js +18 -0
  325. package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
  326. package/dist/server/modules/subscribers/subscribers.module.js +2 -2
  327. package/dist/server/modules/subscribers/subscribers.module.js.map +1 -1
  328. package/dist/server/modules/terminal/gateways/terminal.gateway.js +7 -2
  329. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  330. package/dist/server/modules/terminal/services/tmux.service.d.ts +9 -0
  331. package/dist/server/modules/terminal/services/tmux.service.js +55 -5
  332. package/dist/server/modules/terminal/services/tmux.service.js.map +1 -1
  333. package/dist/server/modules/terminal/terminal.module.js +2 -2
  334. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  335. package/dist/server/modules/watchers/watchers.module.js +2 -2
  336. package/dist/server/modules/watchers/watchers.module.js.map +1 -1
  337. package/dist/server/templates/3-agents-dev.json +662 -0
  338. package/dist/server/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
  339. package/dist/server/test-setup.js +7 -0
  340. package/dist/server/test-setup.js.map +1 -1
  341. package/dist/server/tsconfig.tsbuildinfo +1 -1
  342. package/dist/server/ui/assets/ReviewDetailPage-CZZQtaY7.js +1 -0
  343. package/dist/server/ui/assets/{ReviewsPage-C98ST0lf.js → ReviewsPage-C209GLQG.js} +1 -1
  344. package/dist/server/ui/assets/index-DvRuLfpZ.css +32 -0
  345. package/dist/server/ui/assets/index-Th1FDtKR.js +977 -0
  346. package/dist/server/ui/assets/{useReviewSubscription-CmLuF45Z.js → useReviewSubscription-Dcabsa78.js} +1 -1
  347. package/dist/server/ui/index.html +2 -2
  348. package/dist/templates/3-agents-dev.json +662 -0
  349. package/dist/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
  350. package/package.json +19 -1
  351. package/dist/server/ui/assets/ReviewDetailPage-D13dH7Wh.js +0 -6
  352. package/dist/server/ui/assets/index-C8Dc1yQf.js +0 -945
  353. package/dist/server/ui/assets/index-DZkJ40z9.css +0 -32
@@ -0,0 +1,1380 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.WorktreesService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const event_emitter_1 = require("@nestjs/event-emitter");
18
+ const child_process_1 = require("child_process");
19
+ const crypto_1 = require("crypto");
20
+ const fs_1 = require("fs");
21
+ const logger_1 = require("../../../../common/logging/logger");
22
+ const env_config_1 = require("../../../../common/config/env.config");
23
+ const event_log_service_1 = require("../../../events/services/event-log.service");
24
+ const worktree_dto_1 = require("../dtos/worktree.dto");
25
+ const worktrees_store_1 = require("../worktrees.store");
26
+ const git_worktree_service_1 = require("../../git/services/git-worktree.service");
27
+ const docker_service_1 = require("../../docker/services/docker.service");
28
+ const seed_preparation_service_1 = require("../../docker/services/seed-preparation.service");
29
+ const task_merge_events_1 = require("../../sync/events/task-merge.events");
30
+ const worktree_events_1 = require("../events/worktree.events");
31
+ const promises_1 = require("fs/promises");
32
+ const path_1 = require("path");
33
+ const worktree_validation_1 = require("../worktree-validation");
34
+ const logger = (0, logger_1.createLogger)('OrchestratorWorktreesService');
35
+ const CONTAINER_HEALTH_TIMEOUT_MS = 60_000;
36
+ const HEALTH_MONITOR_INTERVAL_MS = 15_000;
37
+ const HEALTH_MONITOR_PROBE_TIMEOUT_MS = 1_500;
38
+ const MAX_CONSECUTIVE_HEALTH_FAILURES = 3;
39
+ const OVERVIEW_FETCH_TIMEOUT_MS = 2_500;
40
+ const PROCESS_SHUTDOWN_TIMEOUT_MS = 30_000;
41
+ const PROCESS_KILL_TIMEOUT_MS = 5_000;
42
+ const PROCESS_LOG_FILE_NAME = 'devchain.log';
43
+ const PROCESS_DB_FILE_NAME = 'devchain.db';
44
+ const PROCESS_RUNTIME_PORT_FILE = 'runtime-port.json';
45
+ const PROCESS_HEALTH_POLL_INTERVAL_MS = 1_000;
46
+ const PROCESS_RUNTIME_TIMEOUT_MS = 1_500;
47
+ const WORKTREE_ACTIVITY_EVENT_NAME = 'orchestrator.worktree.activity';
48
+ const WORKTREE_STATUS_VALUES = worktree_dto_1.WorktreeStatusSchema.options;
49
+ let WorktreesService = class WorktreesService {
50
+ constructor(store, dockerService, gitService, seedPreparationService, eventEmitter, eventLogService) {
51
+ this.store = store;
52
+ this.dockerService = dockerService;
53
+ this.gitService = gitService;
54
+ this.seedPreparationService = seedPreparationService;
55
+ this.eventEmitter = eventEmitter;
56
+ this.eventLogService = eventLogService;
57
+ this.consecutiveHealthFailures = new Map();
58
+ }
59
+ async onModuleInit() {
60
+ this.monitorTimer = setInterval(() => {
61
+ this.monitorRunningWorktrees().catch((error) => {
62
+ logger.error({ error }, 'Failed to monitor running worktrees');
63
+ });
64
+ }, HEALTH_MONITOR_INTERVAL_MS);
65
+ this.reconcileProcessOrphans().catch((error) => {
66
+ logger.warn({ error }, 'Failed process-runtime orphan detection on startup');
67
+ });
68
+ try {
69
+ this.unsubscribeDockerEvents = await this.dockerService.subscribeToContainerEvents((event) => {
70
+ this.handleContainerEvent(event).catch((error) => {
71
+ logger.error({ error, event }, 'Failed handling docker container event');
72
+ });
73
+ });
74
+ }
75
+ catch (error) {
76
+ logger.warn({ error }, 'Failed to subscribe to docker events stream');
77
+ }
78
+ }
79
+ onModuleDestroy() {
80
+ if (this.monitorTimer) {
81
+ clearInterval(this.monitorTimer);
82
+ this.monitorTimer = undefined;
83
+ }
84
+ if (this.unsubscribeDockerEvents) {
85
+ this.unsubscribeDockerEvents();
86
+ this.unsubscribeDockerEvents = undefined;
87
+ }
88
+ }
89
+ async createWorktree(input) {
90
+ this.assertValidWorktreeName(input.name);
91
+ this.assertValidBranchName(input.branchName, 'branchName');
92
+ this.assertValidBranchName(input.baseBranch, 'baseBranch');
93
+ const runtimeType = this.resolveRuntimeType(input.runtimeType);
94
+ const repoPath = this.resolveRepoPath(input.repoPath);
95
+ const existing = await this.store.getByName(input.name);
96
+ if (existing) {
97
+ throw new common_1.ConflictException(`Worktree with name "${input.name}" already exists`);
98
+ }
99
+ const worktreePath = this.resolveWorktreePath(repoPath, input.name);
100
+ const dataPath = this.resolveDataPath(repoPath, input.name);
101
+ const containerName = this.getContainerName(input.name);
102
+ const projectId = (0, crypto_1.randomUUID)();
103
+ let created = await this.store.create({
104
+ name: input.name,
105
+ branchName: input.branchName,
106
+ baseBranch: input.baseBranch,
107
+ repoPath,
108
+ worktreePath,
109
+ templateSlug: input.templateSlug,
110
+ ownerProjectId: input.ownerProjectId,
111
+ status: 'creating',
112
+ description: input.description ?? null,
113
+ runtimeType,
114
+ });
115
+ let containerId = null;
116
+ let processId = null;
117
+ let gitWorktreeCreated = false;
118
+ try {
119
+ await this.gitService.createWorktree({
120
+ name: input.name,
121
+ branchName: input.branchName,
122
+ baseBranch: input.baseBranch,
123
+ repoPath,
124
+ worktreePath,
125
+ });
126
+ gitWorktreeCreated = true;
127
+ await (0, promises_1.mkdir)(dataPath, { recursive: true });
128
+ await this.seedPreparationService.prepareSeedData(dataPath);
129
+ if (runtimeType === 'process') {
130
+ const runtime = await this.startProcessRuntime({
131
+ worktreePath,
132
+ dataPath,
133
+ projectId,
134
+ });
135
+ processId = runtime.processId;
136
+ const project = await this.registerProjectInContainer(runtime.hostPort, {
137
+ name: input.name,
138
+ templateSlug: input.templateSlug,
139
+ description: input.description ?? null,
140
+ projectId,
141
+ rootPath: worktreePath,
142
+ presetName: input.presetName,
143
+ });
144
+ created = (await this.store.update(created.id, {
145
+ containerId: null,
146
+ processId: runtime.processId,
147
+ runtimeToken: runtime.runtimeToken,
148
+ startedAt: runtime.startedAt,
149
+ containerPort: runtime.hostPort,
150
+ devchainProjectId: project.projectId,
151
+ status: 'running',
152
+ errorMessage: null,
153
+ }));
154
+ }
155
+ else {
156
+ const container = await this.dockerService.createContainer({
157
+ name: containerName,
158
+ worktreePath,
159
+ dataPath,
160
+ worktreeName: input.name,
161
+ env: {
162
+ CONTAINER_PROJECT_ID: projectId,
163
+ },
164
+ });
165
+ containerId = container.id;
166
+ await this.dockerService
167
+ .ensureWorktreeOnComposeNetwork(input.name, container.id)
168
+ .catch(() => undefined);
169
+ const healthy = await this.dockerService.waitForHealthy(container.id, CONTAINER_HEALTH_TIMEOUT_MS);
170
+ if (!healthy) {
171
+ throw new Error('Container did not become healthy before timeout');
172
+ }
173
+ const project = await this.registerProjectInContainer(container.hostPort, {
174
+ name: input.name,
175
+ templateSlug: input.templateSlug,
176
+ description: input.description ?? null,
177
+ projectId,
178
+ rootPath: '/project',
179
+ presetName: input.presetName,
180
+ });
181
+ created = (await this.store.update(created.id, {
182
+ containerId: container.id,
183
+ containerPort: container.hostPort,
184
+ devchainProjectId: project.projectId,
185
+ status: 'running',
186
+ errorMessage: null,
187
+ }));
188
+ }
189
+ this.consecutiveHealthFailures.set(created.id, 0);
190
+ this.eventEmitter.emit(worktree_events_1.WORKTREE_CHANGED_EVENT, {
191
+ worktreeId: created.id,
192
+ });
193
+ this.recordWorktreeActivity({
194
+ worktreeId: created.id,
195
+ worktreeName: created.name,
196
+ ownerProjectId: created.ownerProjectId,
197
+ type: 'created',
198
+ message: `Worktree '${created.name}' created on branch ${created.branchName}`,
199
+ });
200
+ return this.toResponse(created);
201
+ }
202
+ catch (error) {
203
+ const errorMessage = error instanceof Error ? error.message : String(error);
204
+ await this.tryUpdateStatus(created.id, 'error', {
205
+ errorMessage,
206
+ });
207
+ if (containerId) {
208
+ await this.dockerService.removeContainer(containerId, true).catch(() => undefined);
209
+ }
210
+ if (processId) {
211
+ await this.terminateProcess(processId).catch(() => undefined);
212
+ }
213
+ if (gitWorktreeCreated) {
214
+ await this.gitService.removeWorktree(worktreePath, repoPath, true).catch(() => undefined);
215
+ if (created.branchName !== created.baseBranch) {
216
+ await this.gitService
217
+ .deleteBranch(created.branchName, repoPath, true)
218
+ .catch((cleanupError) => logger.warn({
219
+ error: cleanupError,
220
+ worktreeId: created.id,
221
+ branchName: created.branchName,
222
+ }, 'Failed to clean up branch after create-worktree error'));
223
+ }
224
+ }
225
+ const logPath = this.resolveProcessLogPath(dataPath);
226
+ try {
227
+ const logContent = await (0, promises_1.readFile)(logPath, 'utf-8');
228
+ if (logContent.trim()) {
229
+ logger.error({ worktreeId: created.id, logContent: logContent.slice(-2000) }, 'Process runtime log before cleanup');
230
+ }
231
+ }
232
+ catch {
233
+ }
234
+ await (0, promises_1.rm)(dataPath, { recursive: true, force: true }).catch(() => undefined);
235
+ throw new common_1.BadRequestException(`Failed to create worktree: ${errorMessage}`);
236
+ }
237
+ }
238
+ async listWorktrees() {
239
+ const rows = await this.store.list();
240
+ return Promise.all(rows.map((row) => this.toResponse(row)));
241
+ }
242
+ async listByOwnerProject(ownerProjectId) {
243
+ const rows = await this.store.listByOwnerProject(ownerProjectId);
244
+ return Promise.all(rows.map((row) => this.toResponse(row)));
245
+ }
246
+ async getWorktree(id) {
247
+ const row = await this.store.getById(id);
248
+ if (!row) {
249
+ throw new common_1.NotFoundException(`Worktree not found: ${id}`);
250
+ }
251
+ return this.toResponse(row);
252
+ }
253
+ async listWorktreeOverviews(ownerProjectId) {
254
+ const rows = ownerProjectId
255
+ ? await this.store.listByOwnerProject(ownerProjectId)
256
+ : await this.store.list();
257
+ return Promise.all(rows.map((row) => this.buildWorktreeOverview(row)));
258
+ }
259
+ async getWorktreeOverview(id) {
260
+ const row = await this.requireWorktree(id);
261
+ return this.buildWorktreeOverview(row);
262
+ }
263
+ async deleteWorktree(id, options = {}) {
264
+ const row = await this.requireWorktree(id);
265
+ const runtimeType = this.resolveRuntimeType(row.runtimeType);
266
+ const shouldDeleteBranch = options.deleteBranch ?? true;
267
+ const repoPath = this.resolveRepoPath(row.repoPath);
268
+ const worktreeRoot = this.resolveWorktreeRoot(repoPath);
269
+ const worktreePath = row.worktreePath
270
+ ? this.ensurePathWithinRoot(worktreeRoot, row.worktreePath, 'worktree path')
271
+ : null;
272
+ const dataPath = this.resolveDataPath(repoPath, row.name);
273
+ if (runtimeType === 'container' || row.containerId) {
274
+ await this.dockerService
275
+ .cleanupWorktreeProjectContainers(row.name, row.containerId)
276
+ .catch((error) => logger.warn({ error, worktreeId: row.id }, 'Failed cleaning project sub-containers'));
277
+ if (row.containerId) {
278
+ await this.dockerService.stopContainer(row.containerId).catch(() => undefined);
279
+ await this.dockerService.removeContainer(row.containerId, true).catch(() => undefined);
280
+ }
281
+ await this.dockerService
282
+ .removeWorktreeNetwork(row.name)
283
+ .catch((error) => logger.warn({ error, worktreeId: row.id }, 'Failed removing worktree docker network'));
284
+ }
285
+ else {
286
+ await this.terminateProcess(row.processId).catch((error) => logger.warn({ error, worktreeId: row.id }, 'Failed stopping worktree process during delete'));
287
+ }
288
+ if (worktreePath) {
289
+ await this.gitService.removeWorktree(worktreePath, repoPath, true).catch(() => undefined);
290
+ }
291
+ if (shouldDeleteBranch && row.branchName !== row.baseBranch) {
292
+ await this.gitService.deleteBranch(row.branchName, repoPath, true).catch((error) => logger.warn({
293
+ error,
294
+ worktreeId: row.id,
295
+ branchName: row.branchName,
296
+ }, 'Failed deleting branch during worktree cleanup'));
297
+ }
298
+ await (0, promises_1.rm)(dataPath, { recursive: true, force: true }).catch(() => undefined);
299
+ this.consecutiveHealthFailures.delete(row.id);
300
+ await this.store.remove(row.id);
301
+ this.recordWorktreeActivity({
302
+ worktreeId: row.id,
303
+ worktreeName: row.name,
304
+ ownerProjectId: row.ownerProjectId,
305
+ type: 'deleted',
306
+ message: `Worktree '${row.name}' deleted`,
307
+ });
308
+ this.eventEmitter.emit(worktree_events_1.WORKTREE_CHANGED_EVENT, {
309
+ worktreeId: row.id,
310
+ });
311
+ return { success: true };
312
+ }
313
+ async startWorktree(id) {
314
+ const row = await this.requireWorktree(id);
315
+ const runtimeType = this.resolveRuntimeType(row.runtimeType);
316
+ if (runtimeType === 'process') {
317
+ const projectId = row.devchainProjectId?.trim();
318
+ if (!projectId) {
319
+ throw new common_1.BadRequestException('Worktree has no scoped project id to start');
320
+ }
321
+ const repoPath = this.resolveRepoPath(row.repoPath);
322
+ const worktreePath = row.worktreePath ?? this.resolveWorktreePath(repoPath, row.name);
323
+ const dataPath = this.resolveDataPath(repoPath, row.name);
324
+ await (0, promises_1.mkdir)(dataPath, { recursive: true });
325
+ try {
326
+ const runtime = await this.startProcessRuntime({
327
+ worktreePath,
328
+ dataPath,
329
+ projectId,
330
+ });
331
+ const updated = await this.tryUpdateStatus(row.id, 'running', {
332
+ processId: runtime.processId,
333
+ runtimeToken: runtime.runtimeToken,
334
+ startedAt: runtime.startedAt,
335
+ containerPort: runtime.hostPort,
336
+ errorMessage: null,
337
+ });
338
+ return this.toResponse(updated ?? row);
339
+ }
340
+ catch (error) {
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ await this.tryUpdateStatus(row.id, 'error', {
343
+ errorMessage: `Process failed readiness check after start: ${message}`,
344
+ });
345
+ throw new common_1.BadRequestException(`Failed to start process worktree: ${message}`);
346
+ }
347
+ }
348
+ if (!row.containerId) {
349
+ throw new common_1.BadRequestException('Worktree has no container to start');
350
+ }
351
+ await this.dockerService.startContainer(row.containerId);
352
+ const healthy = await this.dockerService.waitForHealthy(row.containerId, CONTAINER_HEALTH_TIMEOUT_MS);
353
+ if (!healthy) {
354
+ await this.tryUpdateStatus(row.id, 'error', {
355
+ errorMessage: 'Container failed readiness check after start',
356
+ });
357
+ throw new common_1.BadRequestException('Container started but failed readiness check');
358
+ }
359
+ await this.dockerService
360
+ .ensureWorktreeOnComposeNetwork(row.name, row.containerId)
361
+ .catch(() => undefined);
362
+ const hostPort = await this.dockerService
363
+ .getContainerHostPort(row.containerId)
364
+ .catch(() => null);
365
+ const updated = await this.tryUpdateStatus(row.id, 'running', {
366
+ errorMessage: null,
367
+ ...(hostPort != null ? { containerPort: hostPort } : {}),
368
+ });
369
+ return this.toResponse(updated ?? row);
370
+ }
371
+ async stopWorktree(id) {
372
+ const row = await this.requireWorktree(id);
373
+ const runtimeType = this.resolveRuntimeType(row.runtimeType);
374
+ if (runtimeType === 'process') {
375
+ await this.terminateProcess(row.processId);
376
+ this.consecutiveHealthFailures.set(row.id, 0);
377
+ const updated = await this.tryUpdateStatus(row.id, 'stopped', {
378
+ processId: null,
379
+ runtimeToken: null,
380
+ startedAt: null,
381
+ containerPort: null,
382
+ errorMessage: null,
383
+ });
384
+ return this.toResponse(updated ?? row);
385
+ }
386
+ if (!row.containerId) {
387
+ throw new common_1.BadRequestException('Worktree has no container to stop');
388
+ }
389
+ await this.dockerService.stopContainer(row.containerId);
390
+ this.consecutiveHealthFailures.set(row.id, 0);
391
+ const updated = await this.tryUpdateStatus(row.id, 'stopped');
392
+ return this.toResponse(updated ?? row);
393
+ }
394
+ async previewMergeWorktree(id) {
395
+ const row = await this.requireWorktree(id);
396
+ const currentStatus = String(row.status).toLowerCase();
397
+ if (currentStatus === 'merged') {
398
+ throw new common_1.BadRequestException('Worktree is already merged');
399
+ }
400
+ const [branchStatus, changeSummary, preview] = await Promise.all([
401
+ this.gitService.getBranchStatus(row.repoPath, row.baseBranch, row.branchName),
402
+ this.gitService.getBranchChangeSummary(row.repoPath, row.baseBranch, row.branchName),
403
+ this.gitService.previewMerge(row.repoPath, row.branchName, row.baseBranch),
404
+ ]);
405
+ const conflicts = this.buildConflictDetails(preview.conflicts.length > 0 ? preview.conflicts : this.extractConflictFiles(preview.output), 'merge');
406
+ if (conflicts.length === 0 && row.mergeConflicts?.trim()) {
407
+ await this.store.update(row.id, {
408
+ mergeConflicts: null,
409
+ });
410
+ }
411
+ return {
412
+ canMerge: !preview.hasConflicts,
413
+ commitsAhead: branchStatus.commitsAhead,
414
+ commitsBehind: branchStatus.commitsBehind,
415
+ filesChanged: changeSummary.filesChanged,
416
+ insertions: changeSummary.insertions,
417
+ deletions: changeSummary.deletions,
418
+ conflicts,
419
+ };
420
+ }
421
+ async mergeWorktree(id) {
422
+ const row = await this.requireWorktree(id);
423
+ const currentStatus = String(row.status).toLowerCase();
424
+ if (currentStatus === 'merged') {
425
+ throw new common_1.BadRequestException('Worktree is already merged');
426
+ }
427
+ if (!['running', 'stopped', 'completed', 'error'].includes(currentStatus)) {
428
+ throw new common_1.BadRequestException(`Cannot merge worktree while status is "${row.status}"`);
429
+ }
430
+ await this.assertCleanWorkingTree(row.worktreePath ?? row.repoPath, 'Merge');
431
+ const extractionRow = currentStatus === 'running' ? row : await this.ensureContainerReadyForTaskExtraction(row);
432
+ await this.extractTasksForMergedHistory(extractionRow);
433
+ if (row.containerId) {
434
+ await this.dockerService.stopContainer(row.containerId).catch(() => undefined);
435
+ }
436
+ const mergeResult = await this.gitService.executeMerge(row.repoPath, row.branchName, row.baseBranch, {
437
+ message: this.buildMergeCommitMessage(row),
438
+ });
439
+ if (!mergeResult.success || !mergeResult.mergeCommit) {
440
+ const message = mergeResult.output.trim() || 'Merge failed';
441
+ const conflictFiles = mergeResult.conflicts && mergeResult.conflicts.length > 0
442
+ ? mergeResult.conflicts
443
+ : this.extractConflictFiles(message);
444
+ const conflictDetails = this.buildConflictDetails(conflictFiles, 'merge');
445
+ const hasConflicts = conflictDetails.length > 0 || /\bconflict\b/i.test(message);
446
+ await this.tryUpdateStatus(row.id, 'error', {
447
+ mergeConflicts: hasConflicts && conflictFiles.length > 0 ? conflictFiles.join('\n') : null,
448
+ errorMessage: message,
449
+ });
450
+ if (hasConflicts) {
451
+ throw new common_1.ConflictException({
452
+ message: 'Merge failed with conflicts',
453
+ conflicts: conflictDetails,
454
+ });
455
+ }
456
+ throw new common_1.BadRequestException(`Merge failed: ${message}`);
457
+ }
458
+ this.consecutiveHealthFailures.set(row.id, 0);
459
+ const mergedMessage = `Worktree '${row.name}' merged into ${row.baseBranch}`;
460
+ const updated = await this.tryUpdateStatus(row.id, 'merged', {
461
+ mergeCommit: mergeResult.mergeCommit,
462
+ mergeConflicts: null,
463
+ errorMessage: null,
464
+ }, { activity: { type: 'merged', message: mergedMessage } });
465
+ return this.toResponse(updated ?? row);
466
+ }
467
+ async rebaseWorktree(id) {
468
+ const row = await this.requireWorktree(id);
469
+ const currentStatus = String(row.status).toLowerCase();
470
+ if (currentStatus === 'merged') {
471
+ throw new common_1.BadRequestException('Cannot rebase a merged worktree');
472
+ }
473
+ if (!['running', 'stopped', 'completed', 'error'].includes(currentStatus)) {
474
+ throw new common_1.BadRequestException(`Cannot rebase worktree while status is "${row.status}"`);
475
+ }
476
+ await this.assertCleanWorkingTree(row.worktreePath ?? row.repoPath, 'Rebase');
477
+ if (row.containerId) {
478
+ await this.dockerService.stopContainer(row.containerId).catch(() => undefined);
479
+ }
480
+ const rebaseResult = await this.gitService.executeRebase(row.worktreePath ?? row.repoPath, row.branchName, row.baseBranch);
481
+ if (!rebaseResult.success) {
482
+ const message = rebaseResult.output.trim() || 'Rebase failed';
483
+ const conflictFiles = rebaseResult.conflicts.length > 0
484
+ ? rebaseResult.conflicts
485
+ : this.extractConflictFiles(rebaseResult.output);
486
+ const conflictDetails = this.buildConflictDetails(conflictFiles, 'rebase');
487
+ const hasConflicts = conflictDetails.length > 0 || /\bconflict\b/i.test(message);
488
+ await this.tryUpdateStatus(row.id, 'error', {
489
+ mergeConflicts: hasConflicts && conflictFiles.length > 0 ? conflictFiles.join('\n') : null,
490
+ errorMessage: message,
491
+ });
492
+ if (hasConflicts) {
493
+ throw new common_1.ConflictException({
494
+ message: 'Rebase failed with conflicts',
495
+ conflicts: conflictDetails,
496
+ });
497
+ }
498
+ throw new common_1.BadRequestException(`Rebase failed: ${message}`);
499
+ }
500
+ if (row.containerId) {
501
+ await this.dockerService.startContainer(row.containerId).catch(() => undefined);
502
+ const healthy = await this.dockerService.waitForHealthy(row.containerId, CONTAINER_HEALTH_TIMEOUT_MS);
503
+ if (!healthy) {
504
+ await this.tryUpdateStatus(row.id, 'error', {
505
+ errorMessage: 'Container failed readiness check after rebase',
506
+ });
507
+ throw new common_1.BadRequestException('Rebase succeeded but container failed readiness check');
508
+ }
509
+ this.recordWorktreeActivity({
510
+ worktreeId: row.id,
511
+ worktreeName: row.name,
512
+ ownerProjectId: row.ownerProjectId,
513
+ type: 'rebased',
514
+ message: `Worktree '${row.name}' rebased onto ${row.baseBranch}`,
515
+ });
516
+ const updated = await this.tryUpdateStatus(row.id, 'running', {
517
+ mergeConflicts: null,
518
+ errorMessage: null,
519
+ });
520
+ return this.toResponse(updated ?? row);
521
+ }
522
+ this.recordWorktreeActivity({
523
+ worktreeId: row.id,
524
+ worktreeName: row.name,
525
+ ownerProjectId: row.ownerProjectId,
526
+ type: 'rebased',
527
+ message: `Worktree '${row.name}' rebased onto ${row.baseBranch}`,
528
+ });
529
+ const updated = await this.store.update(row.id, {
530
+ mergeConflicts: null,
531
+ errorMessage: null,
532
+ });
533
+ return this.toResponse(updated ?? row);
534
+ }
535
+ async extractTasksForMergedHistory(row) {
536
+ let extractionError;
537
+ try {
538
+ await this.emitTaskMergeRequested(row.id);
539
+ return;
540
+ }
541
+ catch (error) {
542
+ extractionError = error;
543
+ }
544
+ const recovered = await this.tryRecoverContainerForTaskExtraction(row);
545
+ if (recovered) {
546
+ try {
547
+ await this.emitTaskMergeRequested(row.id);
548
+ return;
549
+ }
550
+ catch (retryError) {
551
+ extractionError = retryError;
552
+ }
553
+ }
554
+ const message = extractionError instanceof Error ? extractionError.message : String(extractionError);
555
+ const actionableMessage = 'Merge blocked: unable to preserve task history. Start or restore the worktree container, then retry merge.';
556
+ await this.tryUpdateStatus(row.id, 'error', {
557
+ errorMessage: `Task extraction failed before merge: ${message}`,
558
+ });
559
+ throw new common_1.BadRequestException(`${actionableMessage} (${message})`);
560
+ }
561
+ async emitTaskMergeRequested(worktreeId) {
562
+ const results = await this.eventEmitter.emitAsync(task_merge_events_1.WORKTREE_TASK_MERGE_REQUESTED_EVENT, {
563
+ worktreeId,
564
+ });
565
+ if (results.length === 0) {
566
+ throw new Error('No task merge handlers registered');
567
+ }
568
+ }
569
+ async ensureContainerReadyForTaskExtraction(row) {
570
+ if (!row.containerId) {
571
+ throw new common_1.BadRequestException('Merge blocked: worktree container is missing. Recreate or start a container before merge to preserve task history.');
572
+ }
573
+ const recovered = await this.tryRecoverContainerForTaskExtraction(row);
574
+ if (!recovered) {
575
+ await this.tryUpdateStatus(row.id, 'error', {
576
+ errorMessage: 'Task extraction failed before merge: container could not be started',
577
+ });
578
+ throw new common_1.BadRequestException('Merge blocked: unable to start worktree container for task extraction. Restore the container and retry merge.');
579
+ }
580
+ const refreshed = await this.requireWorktree(row.id);
581
+ return refreshed;
582
+ }
583
+ async tryRecoverContainerForTaskExtraction(row) {
584
+ if (!row.containerId) {
585
+ return false;
586
+ }
587
+ try {
588
+ await this.dockerService.startContainer(row.containerId).catch(() => undefined);
589
+ const healthy = await this.dockerService.waitForHealthy(row.containerId, CONTAINER_HEALTH_TIMEOUT_MS);
590
+ if (!healthy) {
591
+ return false;
592
+ }
593
+ await this.tryUpdateStatus(row.id, 'running', {
594
+ errorMessage: null,
595
+ }).catch(() => undefined);
596
+ return true;
597
+ }
598
+ catch (error) {
599
+ logger.warn({ error, worktreeId: row.id }, 'Failed recovering container for task extraction');
600
+ return false;
601
+ }
602
+ }
603
+ async assertCleanWorkingTree(path, operation) {
604
+ const status = await this.gitService.getWorkingTreeStatus(path);
605
+ if (status.clean) {
606
+ return;
607
+ }
608
+ throw new common_1.ConflictException({
609
+ message: `${operation} blocked: worktree has uncommitted changes`,
610
+ conflicts: [{ file: 'WORKTREE_DIRTY', type: 'uncommitted' }],
611
+ details: status.output,
612
+ });
613
+ }
614
+ buildMergeCommitMessage(row) {
615
+ const description = row.description?.trim();
616
+ if (!description) {
617
+ return `Merge ${row.branchName}`;
618
+ }
619
+ return `Merge ${row.branchName}: ${description}`;
620
+ }
621
+ extractConflictFiles(raw) {
622
+ const lines = raw.split('\n');
623
+ const files = new Set();
624
+ for (const line of lines) {
625
+ let match = line.match(/CONFLICT \([^)]+\): .* in (.+)$/i);
626
+ if (!match) {
627
+ match = line.match(/^\s*both modified:\s+(.+)$/i);
628
+ }
629
+ if (!match) {
630
+ match = line.match(/^\s*UU\s+(.+)$/i);
631
+ }
632
+ if (match?.[1]) {
633
+ files.add(match[1].trim());
634
+ }
635
+ }
636
+ return [...files];
637
+ }
638
+ buildConflictDetails(files, type) {
639
+ return [...new Set(files.map((file) => file.trim()).filter(Boolean))].map((file) => ({
640
+ file,
641
+ type,
642
+ }));
643
+ }
644
+ async getWorktreeLogs(id, query) {
645
+ const row = await this.requireWorktree(id);
646
+ if (this.resolveRuntimeType(row.runtimeType) === 'process') {
647
+ const logPath = this.resolveProcessLogPath(this.resolveDataPath(row.repoPath, row.name));
648
+ const content = await (0, promises_1.readFile)(logPath, 'utf8').catch((error) => {
649
+ if (error?.code === 'ENOENT') {
650
+ return '';
651
+ }
652
+ throw error;
653
+ });
654
+ const lines = content.split(/\r?\n/).filter((line, index, all) => {
655
+ if (line.length > 0) {
656
+ return true;
657
+ }
658
+ return index < all.length - 1;
659
+ });
660
+ const tailed = lines.slice(-query.tail).join('\n');
661
+ return { logs: tailed ? `${tailed}\n` : '' };
662
+ }
663
+ if (!row.containerId) {
664
+ throw new common_1.BadRequestException('Worktree has no container');
665
+ }
666
+ const logs = await this.dockerService.getContainerLogs(row.containerId, query.tail);
667
+ return { logs };
668
+ }
669
+ async buildWorktreeOverview(row) {
670
+ const worktree = await this.toResponse(row);
671
+ const fallback = {
672
+ worktree,
673
+ epics: { total: null, done: null },
674
+ agents: { total: null },
675
+ fetchedAt: new Date().toISOString(),
676
+ };
677
+ if (!row.containerPort || !row.devchainProjectId) {
678
+ return fallback;
679
+ }
680
+ const status = String(row.status).toLowerCase();
681
+ if (!['running', 'stopped', 'completed'].includes(status)) {
682
+ return fallback;
683
+ }
684
+ const baseUrl = `http://127.0.0.1:${row.containerPort}`;
685
+ const [epicsPayload, statusesPayload, agentsPayload] = await Promise.all([
686
+ this.fetchContainerJson(`${baseUrl}/api/epics?projectId=${encodeURIComponent(row.devchainProjectId)}&limit=1000`),
687
+ this.fetchContainerJson(`${baseUrl}/api/projects/${encodeURIComponent(row.devchainProjectId)}/statuses?limit=500`),
688
+ this.fetchContainerJson(`${baseUrl}/api/agents?projectId=${encodeURIComponent(row.devchainProjectId)}`),
689
+ ]);
690
+ if (!epicsPayload || !statusesPayload || !agentsPayload) {
691
+ return fallback;
692
+ }
693
+ const statusItems = statusesPayload.items ?? [];
694
+ const doneStatusIds = new Set(statusItems
695
+ .filter((statusItem) => {
696
+ const label = statusItem.label?.trim().toLowerCase();
697
+ return label === 'done' || label === 'completed';
698
+ })
699
+ .map((statusItem) => statusItem.id)
700
+ .filter((statusId) => Boolean(statusId)));
701
+ const epicItems = epicsPayload.items ?? [];
702
+ const doneCount = epicItems.reduce((total, epic) => {
703
+ if (!epic.statusId) {
704
+ return total;
705
+ }
706
+ return doneStatusIds.has(epic.statusId) ? total + 1 : total;
707
+ }, 0);
708
+ return {
709
+ worktree,
710
+ epics: {
711
+ total: typeof epicsPayload.total === 'number' ? epicsPayload.total : epicItems.length,
712
+ done: doneCount,
713
+ },
714
+ agents: {
715
+ total: typeof agentsPayload.total === 'number' ? agentsPayload.total : null,
716
+ },
717
+ fetchedAt: new Date().toISOString(),
718
+ };
719
+ }
720
+ async fetchContainerJson(url) {
721
+ const abortController = new AbortController();
722
+ const timeout = setTimeout(() => abortController.abort(), OVERVIEW_FETCH_TIMEOUT_MS);
723
+ try {
724
+ const response = await fetch(url, {
725
+ headers: { accept: 'application/json' },
726
+ signal: abortController.signal,
727
+ });
728
+ if (!response.ok) {
729
+ return null;
730
+ }
731
+ return (await response.json());
732
+ }
733
+ catch {
734
+ return null;
735
+ }
736
+ finally {
737
+ clearTimeout(timeout);
738
+ }
739
+ }
740
+ async monitorRunningWorktrees() {
741
+ const rows = await this.store.listMonitored();
742
+ await Promise.all(rows.map(async (row) => {
743
+ const runtimeType = this.resolveRuntimeType(row.runtimeType);
744
+ if (runtimeType === 'process') {
745
+ await this.monitorProcessWorktree(row);
746
+ return;
747
+ }
748
+ if (!row.containerId) {
749
+ return;
750
+ }
751
+ await this.dockerService
752
+ .ensureWorktreeOnComposeNetwork(row.name, row.containerId)
753
+ .catch(() => undefined);
754
+ const healthy = await this.dockerService.waitForHealthy(row.containerId, HEALTH_MONITOR_PROBE_TIMEOUT_MS);
755
+ if (healthy) {
756
+ this.consecutiveHealthFailures.set(row.id, 0);
757
+ if (row.status === 'error') {
758
+ await this.tryUpdateStatus(row.id, 'running', {
759
+ errorMessage: null,
760
+ });
761
+ }
762
+ return;
763
+ }
764
+ const failures = (this.consecutiveHealthFailures.get(row.id) ?? 0) + 1;
765
+ this.consecutiveHealthFailures.set(row.id, failures);
766
+ if (row.status === 'running' && failures >= MAX_CONSECUTIVE_HEALTH_FAILURES) {
767
+ await this.tryUpdateStatus(row.id, 'error', {
768
+ errorMessage: `Readiness probe failed ${failures} consecutive times`,
769
+ });
770
+ }
771
+ }));
772
+ }
773
+ async monitorProcessWorktree(row) {
774
+ const pid = row.processId ?? null;
775
+ if (!pid || !this.isProcessAlive(pid)) {
776
+ this.consecutiveHealthFailures.set(row.id, 0);
777
+ await this.tryUpdateStatus(row.id, 'stopped', {
778
+ processId: null,
779
+ runtimeToken: null,
780
+ startedAt: null,
781
+ containerPort: null,
782
+ errorMessage: null,
783
+ });
784
+ return;
785
+ }
786
+ const hostPort = row.containerPort ?? null;
787
+ const runtimeToken = row.runtimeToken?.trim() || null;
788
+ if (!hostPort || !runtimeToken) {
789
+ await this.handleProcessProbeFailure(row.id, row.status);
790
+ return;
791
+ }
792
+ const healthy = await this.checkRuntimeReady(hostPort);
793
+ if (!healthy) {
794
+ await this.handleProcessProbeFailure(row.id, row.status);
795
+ return;
796
+ }
797
+ const runtimeMetadata = await this.fetchRuntimeMetadata(hostPort);
798
+ const runtimeTokenMatches = runtimeMetadata?.runtimeToken === runtimeToken;
799
+ if (!runtimeTokenMatches) {
800
+ this.consecutiveHealthFailures.set(row.id, 0);
801
+ await this.tryUpdateStatus(row.id, 'stopped', {
802
+ processId: null,
803
+ runtimeToken: null,
804
+ startedAt: null,
805
+ containerPort: null,
806
+ errorMessage: null,
807
+ });
808
+ return;
809
+ }
810
+ this.consecutiveHealthFailures.set(row.id, 0);
811
+ if (row.status === 'error') {
812
+ await this.tryUpdateStatus(row.id, 'running', {
813
+ errorMessage: null,
814
+ });
815
+ }
816
+ }
817
+ async handleProcessProbeFailure(id, status) {
818
+ const failures = (this.consecutiveHealthFailures.get(id) ?? 0) + 1;
819
+ this.consecutiveHealthFailures.set(id, failures);
820
+ if (status === 'running' && failures >= MAX_CONSECUTIVE_HEALTH_FAILURES) {
821
+ await this.tryUpdateStatus(id, 'error', {
822
+ errorMessage: `Readiness probe failed ${failures} consecutive times`,
823
+ });
824
+ }
825
+ }
826
+ async reconcileProcessOrphans() {
827
+ const rows = await this.store.listMonitored();
828
+ const candidates = rows.filter((row) => this.resolveRuntimeType(row.runtimeType) === 'process' && row.status === 'running');
829
+ await Promise.all(candidates.map(async (row) => {
830
+ const pid = row.processId ?? null;
831
+ if (!pid || !this.isProcessAlive(pid)) {
832
+ await this.tryUpdateStatus(row.id, 'stopped', {
833
+ processId: null,
834
+ runtimeToken: null,
835
+ startedAt: null,
836
+ containerPort: null,
837
+ errorMessage: null,
838
+ });
839
+ return;
840
+ }
841
+ if (!row.containerPort || !row.runtimeToken) {
842
+ await this.tryUpdateStatus(row.id, 'stopped', {
843
+ processId: null,
844
+ runtimeToken: null,
845
+ startedAt: null,
846
+ containerPort: null,
847
+ errorMessage: null,
848
+ });
849
+ return;
850
+ }
851
+ const metadata = await this.fetchRuntimeMetadata(row.containerPort);
852
+ if (!metadata || metadata.runtimeToken !== row.runtimeToken) {
853
+ await this.tryUpdateStatus(row.id, 'stopped', {
854
+ processId: null,
855
+ runtimeToken: null,
856
+ startedAt: null,
857
+ containerPort: null,
858
+ errorMessage: null,
859
+ });
860
+ }
861
+ }));
862
+ }
863
+ resolveRuntimeType(runtimeType) {
864
+ return runtimeType === 'process' ? 'process' : 'container';
865
+ }
866
+ async startProcessRuntime(input) {
867
+ const runtimeToken = (0, crypto_1.randomUUID)();
868
+ const portFilePath = (0, path_1.join)(input.dataPath, PROCESS_RUNTIME_PORT_FILE);
869
+ await (0, promises_1.rm)(portFilePath, { force: true }).catch(() => undefined);
870
+ const processId = await this.spawnProcessRuntime({
871
+ worktreePath: input.worktreePath,
872
+ dataPath: input.dataPath,
873
+ projectId: input.projectId,
874
+ runtimeToken,
875
+ });
876
+ const portInfo = await this.waitForRuntimePortFile(portFilePath, CONTAINER_HEALTH_TIMEOUT_MS, processId);
877
+ if (!portInfo) {
878
+ await this.terminateProcess(processId).catch(() => undefined);
879
+ throw new Error('Process runtime did not report its port before timeout');
880
+ }
881
+ if (portInfo.runtimeToken !== runtimeToken) {
882
+ await this.terminateProcess(processId).catch(() => undefined);
883
+ throw new Error(`Runtime port file token mismatch: expected ${runtimeToken}, ` +
884
+ `got ${portInfo.runtimeToken ?? 'none'}`);
885
+ }
886
+ const hostPort = portInfo.port;
887
+ const healthy = await this.waitForRuntimeHealthy(hostPort, CONTAINER_HEALTH_TIMEOUT_MS, processId);
888
+ if (!healthy) {
889
+ await this.terminateProcess(processId).catch(() => undefined);
890
+ throw new Error('Process runtime did not become healthy before timeout');
891
+ }
892
+ return { processId, hostPort, runtimeToken, startedAt: new Date() };
893
+ }
894
+ async spawnProcessRuntime(input) {
895
+ const cliPath = this.resolveCliPath();
896
+ const logPath = this.resolveProcessLogPath(input.dataPath);
897
+ const portFilePath = (0, path_1.join)(input.dataPath, PROCESS_RUNTIME_PORT_FILE);
898
+ await (0, promises_1.mkdir)(input.dataPath, { recursive: true });
899
+ const logFile = await (0, promises_1.open)(logPath, 'a');
900
+ try {
901
+ const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'start', '--foreground', '--worktree-runtime', 'process', '--port', '0'], {
902
+ cwd: input.worktreePath,
903
+ detached: true,
904
+ stdio: ['ignore', logFile.fd, logFile.fd],
905
+ env: {
906
+ ...process.env,
907
+ PORT: '0',
908
+ HOST: '127.0.0.1',
909
+ NODE_ENV: 'production',
910
+ DB_PATH: input.dataPath,
911
+ DB_FILENAME: PROCESS_DB_FILE_NAME,
912
+ DEVCHAIN_MODE: 'normal',
913
+ CONTAINER_PROJECT_ID: input.projectId,
914
+ RUNTIME_TOKEN: input.runtimeToken,
915
+ RUNTIME_PORT_FILE: portFilePath,
916
+ },
917
+ });
918
+ const pid = await this.awaitSpawn(child);
919
+ child.unref();
920
+ return pid;
921
+ }
922
+ finally {
923
+ await logFile.close().catch(() => undefined);
924
+ }
925
+ }
926
+ async awaitSpawn(child) {
927
+ if (typeof child.pid === 'number') {
928
+ return child.pid;
929
+ }
930
+ return new Promise((resolve, reject) => {
931
+ const onSpawn = () => {
932
+ cleanup();
933
+ if (typeof child.pid !== 'number') {
934
+ reject(new Error('Process runtime started without a PID'));
935
+ return;
936
+ }
937
+ resolve(child.pid);
938
+ };
939
+ const onError = (error) => {
940
+ cleanup();
941
+ reject(error instanceof Error ? error : new Error(String(error)));
942
+ };
943
+ const cleanup = () => {
944
+ child.off('spawn', onSpawn);
945
+ child.off('error', onError);
946
+ };
947
+ child.once('spawn', onSpawn);
948
+ child.once('error', onError);
949
+ });
950
+ }
951
+ async waitForRuntimeHealthy(hostPort, timeoutMs, pid) {
952
+ if (timeoutMs <= 0) {
953
+ return false;
954
+ }
955
+ const deadline = Date.now() + timeoutMs;
956
+ while (Date.now() < deadline) {
957
+ if (pid && !this.isProcessAlive(pid)) {
958
+ logger.warn({ pid, hostPort }, 'Child process exited during health polling');
959
+ return false;
960
+ }
961
+ const isReady = await this.checkRuntimeReady(hostPort);
962
+ if (isReady) {
963
+ return true;
964
+ }
965
+ await this.sleep(PROCESS_HEALTH_POLL_INTERVAL_MS);
966
+ }
967
+ return false;
968
+ }
969
+ async waitForRuntimePortFile(filePath, timeoutMs, pid) {
970
+ if (timeoutMs <= 0) {
971
+ return null;
972
+ }
973
+ const deadline = Date.now() + timeoutMs;
974
+ while (Date.now() < deadline) {
975
+ if (pid && !this.isProcessAlive(pid)) {
976
+ logger.warn({ pid, filePath }, 'Child process exited before writing port file');
977
+ return null;
978
+ }
979
+ try {
980
+ const raw = await (0, promises_1.readFile)(filePath, 'utf-8');
981
+ const parsed = JSON.parse(raw);
982
+ if (typeof parsed.port === 'number' && parsed.port > 0) {
983
+ return { port: parsed.port, runtimeToken: parsed.runtimeToken ?? null };
984
+ }
985
+ }
986
+ catch {
987
+ }
988
+ await this.sleep(PROCESS_HEALTH_POLL_INTERVAL_MS);
989
+ }
990
+ return null;
991
+ }
992
+ async checkRuntimeReady(hostPort) {
993
+ const controller = new AbortController();
994
+ const timeout = setTimeout(() => controller.abort(), PROCESS_RUNTIME_TIMEOUT_MS);
995
+ try {
996
+ const response = await fetch(`http://127.0.0.1:${hostPort}/health/ready`, {
997
+ signal: controller.signal,
998
+ });
999
+ return response.ok;
1000
+ }
1001
+ catch {
1002
+ return false;
1003
+ }
1004
+ finally {
1005
+ clearTimeout(timeout);
1006
+ }
1007
+ }
1008
+ async fetchRuntimeMetadata(hostPort) {
1009
+ const controller = new AbortController();
1010
+ const timeout = setTimeout(() => controller.abort(), PROCESS_RUNTIME_TIMEOUT_MS);
1011
+ try {
1012
+ const response = await fetch(`http://127.0.0.1:${hostPort}/api/runtime`, {
1013
+ headers: { accept: 'application/json' },
1014
+ signal: controller.signal,
1015
+ });
1016
+ if (!response.ok) {
1017
+ return null;
1018
+ }
1019
+ return (await response.json());
1020
+ }
1021
+ catch {
1022
+ return null;
1023
+ }
1024
+ finally {
1025
+ clearTimeout(timeout);
1026
+ }
1027
+ }
1028
+ resolveCliPath() {
1029
+ const candidates = [
1030
+ (0, path_1.resolve)(process.cwd(), 'scripts', 'cli.js'),
1031
+ (0, path_1.resolve)(__dirname, '../../../../../../../../scripts/cli.js'),
1032
+ (0, path_1.resolve)(__dirname, '../../../../../../../scripts/cli.js'),
1033
+ (0, path_1.resolve)(__dirname, '../../../../../cli.js'),
1034
+ ];
1035
+ if (process.argv[1] && !candidates.includes((0, path_1.resolve)(process.argv[1]))) {
1036
+ candidates.push((0, path_1.resolve)(process.argv[1]));
1037
+ }
1038
+ for (const candidate of candidates) {
1039
+ if ((0, fs_1.existsSync)(candidate)) {
1040
+ return candidate;
1041
+ }
1042
+ }
1043
+ throw new Error('Unable to locate CLI entry point for process runtime start. ' +
1044
+ `Searched: ${candidates.join(', ')}`);
1045
+ }
1046
+ resolveProcessLogPath(dataPath) {
1047
+ return (0, path_1.join)(dataPath, PROCESS_LOG_FILE_NAME);
1048
+ }
1049
+ async terminateProcess(pid) {
1050
+ if (!pid) {
1051
+ return;
1052
+ }
1053
+ const stillRunningAfterSigterm = await this.signalProcessAndAwaitExit(pid, 'SIGTERM', PROCESS_SHUTDOWN_TIMEOUT_MS);
1054
+ if (!stillRunningAfterSigterm) {
1055
+ return;
1056
+ }
1057
+ await this.signalProcessAndAwaitExit(pid, 'SIGKILL', PROCESS_KILL_TIMEOUT_MS).catch((error) => {
1058
+ logger.warn({ error, pid }, 'Failed sending SIGKILL to worktree process');
1059
+ });
1060
+ }
1061
+ async signalProcessAndAwaitExit(pid, signal, timeoutMs) {
1062
+ const signalPid = process.platform === 'win32' ? pid : -pid;
1063
+ try {
1064
+ process.kill(signalPid, signal);
1065
+ }
1066
+ catch (error) {
1067
+ const code = error.code;
1068
+ if (code === 'ESRCH') {
1069
+ return false;
1070
+ }
1071
+ throw error;
1072
+ }
1073
+ const deadline = Date.now() + timeoutMs;
1074
+ while (Date.now() < deadline) {
1075
+ if (!this.isProcessAlive(pid)) {
1076
+ return false;
1077
+ }
1078
+ await this.sleep(200);
1079
+ }
1080
+ return this.isProcessAlive(pid);
1081
+ }
1082
+ isProcessAlive(pid) {
1083
+ try {
1084
+ process.kill(pid, 0);
1085
+ return true;
1086
+ }
1087
+ catch (error) {
1088
+ const code = error.code;
1089
+ if (code === 'ESRCH') {
1090
+ return false;
1091
+ }
1092
+ if (code === 'EPERM') {
1093
+ return true;
1094
+ }
1095
+ return false;
1096
+ }
1097
+ }
1098
+ async sleep(ms) {
1099
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
1100
+ }
1101
+ async handleContainerEvent(event) {
1102
+ const containerId = event.id;
1103
+ if (!containerId) {
1104
+ return;
1105
+ }
1106
+ const action = event.Action ?? event.status;
1107
+ if (!action) {
1108
+ return;
1109
+ }
1110
+ const row = await this.store.getByContainerId(containerId);
1111
+ if (!row) {
1112
+ return;
1113
+ }
1114
+ if (['die', 'stop', 'kill', 'destroy'].includes(action)) {
1115
+ this.consecutiveHealthFailures.set(row.id, 0);
1116
+ await this.tryUpdateStatus(row.id, 'stopped', {
1117
+ errorMessage: null,
1118
+ });
1119
+ return;
1120
+ }
1121
+ if (['start', 'restart'].includes(action)) {
1122
+ this.consecutiveHealthFailures.set(row.id, 0);
1123
+ await this.tryUpdateStatus(row.id, 'running', {
1124
+ errorMessage: null,
1125
+ });
1126
+ }
1127
+ }
1128
+ async requireWorktree(id) {
1129
+ const row = await this.store.getById(id);
1130
+ if (!row) {
1131
+ throw new common_1.NotFoundException(`Worktree not found: ${id}`);
1132
+ }
1133
+ return row;
1134
+ }
1135
+ async registerProjectInContainer(hostPort, input) {
1136
+ if (!hostPort) {
1137
+ throw new Error('Container did not expose a host port');
1138
+ }
1139
+ await this.ensureTemplateExists(hostPort, input.templateSlug);
1140
+ const response = await fetch(`http://127.0.0.1:${hostPort}/api/projects/from-template`, {
1141
+ method: 'POST',
1142
+ headers: { 'content-type': 'application/json' },
1143
+ body: JSON.stringify({
1144
+ name: input.name,
1145
+ description: input.description,
1146
+ rootPath: input.rootPath,
1147
+ slug: input.templateSlug,
1148
+ projectId: input.projectId,
1149
+ ...(input.presetName && { presetName: input.presetName }),
1150
+ }),
1151
+ });
1152
+ const payload = (await response.json().catch(() => ({})));
1153
+ logger.debug({
1154
+ hostPort,
1155
+ status: response.status,
1156
+ success: payload.success,
1157
+ projectId: payload.project?.id,
1158
+ message: payload.message,
1159
+ }, 'registerProjectInContainer response');
1160
+ if (!response.ok) {
1161
+ throw new Error(payload.message || `Project registration failed with HTTP ${response.status}`);
1162
+ }
1163
+ if (!payload.success || !payload.project?.id) {
1164
+ throw new Error('Project registration failed: invalid response payload');
1165
+ }
1166
+ if (payload.project.id !== input.projectId) {
1167
+ throw new Error('Project registration failed: returned project id did not match requested id');
1168
+ }
1169
+ return { projectId: payload.project.id };
1170
+ }
1171
+ async ensureTemplateExists(hostPort, templateSlug) {
1172
+ const response = await fetch(`http://127.0.0.1:${hostPort}/api/templates`);
1173
+ const payload = (await response.json().catch(() => ({})));
1174
+ const templates = payload.templates ?? [];
1175
+ if (!response.ok || !templates.some((template) => template.slug === templateSlug)) {
1176
+ throw new Error(`Template slug "${templateSlug}" is not available in runtime`);
1177
+ }
1178
+ }
1179
+ resolveRepoPath(repoPath) {
1180
+ if (repoPath) {
1181
+ return (0, path_1.resolve)(repoPath);
1182
+ }
1183
+ const env = (0, env_config_1.getEnvConfig)();
1184
+ if (env.DEVCHAIN_MODE !== 'normal' && env.REPO_ROOT) {
1185
+ return (0, path_1.resolve)(env.REPO_ROOT);
1186
+ }
1187
+ return (0, path_1.resolve)(process.cwd());
1188
+ }
1189
+ resolveWorktreeRoot(repoPath) {
1190
+ const env = (0, env_config_1.getEnvConfig)();
1191
+ const root = env.WORKTREES_ROOT ?? (0, path_1.join)(repoPath, 'worktrees');
1192
+ return (0, path_1.resolve)(root);
1193
+ }
1194
+ resolveDataRoot(repoPath) {
1195
+ const env = (0, env_config_1.getEnvConfig)();
1196
+ const root = env.WORKTREES_DATA_ROOT ?? (0, path_1.join)(repoPath, 'worktrees-data');
1197
+ return (0, path_1.resolve)(root);
1198
+ }
1199
+ resolveWorktreePath(repoPath, name) {
1200
+ this.assertValidWorktreeName(name);
1201
+ const root = this.resolveWorktreeRoot(repoPath);
1202
+ return this.ensurePathWithinRoot(root, (0, path_1.resolve)(root, name), 'worktree path');
1203
+ }
1204
+ resolveDataPath(repoPath, name) {
1205
+ this.assertValidWorktreeName(name);
1206
+ const root = this.resolveDataRoot(repoPath);
1207
+ return this.ensurePathWithinRoot(root, (0, path_1.resolve)(root, name, 'data'), 'worktree data path');
1208
+ }
1209
+ getContainerName(name) {
1210
+ this.assertValidWorktreeName(name);
1211
+ return `devchain-wt-${name}`;
1212
+ }
1213
+ ensurePathWithinRoot(rootPath, candidatePath, label) {
1214
+ const resolvedRoot = (0, path_1.resolve)(rootPath);
1215
+ const resolvedCandidate = (0, path_1.resolve)(candidatePath);
1216
+ const rootWithSeparator = resolvedRoot.endsWith(path_1.sep) ? resolvedRoot : `${resolvedRoot}${path_1.sep}`;
1217
+ if (resolvedCandidate !== resolvedRoot && !resolvedCandidate.startsWith(rootWithSeparator)) {
1218
+ throw new common_1.BadRequestException(`Invalid ${label}: path escapes configured root`);
1219
+ }
1220
+ const relativePath = (0, path_1.relative)(resolvedRoot, resolvedCandidate);
1221
+ if (relativePath.startsWith('..') || (0, path_1.isAbsolute)(relativePath)) {
1222
+ throw new common_1.BadRequestException(`Invalid ${label}: path escapes configured root`);
1223
+ }
1224
+ return resolvedCandidate;
1225
+ }
1226
+ assertValidWorktreeName(name) {
1227
+ if (!(0, worktree_validation_1.isValidWorktreeName)(name)) {
1228
+ throw new common_1.BadRequestException('Invalid worktree name. Use lowercase letters, numbers, and hyphens (1-63 chars, no edge hyphen).');
1229
+ }
1230
+ }
1231
+ assertValidBranchName(branchName, fieldName) {
1232
+ if (!(0, worktree_validation_1.isValidGitBranchName)(branchName)) {
1233
+ throw new common_1.BadRequestException(`Invalid ${fieldName}`);
1234
+ }
1235
+ }
1236
+ async toResponse(row) {
1237
+ let commitsAhead = null;
1238
+ let commitsBehind = null;
1239
+ if (row.repoPath && row.baseBranch && row.branchName) {
1240
+ try {
1241
+ const branchStatus = await this.gitService.getBranchStatus(row.repoPath, row.baseBranch, row.branchName);
1242
+ commitsAhead = branchStatus.commitsAhead;
1243
+ commitsBehind = branchStatus.commitsBehind;
1244
+ }
1245
+ catch (error) {
1246
+ logger.debug({ error, worktreeId: row.id }, 'Unable to compute branch ahead/behind');
1247
+ }
1248
+ }
1249
+ return {
1250
+ id: row.id,
1251
+ name: row.name,
1252
+ branchName: row.branchName,
1253
+ baseBranch: row.baseBranch,
1254
+ repoPath: row.repoPath,
1255
+ worktreePath: row.worktreePath ?? null,
1256
+ containerId: row.containerId ?? null,
1257
+ containerPort: row.containerPort ?? null,
1258
+ templateSlug: row.templateSlug,
1259
+ ownerProjectId: row.ownerProjectId,
1260
+ status: row.status,
1261
+ description: row.description ?? null,
1262
+ devchainProjectId: row.devchainProjectId ?? null,
1263
+ mergeCommit: row.mergeCommit ?? null,
1264
+ mergeConflicts: row.mergeConflicts ?? null,
1265
+ errorMessage: row.errorMessage ?? null,
1266
+ commitsAhead,
1267
+ commitsBehind,
1268
+ runtimeType: row.runtimeType ?? 'container',
1269
+ processId: row.processId ?? null,
1270
+ runtimeToken: row.runtimeToken ?? null,
1271
+ startedAt: row.startedAt ? row.startedAt.toISOString() : null,
1272
+ createdAt: row.createdAt.toISOString(),
1273
+ updatedAt: row.updatedAt.toISOString(),
1274
+ };
1275
+ }
1276
+ async tryUpdateStatus(id, nextStatus, extraPatch = {}, options = {}) {
1277
+ const current = await this.store.getById(id);
1278
+ if (!current) {
1279
+ return null;
1280
+ }
1281
+ this.assertValidStatusTransition(current.status, nextStatus);
1282
+ const updated = await this.store.update(id, {
1283
+ ...extraPatch,
1284
+ status: nextStatus,
1285
+ });
1286
+ if (!updated) {
1287
+ return null;
1288
+ }
1289
+ if (current.status !== nextStatus) {
1290
+ this.eventEmitter.emit(worktree_events_1.WORKTREE_CHANGED_EVENT, {
1291
+ worktreeId: id,
1292
+ });
1293
+ const activity = options.activity ?? this.getStatusTransitionActivity(current, updated, nextStatus);
1294
+ if (activity) {
1295
+ this.recordWorktreeActivity({
1296
+ worktreeId: id,
1297
+ worktreeName: updated.name,
1298
+ ownerProjectId: updated.ownerProjectId,
1299
+ type: activity.type,
1300
+ message: activity.message,
1301
+ });
1302
+ }
1303
+ }
1304
+ return updated;
1305
+ }
1306
+ getStatusTransitionActivity(current, updated, nextStatus) {
1307
+ if (current.status === nextStatus) {
1308
+ return null;
1309
+ }
1310
+ if (nextStatus === 'running') {
1311
+ return {
1312
+ type: 'started',
1313
+ message: `Worktree '${updated.name}' started`,
1314
+ };
1315
+ }
1316
+ if (nextStatus === 'stopped') {
1317
+ return {
1318
+ type: 'stopped',
1319
+ message: `Worktree '${updated.name}' stopped`,
1320
+ };
1321
+ }
1322
+ if (nextStatus === 'error') {
1323
+ const detail = updated.errorMessage?.trim() || 'Unknown error';
1324
+ return {
1325
+ type: 'error',
1326
+ message: `Worktree '${updated.name}' encountered an error: ${detail}`,
1327
+ };
1328
+ }
1329
+ if (nextStatus === 'merged') {
1330
+ return {
1331
+ type: 'merged',
1332
+ message: `Worktree '${updated.name}' merged`,
1333
+ };
1334
+ }
1335
+ return null;
1336
+ }
1337
+ recordWorktreeActivity(params) {
1338
+ void this.eventLogService
1339
+ .recordPublished({
1340
+ name: WORKTREE_ACTIVITY_EVENT_NAME,
1341
+ payload: {
1342
+ worktreeId: params.worktreeId,
1343
+ worktreeName: params.worktreeName,
1344
+ ownerProjectId: params.ownerProjectId,
1345
+ type: params.type,
1346
+ message: params.message,
1347
+ },
1348
+ })
1349
+ .catch((error) => {
1350
+ logger.warn({ error, worktreeId: params.worktreeId, type: params.type }, 'Failed to record worktree activity event');
1351
+ });
1352
+ }
1353
+ assertValidStatusTransition(current, next) {
1354
+ if (current === next) {
1355
+ return;
1356
+ }
1357
+ const allowed = {
1358
+ creating: ['running', 'error'],
1359
+ running: ['stopped', 'completed', 'merged', 'error'],
1360
+ stopped: ['running', 'merged', 'error'],
1361
+ completed: ['merged', 'running', 'error'],
1362
+ merged: [],
1363
+ error: ['running', 'stopped'],
1364
+ };
1365
+ if (!allowed[current]?.includes(next)) {
1366
+ throw new common_1.BadRequestException(`Invalid worktree status transition: ${current} -> ${next}`);
1367
+ }
1368
+ }
1369
+ };
1370
+ exports.WorktreesService = WorktreesService;
1371
+ exports.WorktreesService = WorktreesService = __decorate([
1372
+ (0, common_1.Injectable)(),
1373
+ __param(0, (0, common_1.Inject)(worktrees_store_1.WORKTREES_STORE)),
1374
+ __metadata("design:paramtypes", [Object, docker_service_1.OrchestratorDockerService,
1375
+ git_worktree_service_1.GitWorktreeService,
1376
+ seed_preparation_service_1.SeedPreparationService,
1377
+ event_emitter_1.EventEmitter2,
1378
+ event_log_service_1.EventLogService])
1379
+ ], WorktreesService);
1380
+ //# sourceMappingURL=worktrees.service.js.map