devchain-cli 0.9.2 → 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 (313) hide show
  1. package/dist/cli.js +985 -194
  2. package/dist/drizzle/0044_supreme_joshua_kane.sql +57 -0
  3. package/dist/drizzle/0045_provider_auto_compact_threshold.sql +11 -0
  4. package/dist/drizzle/0046_worktrees_owner_project_id.sql +3 -0
  5. package/dist/drizzle/meta/0044_snapshot.json +4620 -0
  6. package/dist/drizzle/meta/_journal.json +22 -1
  7. package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts +18 -0
  8. package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts.map +1 -1
  9. package/dist/node_modules/@devchain/shared/schemas/export-schema.js +6 -0
  10. package/dist/node_modules/@devchain/shared/schemas/export-schema.js.map +1 -1
  11. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  12. package/dist/server/app.main.module.d.ts +4 -0
  13. package/dist/server/app.main.module.js +102 -0
  14. package/dist/server/app.main.module.js.map +1 -0
  15. package/dist/server/app.module.d.ts +1 -4
  16. package/dist/server/app.module.js +2 -78
  17. package/dist/server/app.module.js.map +1 -1
  18. package/dist/server/app.normal.module.d.ts +4 -0
  19. package/dist/server/app.normal.module.js +90 -0
  20. package/dist/server/app.normal.module.js.map +1 -0
  21. package/dist/server/common/config/env.config.d.ts +81 -1
  22. package/dist/server/common/config/env.config.js +52 -2
  23. package/dist/server/common/config/env.config.js.map +1 -1
  24. package/dist/server/common/templates-directory.d.ts +8 -0
  25. package/dist/server/common/templates-directory.js +56 -0
  26. package/dist/server/common/templates-directory.js.map +1 -0
  27. package/dist/server/main.js +58 -7
  28. package/dist/server/main.js.map +1 -1
  29. package/dist/server/modules/chat/dtos/chat.dto.d.ts +2 -2
  30. package/dist/server/modules/core/controllers/health.controller.d.ts +4 -0
  31. package/dist/server/modules/core/controllers/health.controller.js +22 -1
  32. package/dist/server/modules/core/controllers/health.controller.js.map +1 -1
  33. package/dist/server/modules/core/controllers/runtime.controller.d.ts +18 -0
  34. package/dist/server/modules/core/controllers/runtime.controller.js +130 -0
  35. package/dist/server/modules/core/controllers/runtime.controller.js.map +1 -0
  36. package/dist/server/modules/core/core-common.module.d.ts +2 -0
  37. package/dist/server/modules/core/core-common.module.js +24 -0
  38. package/dist/server/modules/core/core-common.module.js.map +1 -0
  39. package/dist/server/modules/core/core-main-health.module.d.ts +2 -0
  40. package/dist/server/modules/core/core-main-health.module.js +32 -0
  41. package/dist/server/modules/core/core-main-health.module.js.map +1 -0
  42. package/dist/server/modules/core/core-normal-health.module.d.ts +2 -0
  43. package/dist/server/modules/core/core-normal-health.module.js +29 -0
  44. package/dist/server/modules/core/core-normal-health.module.js.map +1 -0
  45. package/dist/server/modules/core/core-normal.module.d.ts +2 -0
  46. package/dist/server/modules/core/core-normal.module.js +28 -0
  47. package/dist/server/modules/core/core-normal.module.js.map +1 -0
  48. package/dist/server/modules/core/core.module.js +4 -11
  49. package/dist/server/modules/core/core.module.js.map +1 -1
  50. package/dist/server/modules/core/services/health.service.d.ts +13 -0
  51. package/dist/server/modules/core/services/health.service.js +39 -0
  52. package/dist/server/modules/core/services/health.service.js.map +1 -0
  53. package/dist/server/modules/core/services/main-readiness-checker.service.d.ts +14 -0
  54. package/dist/server/modules/core/services/main-readiness-checker.service.js +82 -0
  55. package/dist/server/modules/core/services/main-readiness-checker.service.js.map +1 -0
  56. package/dist/server/modules/core/services/normal-readiness-checker.service.d.ts +13 -0
  57. package/dist/server/modules/core/services/normal-readiness-checker.service.js +67 -0
  58. package/dist/server/modules/core/services/normal-readiness-checker.service.js.map +1 -0
  59. package/dist/server/modules/core/services/preflight.service.d.ts +1 -0
  60. package/dist/server/modules/core/services/preflight.service.js +18 -1
  61. package/dist/server/modules/core/services/preflight.service.js.map +1 -1
  62. package/dist/server/modules/core/services/provider-mcp-ensure.service.js +8 -0
  63. package/dist/server/modules/core/services/provider-mcp-ensure.service.js.map +1 -1
  64. package/dist/server/modules/epics/epics.module.js +2 -2
  65. package/dist/server/modules/epics/epics.module.js.map +1 -1
  66. package/dist/server/modules/events/catalog/claude.hooks.session.started.d.ts +39 -0
  67. package/dist/server/modules/events/catalog/claude.hooks.session.started.js +20 -0
  68. package/dist/server/modules/events/catalog/claude.hooks.session.started.js.map +1 -0
  69. package/dist/server/modules/events/catalog/epic.created.d.ts +2 -2
  70. package/dist/server/modules/events/catalog/index.d.ts +38 -4
  71. package/dist/server/modules/events/catalog/index.js +2 -0
  72. package/dist/server/modules/events/catalog/index.js.map +1 -1
  73. package/dist/server/modules/events/catalog/terminal.watcher.triggered.d.ts +2 -2
  74. package/dist/server/modules/events/controllers/event-log.controller.d.ts +1 -1
  75. package/dist/server/modules/events/controllers/event-log.controller.js +11 -9
  76. package/dist/server/modules/events/controllers/event-log.controller.js.map +1 -1
  77. package/dist/server/modules/events/dtos/event-log.dto.d.ts +1 -0
  78. package/dist/server/modules/events/events-domain.module.d.ts +2 -0
  79. package/dist/server/modules/events/events-domain.module.js +42 -0
  80. package/dist/server/modules/events/events-domain.module.js.map +1 -0
  81. package/dist/server/modules/events/events-infra.module.d.ts +2 -0
  82. package/dist/server/modules/events/events-infra.module.js +26 -0
  83. package/dist/server/modules/events/events-infra.module.js.map +1 -0
  84. package/dist/server/modules/events/events.module.js +4 -27
  85. package/dist/server/modules/events/events.module.js.map +1 -1
  86. package/dist/server/modules/events/index.d.ts +2 -0
  87. package/dist/server/modules/events/index.js +2 -0
  88. package/dist/server/modules/events/index.js.map +1 -1
  89. package/dist/server/modules/events/services/event-log.service.d.ts +8 -1
  90. package/dist/server/modules/events/services/event-log.service.js +41 -0
  91. package/dist/server/modules/events/services/event-log.service.js.map +1 -1
  92. package/dist/server/modules/events/subscribers/index.js +2 -0
  93. package/dist/server/modules/events/subscribers/index.js.map +1 -1
  94. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.d.ts +8 -0
  95. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js +48 -0
  96. package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js.map +1 -0
  97. package/dist/server/modules/git/dtos/git.dto.d.ts +1 -1
  98. package/dist/server/modules/guests/guests.module.js +2 -2
  99. package/dist/server/modules/guests/guests.module.js.map +1 -1
  100. package/dist/server/modules/hooks/controllers/hooks.controller.d.ts +7 -0
  101. package/dist/server/modules/hooks/controllers/hooks.controller.js +49 -0
  102. package/dist/server/modules/hooks/controllers/hooks.controller.js.map +1 -0
  103. package/dist/server/modules/hooks/dtos/hook-event.dto.d.ts +41 -0
  104. package/dist/server/modules/hooks/dtos/hook-event.dto.js +19 -0
  105. package/dist/server/modules/hooks/dtos/hook-event.dto.js.map +1 -0
  106. package/dist/server/modules/hooks/hooks.module.d.ts +2 -0
  107. package/dist/server/modules/hooks/hooks.module.js +27 -0
  108. package/dist/server/modules/hooks/hooks.module.js.map +1 -0
  109. package/dist/server/modules/hooks/services/hooks-config.service.d.ts +5 -0
  110. package/dist/server/modules/hooks/services/hooks-config.service.js +215 -0
  111. package/dist/server/modules/hooks/services/hooks-config.service.js.map +1 -0
  112. package/dist/server/modules/hooks/services/hooks.service.d.ts +11 -0
  113. package/dist/server/modules/hooks/services/hooks.service.js +83 -0
  114. package/dist/server/modules/hooks/services/hooks.service.js.map +1 -0
  115. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +14 -14
  116. package/dist/server/modules/mcp/mcp.module.js +2 -2
  117. package/dist/server/modules/mcp/mcp.module.js.map +1 -1
  118. package/dist/server/modules/orchestrator/docker/docker.module.d.ts +2 -0
  119. package/dist/server/modules/orchestrator/docker/docker.module.js +22 -0
  120. package/dist/server/modules/orchestrator/docker/docker.module.js.map +1 -0
  121. package/dist/server/modules/orchestrator/docker/index.d.ts +3 -0
  122. package/dist/server/modules/orchestrator/docker/index.js +20 -0
  123. package/dist/server/modules/orchestrator/docker/index.js.map +1 -0
  124. package/dist/server/modules/orchestrator/docker/services/docker.service.d.ts +85 -0
  125. package/dist/server/modules/orchestrator/docker/services/docker.service.js +745 -0
  126. package/dist/server/modules/orchestrator/docker/services/docker.service.js.map +1 -0
  127. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.d.ts +11 -0
  128. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js +181 -0
  129. package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js.map +1 -0
  130. package/dist/server/modules/orchestrator/git/controllers/git.controller.d.ts +8 -0
  131. package/dist/server/modules/orchestrator/git/controllers/git.controller.js +38 -0
  132. package/dist/server/modules/orchestrator/git/controllers/git.controller.js.map +1 -0
  133. package/dist/server/modules/orchestrator/git/git.module.d.ts +2 -0
  134. package/dist/server/modules/orchestrator/git/git.module.js +23 -0
  135. package/dist/server/modules/orchestrator/git/git.module.js.map +1 -0
  136. package/dist/server/modules/orchestrator/git/index.d.ts +3 -0
  137. package/dist/server/modules/orchestrator/git/index.js +20 -0
  138. package/dist/server/modules/orchestrator/git/index.js.map +1 -0
  139. package/dist/server/modules/orchestrator/git/services/git-worktree.service.d.ts +83 -0
  140. package/dist/server/modules/orchestrator/git/services/git-worktree.service.js +474 -0
  141. package/dist/server/modules/orchestrator/git/services/git-worktree.service.js.map +1 -0
  142. package/dist/server/modules/orchestrator/index.d.ts +6 -0
  143. package/dist/server/modules/orchestrator/index.js +23 -0
  144. package/dist/server/modules/orchestrator/index.js.map +1 -0
  145. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.d.ts +1 -0
  146. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js +18 -0
  147. package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js.map +1 -0
  148. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.d.ts +5 -0
  149. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js +10 -0
  150. package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js.map +1 -0
  151. package/dist/server/modules/orchestrator/orchestrator-storage/index.d.ts +2 -0
  152. package/dist/server/modules/orchestrator/orchestrator-storage/index.js +19 -0
  153. package/dist/server/modules/orchestrator/orchestrator-storage/index.js.map +1 -0
  154. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.d.ts +2 -0
  155. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js +23 -0
  156. package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js.map +1 -0
  157. package/dist/server/modules/orchestrator/proxy/index.d.ts +2 -0
  158. package/dist/server/modules/orchestrator/proxy/index.js +19 -0
  159. package/dist/server/modules/orchestrator/proxy/index.js.map +1 -0
  160. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.d.ts +2 -0
  161. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js +22 -0
  162. package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js.map +1 -0
  163. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.d.ts +18 -0
  164. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js +192 -0
  165. package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js.map +1 -0
  166. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.d.ts +9 -0
  167. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js +66 -0
  168. package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js.map +1 -0
  169. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.d.ts +52 -0
  170. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js +3 -0
  171. package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js.map +1 -0
  172. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.d.ts +5 -0
  173. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js +3 -0
  174. package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js.map +1 -0
  175. package/dist/server/modules/orchestrator/sync/events/task-merge.events.d.ts +4 -0
  176. package/dist/server/modules/orchestrator/sync/events/task-merge.events.js +5 -0
  177. package/dist/server/modules/orchestrator/sync/events/task-merge.events.js.map +1 -0
  178. package/dist/server/modules/orchestrator/sync/index.d.ts +7 -0
  179. package/dist/server/modules/orchestrator/sync/index.js +24 -0
  180. package/dist/server/modules/orchestrator/sync/index.js.map +1 -0
  181. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.d.ts +31 -0
  182. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js +410 -0
  183. package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js.map +1 -0
  184. package/dist/server/modules/orchestrator/sync/services/task-merge.service.d.ts +44 -0
  185. package/dist/server/modules/orchestrator/sync/services/task-merge.service.js +730 -0
  186. package/dist/server/modules/orchestrator/sync/services/task-merge.service.js.map +1 -0
  187. package/dist/server/modules/orchestrator/sync/sync.module.d.ts +2 -0
  188. package/dist/server/modules/orchestrator/sync/sync.module.js +36 -0
  189. package/dist/server/modules/orchestrator/sync/sync.module.js.map +1 -0
  190. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.d.ts +118 -0
  191. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js +297 -0
  192. package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js.map +1 -0
  193. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.d.ts +17 -0
  194. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js +752 -0
  195. package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js.map +1 -0
  196. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.d.ts +15 -0
  197. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js +85 -0
  198. package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js.map +1 -0
  199. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.d.ts +29 -0
  200. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js +272 -0
  201. package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js.map +1 -0
  202. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.d.ts +109 -0
  203. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js +57 -0
  204. package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js.map +1 -0
  205. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.d.ts +4 -0
  206. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js +5 -0
  207. package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js.map +1 -0
  208. package/dist/server/modules/orchestrator/worktrees/index.d.ts +7 -0
  209. package/dist/server/modules/orchestrator/worktrees/index.js +24 -0
  210. package/dist/server/modules/orchestrator/worktrees/index.js.map +1 -0
  211. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.d.ts +18 -0
  212. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js +198 -0
  213. package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js.map +1 -0
  214. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.d.ts +87 -0
  215. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js +1380 -0
  216. package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js.map +1 -0
  217. package/dist/server/modules/orchestrator/worktrees/worktree-validation.d.ts +4 -0
  218. package/dist/server/modules/orchestrator/worktrees/worktree-validation.js +43 -0
  219. package/dist/server/modules/orchestrator/worktrees/worktree-validation.js.map +1 -0
  220. package/dist/server/modules/orchestrator/worktrees/worktrees.module.d.ts +2 -0
  221. package/dist/server/modules/orchestrator/worktrees/worktrees.module.js +44 -0
  222. package/dist/server/modules/orchestrator/worktrees/worktrees.module.js.map +1 -0
  223. package/dist/server/modules/orchestrator/worktrees/worktrees.store.d.ts +38 -0
  224. package/dist/server/modules/orchestrator/worktrees/worktrees.store.js +5 -0
  225. package/dist/server/modules/orchestrator/worktrees/worktrees.store.js.map +1 -0
  226. package/dist/server/modules/profiles/dto.d.ts +4 -4
  227. package/dist/server/modules/projects/controllers/projects.controller.d.ts +13 -3
  228. package/dist/server/modules/projects/controllers/projects.controller.js +55 -7
  229. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  230. package/dist/server/modules/projects/projects.module.js +3 -2
  231. package/dist/server/modules/projects/projects.module.js.map +1 -1
  232. package/dist/server/modules/projects/services/main-project-bootstrap.service.d.ts +11 -0
  233. package/dist/server/modules/projects/services/main-project-bootstrap.service.js +76 -0
  234. package/dist/server/modules/projects/services/main-project-bootstrap.service.js.map +1 -0
  235. package/dist/server/modules/projects/services/projects.service.d.ts +8 -0
  236. package/dist/server/modules/projects/services/projects.service.js +115 -37
  237. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  238. package/dist/server/modules/providers/adapters/gemini.adapter.d.ts +1 -1
  239. package/dist/server/modules/providers/adapters/gemini.adapter.js +6 -4
  240. package/dist/server/modules/providers/adapters/gemini.adapter.js.map +1 -1
  241. package/dist/server/modules/providers/controllers/providers.controller.d.ts +3 -0
  242. package/dist/server/modules/providers/controllers/providers.controller.js +28 -0
  243. package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
  244. package/dist/server/modules/providers/providers.module.js +2 -2
  245. package/dist/server/modules/providers/providers.module.js.map +1 -1
  246. package/dist/server/modules/registry/services/unified-template.service.js +6 -18
  247. package/dist/server/modules/registry/services/unified-template.service.js.map +1 -1
  248. package/dist/server/modules/reviews/dtos/review.dto.d.ts +4 -4
  249. package/dist/server/modules/reviews/reviews.module.js +2 -2
  250. package/dist/server/modules/reviews/reviews.module.js.map +1 -1
  251. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.d.ts +3 -0
  252. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js +89 -0
  253. package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js.map +1 -0
  254. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.d.ts +3 -0
  255. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js +60 -0
  256. package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js.map +1 -0
  257. package/dist/server/modules/seeders/services/data-seeder.service.js +4 -0
  258. package/dist/server/modules/seeders/services/data-seeder.service.js.map +1 -1
  259. package/dist/server/modules/sessions/services/sessions.service.d.ts +3 -1
  260. package/dist/server/modules/sessions/services/sessions.service.js +79 -22
  261. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  262. package/dist/server/modules/sessions/sessions.module.js +6 -4
  263. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  264. package/dist/server/modules/sessions/utils/claude-config.d.ts +6 -2
  265. package/dist/server/modules/sessions/utils/claude-config.js +19 -6
  266. package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
  267. package/dist/server/modules/settings/dtos/settings.dto.d.ts +4 -4
  268. package/dist/server/modules/skills/dtos/community-sources.dto.d.ts +2 -2
  269. package/dist/server/modules/skills/dtos/local-sources.dto.d.ts +2 -2
  270. package/dist/server/modules/skills/dtos/skill.dto.d.ts +7 -7
  271. package/dist/server/modules/storage/db/schema.d.ts +811 -0
  272. package/dist/server/modules/storage/db/schema.js +63 -1
  273. package/dist/server/modules/storage/db/schema.js.map +1 -1
  274. package/dist/server/modules/storage/db/sqlite-json.d.ts +2 -0
  275. package/dist/server/modules/storage/db/sqlite-json.js +8 -0
  276. package/dist/server/modules/storage/db/sqlite-json.js.map +1 -0
  277. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +4 -1
  278. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  279. package/dist/server/modules/storage/local/local-storage.service.d.ts +1 -1
  280. package/dist/server/modules/storage/local/local-storage.service.js +19 -2
  281. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  282. package/dist/server/modules/storage/models/domain.models.d.ts +2 -0
  283. package/dist/server/modules/subscribers/dtos/subscriber.dto.d.ts +16 -16
  284. package/dist/server/modules/subscribers/events/event-fields-catalog.js +18 -0
  285. package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
  286. package/dist/server/modules/subscribers/subscribers.module.js +2 -2
  287. package/dist/server/modules/subscribers/subscribers.module.js.map +1 -1
  288. package/dist/server/modules/terminal/gateways/terminal.gateway.js +7 -2
  289. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  290. package/dist/server/modules/terminal/services/tmux.service.d.ts +9 -0
  291. package/dist/server/modules/terminal/services/tmux.service.js +55 -5
  292. package/dist/server/modules/terminal/services/tmux.service.js.map +1 -1
  293. package/dist/server/modules/terminal/terminal.module.js +2 -2
  294. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  295. package/dist/server/modules/watchers/watchers.module.js +2 -2
  296. package/dist/server/modules/watchers/watchers.module.js.map +1 -1
  297. package/dist/server/templates/3-agents-dev.json +662 -0
  298. package/dist/server/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
  299. package/dist/server/test-setup.js +7 -0
  300. package/dist/server/test-setup.js.map +1 -1
  301. package/dist/server/tsconfig.tsbuildinfo +1 -1
  302. package/dist/server/ui/assets/ReviewDetailPage-CZZQtaY7.js +1 -0
  303. package/dist/server/ui/assets/{ReviewsPage-MKT-vv59.js → ReviewsPage-C209GLQG.js} +1 -1
  304. package/dist/server/ui/assets/index-DvRuLfpZ.css +32 -0
  305. package/dist/server/ui/assets/index-Th1FDtKR.js +977 -0
  306. package/dist/server/ui/assets/{useReviewSubscription-Dc58i6Bk.js → useReviewSubscription-Dcabsa78.js} +1 -1
  307. package/dist/server/ui/index.html +2 -2
  308. package/dist/templates/3-agents-dev.json +662 -0
  309. package/dist/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
  310. package/package.json +15 -1
  311. package/dist/server/ui/assets/ReviewDetailPage-BvSckWKj.js +0 -6
  312. package/dist/server/ui/assets/index-BtUq-Qxb.css +0 -32
  313. package/dist/server/ui/assets/index-kTb634Zp.js +0 -945
package/dist/cli.js CHANGED
@@ -10,9 +10,9 @@ const { Command } = require('commander');
10
10
  const getPort = require('get-port');
11
11
  const open = require('open');
12
12
  const { join, dirname, basename } = require('path');
13
- const { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync, openSync } = require('fs');
13
+ const { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync, openSync, realpathSync } = require('fs');
14
14
  const { homedir, platform } = require('os');
15
- const { spawn, execSync } = require('child_process');
15
+ const { spawn, execSync, execFileSync } = require('child_process');
16
16
  const { InteractiveCLI } = require('./lib/interactive-cli');
17
17
  const readline = require('readline');
18
18
 
@@ -96,6 +96,116 @@ function getChangelogBetweenVersions(changelog, fromVersion, toVersion) {
96
96
  return changes;
97
97
  }
98
98
 
99
+ function normalizeCliArgv(argv) {
100
+ const rawArgs = argv.slice(2);
101
+ const hasContainerFlag = rawArgs.includes('--container');
102
+ if (!hasContainerFlag) {
103
+ return argv;
104
+ }
105
+
106
+ const knownCommands = new Set(['start', 'stop', 'help']);
107
+ const hasKnownCommand = rawArgs.some((arg) => knownCommands.has(arg));
108
+ if (hasKnownCommand) {
109
+ return argv;
110
+ }
111
+
112
+ const passthrough = rawArgs.filter((arg) => arg !== '--container');
113
+ return [argv[0], argv[1], 'start', '--container', ...passthrough];
114
+ }
115
+
116
+ const WORKTREE_RUNTIME_TYPES = new Set(['container', 'process']);
117
+
118
+ function normalizeWorktreeRuntimeType(rawRuntimeType) {
119
+ if (typeof rawRuntimeType !== 'string') {
120
+ return null;
121
+ }
122
+
123
+ const normalized = rawRuntimeType.trim().toLowerCase();
124
+ if (!normalized) {
125
+ return null;
126
+ }
127
+
128
+ if (!WORKTREE_RUNTIME_TYPES.has(normalized)) {
129
+ throw new Error(
130
+ `Invalid --worktree-runtime value "${rawRuntimeType}". Expected one of: container, process.`,
131
+ );
132
+ }
133
+
134
+ return normalized;
135
+ }
136
+
137
+ function isWorktreeRuntimeModeEnabled(worktreeRuntimeType) {
138
+ return worktreeRuntimeType === 'container' || worktreeRuntimeType === 'process';
139
+ }
140
+
141
+ /**
142
+ * Detect which global package manager owns the devchain install.
143
+ *
144
+ * @param {string} packageName - The npm package name (e.g. 'devchain-cli')
145
+ * @param {object} [deps] - Dependency-injected functions for testability
146
+ * @returns {{ name: 'npm'|'pnpm', installCmd: string[], sudoInstallCmd: string[]|null, manualCmd: string }|null}
147
+ */
148
+ function detectGlobalPackageManager(packageName, {
149
+ realpathSyncFn = realpathSync,
150
+ execFileSyncFn = execFileSync,
151
+ argvPath = process.argv[1],
152
+ } = {}) {
153
+ try {
154
+ let scriptRealPath;
155
+ try {
156
+ scriptRealPath = realpathSyncFn(argvPath);
157
+ } catch {
158
+ return null;
159
+ }
160
+
161
+ const pms = [
162
+ { name: 'pnpm', rootArgs: ['root', '-g'], installVerb: 'add' },
163
+ { name: 'npm', rootArgs: ['root', '-g'], installVerb: 'install' },
164
+ ];
165
+
166
+ const available = [];
167
+ for (const pm of pms) {
168
+ try {
169
+ execFileSyncFn(pm.name, ['--version'], { stdio: 'ignore' });
170
+ available.push(pm);
171
+ } catch {
172
+ // PM not on PATH
173
+ }
174
+ }
175
+
176
+ if (available.length === 0) return null;
177
+
178
+ const owners = [];
179
+ for (const pm of available) {
180
+ try {
181
+ const globalRoot = execFileSyncFn(pm.name, pm.rootArgs, {
182
+ encoding: 'utf8',
183
+ stdio: ['ignore', 'pipe', 'ignore'],
184
+ }).trim();
185
+ if (globalRoot && scriptRealPath.startsWith(globalRoot)) {
186
+ owners.push(pm);
187
+ }
188
+ } catch {
189
+ // root -g failed
190
+ }
191
+ }
192
+
193
+ // Ambiguous: both match or neither match
194
+ if (owners.length !== 1) return null;
195
+
196
+ const pm = owners[0];
197
+ const installCmd = [pm.name, pm.installVerb, '-g', `${packageName}@latest`];
198
+ const sudoInstallCmd = platform() !== 'win32'
199
+ ? ['sudo', ...installCmd]
200
+ : null;
201
+ const manualCmd = `${pm.name} ${pm.installVerb} -g ${packageName}`;
202
+
203
+ return { name: pm.name, installCmd, sudoInstallCmd, manualCmd };
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
99
209
  async function checkForUpdates(cli, askYesNoFn) {
100
210
  try {
101
211
  const pkg = require('../package.json');
@@ -130,26 +240,35 @@ async function checkForUpdates(cli, askYesNoFn) {
130
240
  const shouldUpdate = await askYesNoFn('Would you like to update now?', true);
131
241
 
132
242
  if (shouldUpdate) {
133
- cli.info('Updating devchain...');
243
+ const pm = detectGlobalPackageManager(packageName);
244
+
245
+ if (!pm) {
246
+ // Cannot determine owning PM — show manual instructions
247
+ cli.warn('Could not detect the package manager used to install devchain.');
248
+ cli.info('Please update manually:');
249
+ cli.info(' npm install -g ' + packageName);
250
+ cli.info(' pnpm add -g ' + packageName);
251
+ return;
252
+ }
253
+
254
+ cli.info(`Updating devchain via ${pm.name}...`);
134
255
  try {
135
- // Try without sudo first (works for nvm/fnm/Windows)
136
- // Use npm install -g instead of update -g (update can remove packages)
137
- execSync(`npm install -g ${packageName}@latest`, { stdio: 'inherit' });
256
+ execFileSync(pm.installCmd[0], pm.installCmd.slice(1), { stdio: 'inherit' });
138
257
  cli.success('Update complete! Please restart devchain.');
139
258
  process.exit(0);
140
259
  } catch (e) {
141
- // On Linux/Mac, might need sudo for system npm
142
- if (platform() !== 'win32') {
260
+ // On Linux/Mac, might need sudo for system installs
261
+ if (pm.sudoInstallCmd) {
143
262
  cli.info('Retrying with sudo...');
144
263
  try {
145
- execSync(`sudo npm install -g ${packageName}@latest`, { stdio: 'inherit' });
264
+ execFileSync(pm.sudoInstallCmd[0], pm.sudoInstallCmd.slice(1), { stdio: 'inherit' });
146
265
  cli.success('Update complete! Please restart devchain.');
147
266
  process.exit(0);
148
267
  } catch (e2) {
149
- cli.error('Update failed. You can manually run: sudo npm install -g ' + packageName);
268
+ cli.error('Update failed. You can manually run: sudo ' + pm.manualCmd);
150
269
  }
151
270
  } else {
152
- cli.error('Update failed. You can manually run: npm install -g ' + packageName);
271
+ cli.error('Update failed. You can manually run: ' + pm.manualCmd);
153
272
  }
154
273
  }
155
274
  }
@@ -181,6 +300,424 @@ function detectInstalledProviders() {
181
300
  return detected; // Map<name, absolutePath>
182
301
  }
183
302
 
303
+ const ORCHESTRATOR_WORKTREE_IMAGE_REPO = 'ghcr.io/twitech-lab/devchain';
304
+ const WORKTREE_IMAGE_BUILD_TIMEOUT_MS = 30 * 60 * 1000;
305
+
306
+ function sleep(ms) {
307
+ return new Promise((resolve) => setTimeout(resolve, ms));
308
+ }
309
+
310
+ function ensureDockerAvailable(execSyncFn = execSync) {
311
+ try {
312
+ execSyncFn('docker info --format "{{.ID}}"', {
313
+ stdio: 'pipe',
314
+ timeout: 10000,
315
+ });
316
+ } catch (_) {
317
+ throw new Error('Container mode requires Docker. Please install Docker and try again.');
318
+ }
319
+ }
320
+
321
+ function isDockerAvailable(execSyncFn = execSync) {
322
+ try {
323
+ ensureDockerAvailable(execSyncFn);
324
+ return true;
325
+ } catch (_) {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ function deriveRepoRootFromGit(execSyncFn = execSync) {
331
+ try {
332
+ const gitTopLevel = execSyncFn('git rev-parse --show-toplevel', {
333
+ encoding: 'utf8',
334
+ stdio: 'pipe',
335
+ timeout: 5000,
336
+ }).trim();
337
+
338
+ if (!gitTopLevel) {
339
+ throw new Error('empty-git-top-level');
340
+ }
341
+
342
+ process.env.REPO_ROOT = gitTopLevel;
343
+ return gitTopLevel;
344
+ } catch (_) {
345
+ throw new Error('Container mode must be run from within a git repository.');
346
+ }
347
+ }
348
+
349
+ function isInsideGitRepo(execSyncFn = execSync) {
350
+ const previousRepoRoot = process.env.REPO_ROOT;
351
+ try {
352
+ deriveRepoRootFromGit(execSyncFn);
353
+ return true;
354
+ } catch (_) {
355
+ return false;
356
+ } finally {
357
+ if (typeof previousRepoRoot === 'string') {
358
+ process.env.REPO_ROOT = previousRepoRoot;
359
+ } else {
360
+ delete process.env.REPO_ROOT;
361
+ }
362
+ }
363
+ }
364
+
365
+ function ensureProjectGitignoreIncludesDevchain(repoRoot) {
366
+ const normalizedRepoRoot = typeof repoRoot === 'string' ? repoRoot.trim() : '';
367
+ if (!normalizedRepoRoot || !existsSync(normalizedRepoRoot)) {
368
+ return;
369
+ }
370
+
371
+ const gitignorePath = join(normalizedRepoRoot, '.gitignore');
372
+
373
+ try {
374
+ const existingContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
375
+ const alreadyIgnored = existingContent.split(/\r?\n/).some((line) => {
376
+ const trimmed = line.trim();
377
+ if (!trimmed || trimmed.startsWith('#')) {
378
+ return false;
379
+ }
380
+ return (
381
+ trimmed === '.devchain/' ||
382
+ trimmed === '/.devchain/' ||
383
+ trimmed === '.devchain' ||
384
+ trimmed === '/.devchain'
385
+ );
386
+ });
387
+
388
+ if (alreadyIgnored) {
389
+ return;
390
+ }
391
+
392
+ const delimiter = existingContent.length > 0 && !existingContent.endsWith('\n') ? '\n' : '';
393
+ writeFileSync(gitignorePath, `${existingContent}${delimiter}.devchain/\n`, 'utf8');
394
+ } catch (error) {
395
+ const message = error instanceof Error ? error.message : String(error);
396
+ console.warn(`Warning: unable to update project .gitignore with .devchain/ (${message})`);
397
+ }
398
+ }
399
+
400
+ function resolveRepoRootForDockerBuild(execSyncFn = execSync) {
401
+ const repoRootFromEnv = typeof process.env.REPO_ROOT === 'string' ? process.env.REPO_ROOT.trim() : '';
402
+ if (repoRootFromEnv) {
403
+ return repoRootFromEnv;
404
+ }
405
+ try {
406
+ return deriveRepoRootFromGit(execSyncFn);
407
+ } catch (_) {
408
+ return join(__dirname, '..');
409
+ }
410
+ }
411
+
412
+ function shouldSkipHostPreflights(enableOrchestration) {
413
+ return Boolean(enableOrchestration);
414
+ }
415
+
416
+ function resolveWorktreeImageFromPackageVersion() {
417
+ const pkg = require('../package.json');
418
+ const version = typeof pkg?.version === 'string' ? pkg.version.trim() : '';
419
+ if (!version) {
420
+ throw new Error('Unable to resolve CLI package version for worktree image provisioning.');
421
+ }
422
+ return `${ORCHESTRATOR_WORKTREE_IMAGE_REPO}:${version}`;
423
+ }
424
+
425
+ function hasWorktreeImageLocally(imageRef, execSyncFn = execSync) {
426
+ try {
427
+ execSyncFn(`docker image inspect ${imageRef}`, {
428
+ stdio: 'pipe',
429
+ timeout: 15000,
430
+ });
431
+ return true;
432
+ } catch (_) {
433
+ return false;
434
+ }
435
+ }
436
+
437
+ function buildWorktreeImage({
438
+ imageRef = resolveWorktreeImageFromPackageVersion(),
439
+ execSyncFn = execSync,
440
+ } = {}) {
441
+ const repoRoot = resolveRepoRootForDockerBuild(execSyncFn);
442
+ const dockerfilePath = join(repoRoot, 'apps', 'local-app', 'Dockerfile');
443
+ console.log(`Building worktree image: ${imageRef}`);
444
+ try {
445
+ execSyncFn(
446
+ `docker build -f "${dockerfilePath}" -t ${imageRef} "${repoRoot}"`,
447
+ {
448
+ stdio: 'inherit',
449
+ timeout: WORKTREE_IMAGE_BUILD_TIMEOUT_MS,
450
+ },
451
+ );
452
+ console.log(`Built worktree image: ${imageRef}`);
453
+ return imageRef;
454
+ } catch (error) {
455
+ const message = error instanceof Error ? error.message : String(error);
456
+ throw new Error(
457
+ [
458
+ `Failed to build worktree image: ${imageRef}`,
459
+ `Reason: ${message}`,
460
+ `Try manually: docker build -f "${dockerfilePath}" -t ${imageRef} "${repoRoot}"`,
461
+ ].join('\n'),
462
+ );
463
+ }
464
+ }
465
+
466
+ function resolveDevchainApiBaseUrlForRestart({
467
+ readPidFileFn = readPidFile,
468
+ isProcessRunningFn = isProcessRunning,
469
+ } = {}) {
470
+ const pidData = readPidFileFn();
471
+ if (!pidData || !Number.isFinite(Number(pidData.pid)) || !Number.isFinite(Number(pidData.port))) {
472
+ throw new Error(
473
+ 'Image was rebuilt, but Devchain is not running. Start container mode first, then retry with --restart.',
474
+ );
475
+ }
476
+
477
+ const pid = Number(pidData.pid);
478
+ const port = Number(pidData.port);
479
+ if (!isProcessRunningFn(pid)) {
480
+ throw new Error(
481
+ 'Image was rebuilt, but Devchain is not running. Start container mode first, then retry with --restart.',
482
+ );
483
+ }
484
+
485
+ return `http://127.0.0.1:${port}`;
486
+ }
487
+
488
+ function normalizeWorktreeListPayload(payload) {
489
+ if (Array.isArray(payload)) {
490
+ return payload;
491
+ }
492
+ if (payload && typeof payload === 'object' && Array.isArray(payload.items)) {
493
+ return payload.items;
494
+ }
495
+ throw new Error('Unexpected response shape from /api/worktrees.');
496
+ }
497
+
498
+ async function restartRunningWorktrees({
499
+ baseUrl,
500
+ fetchFn = fetch,
501
+ } = {}) {
502
+ if (!baseUrl || typeof baseUrl !== 'string') {
503
+ throw new Error('A baseUrl is required to restart running worktrees.');
504
+ }
505
+
506
+ const listRes = await fetchFn(`${baseUrl}/api/worktrees`);
507
+ if (!listRes.ok) {
508
+ throw new Error(`Failed to list worktrees for restart (HTTP ${listRes.status}).`);
509
+ }
510
+
511
+ const worktrees = normalizeWorktreeListPayload(await listRes.json());
512
+ const runningWorktrees = worktrees.filter(
513
+ (worktree) => String(worktree?.status || '').toLowerCase() === 'running',
514
+ );
515
+
516
+ if (runningWorktrees.length === 0) {
517
+ console.log('No running worktrees found. Build completed without restarts.');
518
+ return 0;
519
+ }
520
+
521
+ console.log(`Restarting ${runningWorktrees.length} running worktree(s)...`);
522
+ for (const worktree of runningWorktrees) {
523
+ const worktreeId = typeof worktree?.id === 'string' ? worktree.id.trim() : '';
524
+ const worktreeName =
525
+ typeof worktree?.name === 'string' && worktree.name.trim()
526
+ ? worktree.name.trim()
527
+ : worktreeId || 'unknown';
528
+
529
+ if (!worktreeId) {
530
+ throw new Error('Cannot restart worktree without an id from /api/worktrees response.');
531
+ }
532
+
533
+ console.log(`Stopping worktree "${worktreeName}"...`);
534
+ const stopRes = await fetchFn(`${baseUrl}/api/worktrees/${encodeURIComponent(worktreeId)}/stop`, {
535
+ method: 'POST',
536
+ });
537
+ if (!stopRes.ok) {
538
+ throw new Error(`Failed stopping worktree "${worktreeName}" (HTTP ${stopRes.status}).`);
539
+ }
540
+
541
+ console.log(`Starting worktree "${worktreeName}"...`);
542
+ const startRes = await fetchFn(`${baseUrl}/api/worktrees/${encodeURIComponent(worktreeId)}/start`, {
543
+ method: 'POST',
544
+ });
545
+ if (!startRes.ok) {
546
+ throw new Error(`Failed starting worktree "${worktreeName}" (HTTP ${startRes.status}).`);
547
+ }
548
+ }
549
+
550
+ console.log('Worktree restart complete.');
551
+ return runningWorktrees.length;
552
+ }
553
+
554
+ async function ensureWorktreeImage({
555
+ execSyncFn = execSync,
556
+ onMissing = 'pull',
557
+ } = {}) {
558
+ const existingImageOverride =
559
+ typeof process.env.ORCHESTRATOR_CONTAINER_IMAGE === 'string'
560
+ ? process.env.ORCHESTRATOR_CONTAINER_IMAGE.trim()
561
+ : '';
562
+ if (existingImageOverride) {
563
+ process.env.ORCHESTRATOR_CONTAINER_IMAGE = existingImageOverride;
564
+ console.log(`Using worktree image override: ${existingImageOverride}`);
565
+ return existingImageOverride;
566
+ }
567
+
568
+ const imageRef = resolveWorktreeImageFromPackageVersion();
569
+
570
+ if (hasWorktreeImageLocally(imageRef, execSyncFn)) {
571
+ console.log(`Using local worktree image: ${imageRef}`);
572
+ } else if (onMissing === 'build') {
573
+ console.log(`Local worktree image missing: ${imageRef}`);
574
+ buildWorktreeImage({ imageRef, execSyncFn });
575
+ } else if (onMissing === 'pull') {
576
+ console.log(`Pulling worktree image: ${imageRef}`);
577
+ try {
578
+ execSyncFn(`docker pull ${imageRef}`, {
579
+ stdio: 'inherit',
580
+ timeout: 300000,
581
+ });
582
+ console.log(`Pulled worktree image: ${imageRef}`);
583
+ } catch (error) {
584
+ const message = error instanceof Error ? error.message : String(error);
585
+ throw new Error(
586
+ [
587
+ `Failed to pull worktree image: ${imageRef}`,
588
+ `Reason: ${message}`,
589
+ `Try manually: docker pull ${imageRef}`,
590
+ `Or build locally: docker build -t ${imageRef} -f apps/local-app/Dockerfile .`,
591
+ ].join('\n'),
592
+ );
593
+ }
594
+ } else {
595
+ throw new Error(`Invalid ensureWorktreeImage onMissing strategy: ${onMissing}`);
596
+ }
597
+
598
+ process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
599
+ return imageRef;
600
+ }
601
+
602
+ function ensureWorktreeImageRefFromPackageVersion() {
603
+ const existingImageOverride =
604
+ typeof process.env.ORCHESTRATOR_CONTAINER_IMAGE === 'string'
605
+ ? process.env.ORCHESTRATOR_CONTAINER_IMAGE.trim()
606
+ : '';
607
+ if (existingImageOverride) {
608
+ process.env.ORCHESTRATOR_CONTAINER_IMAGE = existingImageOverride;
609
+ return existingImageOverride;
610
+ }
611
+
612
+ const imageRef = resolveWorktreeImageFromPackageVersion();
613
+ process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
614
+ return imageRef;
615
+ }
616
+
617
+ async function bootstrapContainerMode({
618
+ execSyncFn = execSync,
619
+ } = {}) {
620
+ ensureDockerAvailable(execSyncFn);
621
+ const repoRoot = deriveRepoRootFromGit(execSyncFn);
622
+ ensureProjectGitignoreIncludesDevchain(repoRoot);
623
+ ensureWorktreeImageRefFromPackageVersion();
624
+ }
625
+
626
+ function formatOrchestrationDetectionFailureReason({
627
+ skippedByEnvNormal = false,
628
+ dockerAvailable = false,
629
+ insideGitRepo = false,
630
+ } = {}) {
631
+ if (skippedByEnvNormal) {
632
+ return 'DEVCHAIN_MODE=normal override is active';
633
+ }
634
+
635
+ const missing = [];
636
+ if (!dockerAvailable) {
637
+ missing.push('Docker is unavailable');
638
+ }
639
+ if (!insideGitRepo) {
640
+ missing.push('current directory is not inside a git repository');
641
+ }
642
+
643
+ if (missing.length === 0) {
644
+ return 'orchestration prerequisites are not met';
645
+ }
646
+ if (missing.length === 1) {
647
+ return missing[0];
648
+ }
649
+ return `${missing.slice(0, -1).join(', ')} and ${missing[missing.length - 1]}`;
650
+ }
651
+
652
+ async function resolveStartupOrchestration({
653
+ forceContainer = false,
654
+ env = process.env,
655
+ execSyncFn = execSync,
656
+ bootstrapContainerModeFn = bootstrapContainerMode,
657
+ warnFn = (message) => console.warn(message),
658
+ } = {}) {
659
+ const modeOverride = typeof env.DEVCHAIN_MODE === 'string' ? env.DEVCHAIN_MODE.trim() : '';
660
+ const skippedByEnvNormal = modeOverride.toLowerCase() === 'normal';
661
+ if (skippedByEnvNormal) {
662
+ if (forceContainer) {
663
+ throw new Error(
664
+ `--container requires orchestration, but ${formatOrchestrationDetectionFailureReason({ skippedByEnvNormal })}.`,
665
+ );
666
+ }
667
+ return {
668
+ enableOrchestration: false,
669
+ skippedByEnvNormal,
670
+ dockerAvailable: false,
671
+ insideGitRepo: false,
672
+ };
673
+ }
674
+
675
+ const dockerAvailable = isDockerAvailable(execSyncFn);
676
+ const insideGitRepo = isInsideGitRepo(execSyncFn);
677
+
678
+ if (!dockerAvailable || !insideGitRepo) {
679
+ const reason = formatOrchestrationDetectionFailureReason({
680
+ dockerAvailable,
681
+ insideGitRepo,
682
+ });
683
+ if (forceContainer) {
684
+ throw new Error(`--container requires orchestration, but ${reason}.`);
685
+ }
686
+ return {
687
+ enableOrchestration: false,
688
+ skippedByEnvNormal: false,
689
+ dockerAvailable,
690
+ insideGitRepo,
691
+ };
692
+ }
693
+
694
+ try {
695
+ await bootstrapContainerModeFn({
696
+ execSyncFn,
697
+ });
698
+ env.DEVCHAIN_MODE = 'main';
699
+ return {
700
+ enableOrchestration: true,
701
+ skippedByEnvNormal: false,
702
+ dockerAvailable: true,
703
+ insideGitRepo: true,
704
+ };
705
+ } catch (error) {
706
+ const message = error instanceof Error ? error.message : String(error);
707
+ if (forceContainer) {
708
+ throw new Error(`--container failed to initialize orchestration: ${message}`);
709
+ }
710
+ warnFn(`Container auto-detection found prerequisites but bootstrap failed: ${message}`);
711
+ return {
712
+ enableOrchestration: false,
713
+ skippedByEnvNormal: false,
714
+ dockerAvailable: true,
715
+ insideGitRepo: true,
716
+ bootstrapError: message,
717
+ };
718
+ }
719
+ }
720
+
184
721
  async function ensureProvidersInDb(baseUrl, detected, log) {
185
722
  try {
186
723
  const res = await fetch(`${baseUrl}/api/providers`);
@@ -542,13 +1079,215 @@ function getTmuxErrorMessage(osType) {
542
1079
  }
543
1080
  }
544
1081
 
1082
+ async function runHostPreflightChecks(
1083
+ {
1084
+ enableOrchestration,
1085
+ opts,
1086
+ cli,
1087
+ log,
1088
+ isDetachedChild,
1089
+ },
1090
+ {
1091
+ execSyncFn = execSync,
1092
+ isTmuxInstalledFn = isTmuxInstalled,
1093
+ getOSTypeFn = getOSType,
1094
+ detectInstalledProvidersFn = detectInstalledProviders,
1095
+ ensureClaudeBypassPermissionsFn = ensureClaudeBypassPermissions,
1096
+ platformFn = platform,
1097
+ } = {},
1098
+ ) {
1099
+ const skipHostPreflights = shouldSkipHostPreflights(enableOrchestration);
1100
+
1101
+ // Tmux preflight check
1102
+ const skipTmuxCheck = process.env.DEVCHAIN_SKIP_TMUX_CHECK === '1';
1103
+ if (skipHostPreflights) {
1104
+ if (opts.foreground) {
1105
+ log('info', 'Skipping tmux check in container mode', { skipReason: 'container_mode' });
1106
+ } else {
1107
+ cli.info('Skipping tmux check in container mode');
1108
+ }
1109
+ } else if (skipTmuxCheck) {
1110
+ if (opts.foreground) {
1111
+ log('info', 'Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)', { skipReason: 'env_var' });
1112
+ } else {
1113
+ cli.info('Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)');
1114
+ }
1115
+ } else {
1116
+ const osType = getOSTypeFn();
1117
+
1118
+ if (osType === 'windows') {
1119
+ if (opts.foreground) {
1120
+ log('info', 'Skipping tmux check on Windows', { skipReason: 'windows', platform: 'win32' });
1121
+ } else {
1122
+ cli.info('Skipping tmux check on Windows');
1123
+ }
1124
+ } else {
1125
+ if (!opts.foreground) {
1126
+ cli.step('Checking tmux');
1127
+ }
1128
+
1129
+ if (!isTmuxInstalledFn()) {
1130
+ if (opts.foreground) {
1131
+ log('error', 'tmux not found; aborting startup', { platform: osType, checked: 'which tmux' });
1132
+ } else {
1133
+ cli.stepDone('✗ not found');
1134
+ cli.blank();
1135
+ }
1136
+ console.error('\n' + getTmuxErrorMessage(osType));
1137
+ process.exit(1);
1138
+ }
1139
+
1140
+ try {
1141
+ const tmuxPath = execSyncFn('which tmux', { encoding: 'utf8' }).trim();
1142
+ if (opts.foreground) {
1143
+ log('info', 'tmux found', { tmuxPath });
1144
+ } else {
1145
+ cli.stepDone('✓ found');
1146
+ }
1147
+ } catch {
1148
+ if (opts.foreground) {
1149
+ log('info', 'tmux check passed');
1150
+ } else {
1151
+ cli.stepDone('✓');
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // Provider detection (Linux/macOS only)
1158
+ const skipProviderCheck = process.env.DEVCHAIN_SKIP_PROVIDER_CHECK === '1';
1159
+ const plat = platformFn();
1160
+ if (skipHostPreflights) {
1161
+ if (opts.foreground) {
1162
+ log('info', 'Skipping provider check in container mode', { skipReason: 'container_mode' });
1163
+ } else {
1164
+ cli.info('Skipping provider check in container mode');
1165
+ }
1166
+ } else if (skipProviderCheck) {
1167
+ if (opts.foreground) {
1168
+ log('info', 'Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)', {
1169
+ skipReason: 'env_var',
1170
+ });
1171
+ } else {
1172
+ cli.info('Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)');
1173
+ }
1174
+ } else if (plat === 'win32') {
1175
+ if (opts.foreground) {
1176
+ log('info', 'Skipping provider check on Windows', { skipReason: 'windows' });
1177
+ } else {
1178
+ cli.info('Skipping provider check on Windows');
1179
+ }
1180
+ } else {
1181
+ if (!opts.foreground) {
1182
+ cli.step('Detecting providers');
1183
+ }
1184
+
1185
+ const providersDetected = detectInstalledProvidersFn();
1186
+ if (providersDetected.size === 0) {
1187
+ const guide = [
1188
+ 'No provider binaries detected on PATH. Install at least one provider and retry.',
1189
+ 'Checked: "which codex", "which claude", and "which gemini"',
1190
+ 'Examples:',
1191
+ ' - Install Codex: npm i -g @openai/codex (example) or follow provider docs',
1192
+ ' - Install Claude: npm i -g @anthropic-ai/claude-code (example) or follow provider docs',
1193
+ ' - Install Gemini: npm i -g @google/gemini-cli (example) or follow provider docs',
1194
+ 'Advanced: bypass with DEVCHAIN_SKIP_PROVIDER_CHECK=1',
1195
+ ].join('\n');
1196
+ if (opts.foreground) {
1197
+ log('error', 'No providers found; aborting startup', {
1198
+ checked: ['which codex', 'which claude', 'which gemini'],
1199
+ });
1200
+ } else {
1201
+ cli.stepDone('✗ none found');
1202
+ cli.blank();
1203
+ }
1204
+ console.error('\n' + guide + '\n');
1205
+ process.exit(1);
1206
+ }
1207
+
1208
+ opts.__providersDetected = providersDetected;
1209
+ const providerNames = Array.from(providersDetected.keys());
1210
+
1211
+ if (opts.foreground) {
1212
+ log('info', 'Detected providers', {
1213
+ providers: Array.from(providersDetected.entries()).map(([name, p]) => ({ name, path: p })),
1214
+ });
1215
+ } else {
1216
+ cli.stepDone(`✓ ${providerNames.join(', ')}`);
1217
+ }
1218
+ }
1219
+
1220
+ // Prompt for Claude bypass permissions (parent only - requires stdin)
1221
+ // This runs BEFORE detach since it needs user interaction
1222
+ if (!isDetachedChild && opts.__providersDetected && opts.__providersDetected.has('claude')) {
1223
+ await ensureClaudeBypassPermissionsFn(cli);
1224
+ }
1225
+ }
1226
+
1227
+ function getDevUiConfig(containerMode) {
1228
+ if (containerMode) {
1229
+ return {
1230
+ script: 'dev:ui',
1231
+ startMessage: 'Starting UI (dev mode)...',
1232
+ logLabel: 'UI dev server',
1233
+ url: 'http://127.0.0.1:5175',
1234
+ };
1235
+ }
1236
+
1237
+ return {
1238
+ script: 'dev:ui',
1239
+ startMessage: 'Starting UI (dev mode)...',
1240
+ logLabel: 'UI dev server',
1241
+ url: 'http://127.0.0.1:5175',
1242
+ };
1243
+ }
1244
+
1245
+ function applyContainerModeDefaults(containerMode, opts = {}, env = process.env) {
1246
+ if (!containerMode) {
1247
+ return;
1248
+ }
1249
+
1250
+ const hasExplicitPortEnv = typeof env.PORT === 'string' && env.PORT.trim() !== '';
1251
+ if (!opts.port && !hasExplicitPortEnv) {
1252
+ env.PORT = '3000';
1253
+ }
1254
+ }
1255
+
1256
+ function getPreferredDevApiPort(optsPort, containerMode, env = process.env) {
1257
+ if (optsPort) {
1258
+ return Number(optsPort);
1259
+ }
1260
+ if (containerMode) {
1261
+ return Number(env.PORT || 3000);
1262
+ }
1263
+ return 3000;
1264
+ }
1265
+
1266
+ function getDevModeSpawnConfig({ containerMode, port, env = process.env }) {
1267
+ const ui = getDevUiConfig(containerMode);
1268
+ return {
1269
+ ui,
1270
+ nest: {
1271
+ command: 'pnpm',
1272
+ args: ['--filter', 'local-app', 'dev:api'],
1273
+ env: { ...env, PORT: String(port) },
1274
+ },
1275
+ vite: {
1276
+ command: 'pnpm',
1277
+ args: ['--filter', 'local-app', ui.script],
1278
+ env: { ...env, VITE_API_PORT: String(port) },
1279
+ },
1280
+ };
1281
+ }
1282
+
545
1283
  async function main(argv) {
546
1284
  const program = new Command();
547
1285
  const pkg = require('../package.json');
548
1286
  program
549
1287
  .name('devchain')
550
1288
  .description('Devchain — Local-first AI agent orchestration')
551
- .version(pkg.version);
1289
+ .version(pkg.version)
1290
+ .option('--container', 'Shorthand for "start --container"');
552
1291
 
553
1292
  const startCommand = program
554
1293
  .command('start [args...]')
@@ -559,6 +1298,14 @@ async function main(argv) {
559
1298
  .option('--no-open', 'Do not open a browser window')
560
1299
  .option('--db <path>', 'Path to database directory or file (overrides DB_PATH/DB_FILENAME)')
561
1300
  .option('--project <path>', 'Initial project root path; creates project if missing')
1301
+ .option(
1302
+ '--container',
1303
+ 'Force orchestration startup (errors if Docker or git repository prerequisites are missing)'
1304
+ )
1305
+ .option(
1306
+ '--worktree-runtime <type>',
1307
+ '[internal] Worktree runtime context (container|process); bypasses singleton and interactive startup flows',
1308
+ )
562
1309
  .option(
563
1310
  '--log-level <level>',
564
1311
  'Set log verbosity: error (errors only), warn, info, debug, or trace. ' +
@@ -567,7 +1314,24 @@ async function main(argv) {
567
1314
  )
568
1315
  .option('--dev', 'Development mode with hot reload (spawns nest --watch + vite)')
569
1316
  .option('--internal-detached-child', '[internal] Marker for detached child process')
570
- .action(async (args, opts) => {
1317
+ .action(async (rawArgs, opts) => {
1318
+ const args = Array.isArray(rawArgs) ? [...rawArgs] : [];
1319
+ const usesContainerSubcommand = args[0] === 'container';
1320
+ if (usesContainerSubcommand) {
1321
+ args.shift();
1322
+ }
1323
+ const forceContainer = Boolean(
1324
+ opts.container || program.opts().container || usesContainerSubcommand,
1325
+ );
1326
+ let worktreeRuntimeType = null;
1327
+ try {
1328
+ worktreeRuntimeType = normalizeWorktreeRuntimeType(opts.worktreeRuntime);
1329
+ } catch (error) {
1330
+ console.error(error instanceof Error ? error.message : 'Invalid --worktree-runtime value.');
1331
+ process.exit(1);
1332
+ }
1333
+ const worktreeRuntimeMode = isWorktreeRuntimeModeEnabled(worktreeRuntimeType);
1334
+
571
1335
  // Check if "help" was passed as an argument
572
1336
  if (args && args.length > 0 && args[0] === 'help') {
573
1337
  startCommand.help();
@@ -575,7 +1339,7 @@ async function main(argv) {
575
1339
  }
576
1340
 
577
1341
  // Check if already running (skip for detached child - parent already checked)
578
- if (!opts.internalDetachedChild) {
1342
+ if (!opts.internalDetachedChild && !worktreeRuntimeMode) {
579
1343
  const existingPid = readPidFile();
580
1344
  if (existingPid && isProcessRunning(existingPid.pid)) {
581
1345
  console.error(`Devchain is already running (PID ${existingPid.pid}, port ${existingPid.port})`);
@@ -591,16 +1355,19 @@ async function main(argv) {
591
1355
 
592
1356
  // Normalize defaults for negatable options (Commander may leave undefined)
593
1357
  if (typeof opts.open === 'undefined') {
594
- opts.open = true;
1358
+ opts.open = worktreeRuntimeMode ? false : forceContainer ? false : true;
1359
+ }
1360
+ if (worktreeRuntimeMode) {
1361
+ opts.open = false;
595
1362
  }
596
1363
  // Detached mode by default, unless foreground is explicitly requested
597
1364
  // Don't detach again if we're already the detached child process
598
- const isDetachedChild = opts.internalDetachedChild;
599
- const shouldDetach = opts.foreground !== true && !isDetachedChild;
1365
+ const isDetachedChild = Boolean(opts.internalDetachedChild);
1366
+ const shouldDetach = opts.foreground !== true && !isDetachedChild && !worktreeRuntimeMode;
600
1367
 
601
1368
  // Initialize interactive CLI (user-friendly output unless in foreground mode)
602
1369
  const cli = new InteractiveCLI({
603
- interactive: !opts.foreground && !isDetachedChild,
1370
+ interactive: !opts.foreground && !isDetachedChild && !worktreeRuntimeMode,
604
1371
  colors: true,
605
1372
  spinners: true
606
1373
  });
@@ -610,141 +1377,70 @@ async function main(argv) {
610
1377
  console.log(JSON.stringify(entry));
611
1378
  };
612
1379
 
1380
+ let enableOrchestration = false;
1381
+ if (worktreeRuntimeMode) {
1382
+ enableOrchestration = worktreeRuntimeType === 'container';
1383
+ } else {
1384
+ try {
1385
+ const orchestrationResolution = await resolveStartupOrchestration({
1386
+ forceContainer,
1387
+ env: process.env,
1388
+ warnFn: (message) => {
1389
+ if (opts.foreground) {
1390
+ log('warn', message);
1391
+ } else {
1392
+ cli.warn(message);
1393
+ }
1394
+ },
1395
+ });
1396
+ enableOrchestration = orchestrationResolution.enableOrchestration;
1397
+ } catch (error) {
1398
+ console.error(
1399
+ error instanceof Error
1400
+ ? error.message
1401
+ : 'Failed to resolve orchestration startup prerequisites.',
1402
+ );
1403
+ process.exit(1);
1404
+ }
1405
+ }
1406
+
1407
+ applyContainerModeDefaults(enableOrchestration, opts, process.env);
1408
+
613
1409
  // Check for updates (parent process only, skip in dev mode)
614
- if (!isDetachedChild && !opts.dev) {
1410
+ if (!isDetachedChild && !opts.dev && !worktreeRuntimeMode) {
615
1411
  await checkForUpdates(cli, askYesNo);
616
1412
  }
617
1413
 
618
1414
  // Show startup banner (interactive mode only)
619
- if (!opts.foreground) {
1415
+ if (!opts.foreground && !worktreeRuntimeMode) {
620
1416
  cli.blank();
621
1417
  cli.info('Starting Devchain...');
622
1418
  cli.blank();
623
1419
  }
624
1420
 
625
- // Tmux preflight check
626
- const skipTmuxCheck = process.env.DEVCHAIN_SKIP_TMUX_CHECK === '1';
627
- if (skipTmuxCheck) {
628
- if (opts.foreground) {
629
- log('info', 'Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)', { skipReason: 'env_var' });
630
- } else {
631
- cli.info('Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)');
632
- }
633
- } else {
634
- const osType = getOSType();
635
-
636
- if (osType === 'windows') {
637
- if (opts.foreground) {
638
- log('info', 'Skipping tmux check on Windows', { skipReason: 'windows', platform: 'win32' });
639
- } else {
640
- cli.info('Skipping tmux check on Windows');
641
- }
642
- } else {
643
- // Interactive: show step
644
- if (!opts.foreground) {
645
- cli.step('Checking tmux');
646
- }
647
-
648
- // Check if tmux is installed
649
- if (!isTmuxInstalled()) {
650
- if (opts.foreground) {
651
- log('error', 'tmux not found; aborting startup', { platform: osType, checked: 'which tmux' });
652
- } else {
653
- cli.stepDone('✗ not found');
654
- cli.blank();
655
- }
656
- console.error('\n' + getTmuxErrorMessage(osType));
657
- process.exit(1);
658
- }
659
-
660
- // tmux found, log success
661
- try {
662
- const tmuxPath = execSync('which tmux', { encoding: 'utf8' }).trim();
663
- if (opts.foreground) {
664
- log('info', 'tmux found', { tmuxPath });
665
- } else {
666
- cli.stepDone('✓ found');
667
- }
668
- } catch {
669
- // Shouldn't happen since we just checked, but handle gracefully
670
- if (opts.foreground) {
671
- log('info', 'tmux check passed');
672
- } else {
673
- cli.stepDone('✓');
674
- }
675
- }
676
- }
1421
+ if (!worktreeRuntimeMode) {
1422
+ await runHostPreflightChecks({
1423
+ enableOrchestration,
1424
+ opts,
1425
+ cli,
1426
+ log,
1427
+ isDetachedChild,
1428
+ });
677
1429
  }
678
1430
 
679
- // Provider detection (Linux/macOS only)
680
- const skipProviderCheck = process.env.DEVCHAIN_SKIP_PROVIDER_CHECK === '1';
681
- const plat = platform();
682
- if (skipProviderCheck) {
683
- if (opts.foreground) {
684
- log('info', 'Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)', {
685
- skipReason: 'env_var',
686
- });
687
- } else {
688
- cli.info('Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)');
689
- }
690
- } else if (plat === 'win32') {
691
- if (opts.foreground) {
692
- log('info', 'Skipping provider check on Windows', { skipReason: 'windows' });
693
- } else {
694
- cli.info('Skipping provider check on Windows');
695
- }
696
- } else {
697
- // Interactive: show step
698
- if (!opts.foreground) {
699
- cli.step('Detecting providers');
700
- }
701
-
702
- const providersDetected = detectInstalledProviders();
703
- if (providersDetected.size === 0) {
704
- // Provide guidance and exit prior to server startup
705
- const guide = [
706
- 'No provider binaries detected on PATH. Install at least one provider and retry.',
707
- 'Checked: "which codex", "which claude", and "which gemini"',
708
- 'Examples:',
709
- ' - Install Codex: npm i -g @openai/codex (example) or follow provider docs',
710
- ' - Install Claude: npm i -g @anthropic-ai/claude-code (example) or follow provider docs',
711
- ' - Install Gemini: npm i -g @google/gemini-cli (example) or follow provider docs',
712
- 'Advanced: bypass with DEVCHAIN_SKIP_PROVIDER_CHECK=1',
713
- ].join('\n');
714
- if (opts.foreground) {
715
- log('error', 'No providers found; aborting startup', {
716
- checked: ['which codex', 'which claude', 'which gemini'],
717
- });
718
- } else {
719
- cli.stepDone('✗ none found');
720
- cli.blank();
721
- }
722
- console.error('\n' + guide + '\n');
723
- process.exit(1);
724
- }
725
-
726
- // stash on opts for later ensure step after server ready
727
- opts.__providersDetected = providersDetected;
728
- const providerNames = Array.from(providersDetected.keys());
729
-
730
- if (opts.foreground) {
731
- log('info', 'Detected providers', {
732
- providers: Array.from(providersDetected.entries()).map(([name, p]) => ({ name, path: p })),
733
- });
734
- } else {
735
- cli.stepDone(`✓ ${providerNames.join(', ')}`);
736
- }
737
- }
1431
+ const preferPort = getPreferredDevApiPort(opts.port, enableOrchestration, process.env);
738
1432
 
739
- // Prompt for Claude bypass permissions (parent only - requires stdin)
740
- // This runs BEFORE detach since it needs user interaction
741
- if (!isDetachedChild && opts.__providersDetected && opts.__providersDetected.has('claude')) {
742
- await ensureClaudeBypassPermissions(cli);
1433
+ // In worktree runtime mode, bind strictly to the requested port.
1434
+ // getPort() silently picks a different port when the requested one is unavailable,
1435
+ // which causes the parent orchestrator to talk to the wrong instance.
1436
+ // Let NestJS fail fast with EADDRINUSE instead of silently rebinding.
1437
+ let port;
1438
+ if (worktreeRuntimeMode && preferPort) {
1439
+ port = preferPort;
1440
+ } else {
1441
+ port = await getPort({ port: preferPort });
743
1442
  }
744
1443
 
745
- const preferPort = opts.port ? Number(opts.port) : 3000;
746
- const port = await getPort({ port: preferPort });
747
-
748
1444
  // === DETACH POINT ===
749
1445
  // All interactive prompts are done. Now spawn the detached child if needed.
750
1446
  if (shouldDetach) {
@@ -831,9 +1527,15 @@ async function main(argv) {
831
1527
  };
832
1528
 
833
1529
  // Spawn NestJS in watch mode (detached to create process group)
834
- const nestProcess = spawn('pnpm', ['--filter', 'local-app', 'dev:api'], {
1530
+ const devSpawnConfig = getDevModeSpawnConfig({
1531
+ containerMode: enableOrchestration,
1532
+ port,
1533
+ env: process.env,
1534
+ });
1535
+
1536
+ const nestProcess = spawn(devSpawnConfig.nest.command, devSpawnConfig.nest.args, {
835
1537
  stdio: 'inherit',
836
- env: { ...process.env, PORT: String(port) },
1538
+ env: devSpawnConfig.nest.env,
837
1539
  shell: true,
838
1540
  detached: platform() !== 'win32', // Create process group on Unix
839
1541
  });
@@ -853,7 +1555,12 @@ async function main(argv) {
853
1555
  cli.info(`API docs: ${baseUrl}/api/docs`);
854
1556
 
855
1557
  // Ensure provider rows exist
856
- if (opts.__providersDetected && opts.__providersDetected.size > 0) {
1558
+ if (
1559
+ !worktreeRuntimeMode
1560
+ && !enableOrchestration
1561
+ && opts.__providersDetected
1562
+ && opts.__providersDetected.size > 0
1563
+ ) {
857
1564
  await ensureProvidersInDb(baseUrl, opts.__providersDetected, log);
858
1565
  }
859
1566
 
@@ -863,36 +1570,44 @@ async function main(argv) {
863
1570
  : process.cwd();
864
1571
 
865
1572
  // Validate MCP for all providers
866
- await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
1573
+ if (!worktreeRuntimeMode && !enableOrchestration) {
1574
+ await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
1575
+ }
867
1576
 
868
1577
  // Note: Claude bypass prompt already handled before server start
869
1578
 
1579
+ const devUiConfig = devSpawnConfig.ui;
1580
+
870
1581
  cli.blank();
871
- cli.info('Starting UI (dev mode)...');
1582
+ cli.info(devUiConfig.startMessage);
872
1583
 
873
1584
  // Spawn Vite for UI hot reload (pass API port, detached to create process group)
874
- const viteProcess = spawn('pnpm', ['--filter', 'local-app', 'dev:ui'], {
1585
+ const viteProcess = spawn(devSpawnConfig.vite.command, devSpawnConfig.vite.args, {
875
1586
  stdio: 'inherit',
876
- env: { ...process.env, VITE_API_PORT: String(port) },
1587
+ env: devSpawnConfig.vite.env,
877
1588
  shell: true,
878
1589
  detached: platform() !== 'win32', // Create process group on Unix
879
1590
  });
880
1591
 
881
1592
  cli.blank();
882
1593
  cli.success('Development servers running');
883
- cli.info(`UI: http://127.0.0.1:5175 (Vite dev server)`);
1594
+ cli.info(`${devUiConfig.logLabel}: ${devUiConfig.url}`);
884
1595
  cli.info(`API: ${baseUrl}`);
885
1596
  cli.blank();
886
1597
 
887
- // Write PID file
888
- writePidFile(port);
1598
+ // Write PID file for top-level runtime only
1599
+ if (!worktreeRuntimeMode) {
1600
+ writePidFile(port);
1601
+ }
889
1602
 
890
1603
  // Handle cleanup on exit - kill entire process groups
891
1604
  const cleanup = () => {
892
1605
  console.log('\nShutting down development servers...');
893
1606
  killProcessGroup(nestProcess);
894
1607
  killProcessGroup(viteProcess);
895
- removePidFile();
1608
+ if (!worktreeRuntimeMode) {
1609
+ removePidFile();
1610
+ }
896
1611
  process.exit(0);
897
1612
  };
898
1613
 
@@ -960,7 +1675,12 @@ async function main(argv) {
960
1675
  }
961
1676
 
962
1677
  // Ensure provider rows exist (idempotent) before opening UI
963
- if (opts.__providersDetected && opts.__providersDetected.size > 0) {
1678
+ if (
1679
+ !worktreeRuntimeMode
1680
+ && !enableOrchestration
1681
+ && opts.__providersDetected
1682
+ && opts.__providersDetected.size > 0
1683
+ ) {
964
1684
  await ensureProvidersInDb(baseUrl, opts.__providersDetected, log);
965
1685
  }
966
1686
 
@@ -970,7 +1690,9 @@ async function main(argv) {
970
1690
  : process.cwd();
971
1691
 
972
1692
  // Validate MCP for all providers (with project context)
973
- await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
1693
+ if (!worktreeRuntimeMode && !enableOrchestration) {
1694
+ await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
1695
+ }
974
1696
 
975
1697
  // Note: Claude bypass prompt already handled before server start (in parent process for detach mode)
976
1698
 
@@ -1020,13 +1742,15 @@ async function main(argv) {
1020
1742
  cli.blank();
1021
1743
  }
1022
1744
 
1023
- // Write PID file for stop command
1024
- writePidFile(port);
1745
+ if (!worktreeRuntimeMode) {
1746
+ // Write PID file for stop command
1747
+ writePidFile(port);
1025
1748
 
1026
- // Clean up PID file on exit (main.ts handles SIGINT/SIGTERM and graceful shutdown)
1027
- process.on('exit', () => {
1028
- removePidFile();
1029
- });
1749
+ // Clean up PID file on exit (main.ts handles SIGINT/SIGTERM and graceful shutdown)
1750
+ process.on('exit', () => {
1751
+ removePidFile();
1752
+ });
1753
+ }
1030
1754
 
1031
1755
  if (opts.open) {
1032
1756
  if (!opts.foreground) {
@@ -1056,66 +1780,133 @@ async function main(argv) {
1056
1780
  }
1057
1781
  });
1058
1782
 
1783
+ program
1784
+ .command('dev:image')
1785
+ .description('Rebuild worktree image for Docker-enabled development')
1786
+ .option('--restart', 'After rebuild, restart running worktrees via orchestrator API')
1787
+ .action(async (opts) => {
1788
+ try {
1789
+ ensureDockerAvailable();
1790
+ const imageRef = buildWorktreeImage();
1791
+ process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
1792
+
1793
+ if (!opts.restart) {
1794
+ console.log('Build complete. Running worktrees were not restarted.');
1795
+ process.exit(0);
1796
+ }
1797
+
1798
+ const baseUrl = resolveDevchainApiBaseUrlForRestart();
1799
+ const ready = await waitForHealth(`${baseUrl}/health`, {
1800
+ timeoutMs: 5000,
1801
+ intervalMs: 250,
1802
+ });
1803
+ if (!ready) {
1804
+ throw new Error(
1805
+ `Image was rebuilt, but orchestrator is not reachable at ${baseUrl}. Start it and retry --restart.`,
1806
+ );
1807
+ }
1808
+
1809
+ await restartRunningWorktrees({ baseUrl });
1810
+ process.exit(0);
1811
+ } catch (error) {
1812
+ console.error(
1813
+ error instanceof Error
1814
+ ? error.message
1815
+ : 'Failed to rebuild worktree image.',
1816
+ );
1817
+ process.exit(1);
1818
+ }
1819
+ });
1820
+
1059
1821
  program
1060
1822
  .command('stop')
1061
1823
  .description('Stop the running Devchain instance')
1062
- .action(() => {
1824
+ .action(async () => {
1063
1825
  const pidData = readPidFile();
1064
1826
 
1065
1827
  if (!pidData) {
1066
1828
  console.log('No running Devchain instance found.');
1067
1829
  process.exit(1);
1068
- }
1069
-
1070
- const { pid, port } = pidData;
1830
+ } else {
1831
+ const { pid, port } = pidData;
1071
1832
 
1072
- if (!isProcessRunning(pid)) {
1073
- console.log(`Devchain process (PID ${pid}) is not running. Cleaning up stale PID file.`);
1074
- removePidFile();
1075
- process.exit(1);
1076
- }
1833
+ if (!isProcessRunning(pid)) {
1834
+ console.log(`Devchain process (PID ${pid}) is not running. Cleaning up stale PID file.`);
1835
+ removePidFile();
1836
+ process.exit(1);
1837
+ } else {
1838
+ console.log(`Stopping Devchain (PID ${pid}, port ${port})...`);
1077
1839
 
1078
- console.log(`Stopping Devchain (PID ${pid}, port ${port})...`);
1840
+ try {
1841
+ process.kill(pid, 'SIGTERM');
1842
+ } catch (err) {
1843
+ console.error('Failed to stop Devchain:', err.message);
1844
+ process.exit(1);
1845
+ }
1079
1846
 
1080
- try {
1081
- process.kill(pid, 'SIGTERM');
1082
-
1083
- // Wait a bit for graceful shutdown
1084
- let attempts = 0;
1085
- const checkInterval = setInterval(() => {
1086
- attempts++;
1087
- if (!isProcessRunning(pid)) {
1088
- clearInterval(checkInterval);
1089
- removePidFile();
1090
- console.log('Devchain stopped successfully.');
1091
- process.exit(0);
1847
+ let stopped = false;
1848
+ for (let attempts = 0; attempts <= 20; attempts += 1) {
1849
+ if (!isProcessRunning(pid)) {
1850
+ stopped = true;
1851
+ break;
1852
+ }
1853
+ await sleep(100);
1092
1854
  }
1093
1855
 
1094
- if (attempts > 20) {
1095
- // After 2 seconds, force kill
1096
- clearInterval(checkInterval);
1856
+ if (!stopped) {
1097
1857
  console.log('Graceful shutdown timed out, forcing...');
1098
1858
  try {
1099
1859
  process.kill(pid, 'SIGKILL');
1100
- removePidFile();
1101
- console.log('Devchain stopped (forced).');
1102
1860
  } catch (err) {
1103
1861
  console.error('Failed to stop Devchain:', err.message);
1104
1862
  process.exit(1);
1105
1863
  }
1106
- process.exit(0);
1107
1864
  }
1108
- }, 100);
1109
- } catch (err) {
1110
- console.error('Failed to stop Devchain:', err.message);
1111
- process.exit(1);
1865
+
1866
+ removePidFile();
1867
+ console.log(stopped ? 'Devchain stopped successfully.' : 'Devchain stopped (forced).');
1868
+ }
1112
1869
  }
1870
+
1871
+ process.exit(0);
1113
1872
  });
1114
1873
 
1115
- await program.parseAsync(argv);
1874
+ await program.parseAsync(normalizeCliArgv(argv));
1875
+ }
1876
+
1877
+ if (require.main === module) {
1878
+ main(process.argv).catch((err) => {
1879
+ console.error('Fatal error:', err);
1880
+ process.exit(1);
1881
+ });
1116
1882
  }
1117
1883
 
1118
- main(process.argv).catch((err) => {
1119
- console.error('Fatal error:', err);
1120
- process.exit(1);
1121
- });
1884
+ module.exports = {
1885
+ main,
1886
+ normalizeCliArgv,
1887
+ __test__: {
1888
+ ensureDockerAvailable,
1889
+ isDockerAvailable,
1890
+ deriveRepoRootFromGit,
1891
+ isInsideGitRepo,
1892
+ ensureProjectGitignoreIncludesDevchain,
1893
+ shouldSkipHostPreflights,
1894
+ runHostPreflightChecks,
1895
+ getDevUiConfig,
1896
+ applyContainerModeDefaults,
1897
+ getPreferredDevApiPort,
1898
+ getDevModeSpawnConfig,
1899
+ hasWorktreeImageLocally,
1900
+ buildWorktreeImage,
1901
+ resolveDevchainApiBaseUrlForRestart,
1902
+ restartRunningWorktrees,
1903
+ ensureWorktreeImage,
1904
+ bootstrapContainerMode,
1905
+ ensureWorktreeImageRefFromPackageVersion,
1906
+ formatOrchestrationDetectionFailureReason,
1907
+ resolveStartupOrchestration,
1908
+ normalizeWorktreeRuntimeType,
1909
+ isWorktreeRuntimeModeEnabled,
1910
+ detectGlobalPackageManager,
1911
+ },
1912
+ };