conductor-oss 0.2.17 → 0.2.19

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 (211) hide show
  1. package/dist/commands/dashboard.js +2 -2
  2. package/dist/commands/start.d.ts +3 -6
  3. package/dist/commands/start.d.ts.map +1 -1
  4. package/dist/commands/start.js +389 -220
  5. package/dist/commands/start.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +4 -6
  8. package/dist/index.js.map +1 -1
  9. package/native/conductor +0 -0
  10. package/node_modules/@conductor-oss/core/dist/session-manager.js +1 -1
  11. package/node_modules/@conductor-oss/core/dist/session-manager.js.map +1 -1
  12. package/node_modules/@conductor-oss/plugin-agent-amp/package.json +1 -1
  13. package/node_modules/@conductor-oss/plugin-agent-ccr/package.json +1 -1
  14. package/node_modules/@conductor-oss/plugin-agent-claude-code/package.json +1 -1
  15. package/node_modules/@conductor-oss/plugin-agent-codex/package.json +1 -1
  16. package/node_modules/@conductor-oss/plugin-agent-cursor-cli/package.json +1 -1
  17. package/node_modules/@conductor-oss/plugin-agent-droid/package.json +1 -1
  18. package/node_modules/@conductor-oss/plugin-agent-gemini/package.json +1 -1
  19. package/node_modules/@conductor-oss/plugin-agent-github-copilot/package.json +1 -1
  20. package/node_modules/@conductor-oss/plugin-agent-opencode/package.json +1 -1
  21. package/node_modules/@conductor-oss/plugin-agent-qwen-code/package.json +1 -1
  22. package/node_modules/@conductor-oss/plugin-mcp-server/package.json +1 -1
  23. package/node_modules/@conductor-oss/plugin-notifier-desktop/package.json +1 -1
  24. package/node_modules/@conductor-oss/plugin-notifier-discord/package.json +1 -1
  25. package/node_modules/@conductor-oss/plugin-runtime-tmux/package.json +1 -1
  26. package/node_modules/@conductor-oss/plugin-scm-github/package.json +1 -1
  27. package/node_modules/@conductor-oss/plugin-terminal-web/package.json +1 -1
  28. package/node_modules/@conductor-oss/plugin-tracker-github/package.json +1 -1
  29. package/node_modules/@conductor-oss/plugin-workspace-worktree/package.json +1 -1
  30. package/package.json +21 -22
  31. package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
  32. package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +0 -1
  33. package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
  34. package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
  35. package/web/.next/standalone/packages/web/.next/routes-manifest.json +0 -6
  36. package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  37. package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  38. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  39. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
  44. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  45. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
  47. package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +3 -3
  48. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  49. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  51. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  54. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -1
  55. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -1
  56. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -1
  57. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -1
  58. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -1
  59. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -1
  60. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -1
  61. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -1
  62. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -1
  63. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -1
  64. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -1
  65. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -1
  66. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -1
  67. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -1
  68. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -1
  69. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -1
  70. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js.nft.json +1 -1
  71. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -1
  72. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -1
  73. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.nft.json +1 -1
  74. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  75. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -1
  76. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +1 -1
  77. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  78. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  79. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
  80. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
  81. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -1
  82. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -1
  83. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +4 -4
  84. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/web/.next/standalone/packages/web/.next/server/app/index.html +1 -1
  86. package/web/.next/standalone/packages/web/.next/server/app/index.rsc +4 -4
  87. package/web/.next/standalone/packages/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  88. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  89. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  90. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  91. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  92. package/web/.next/standalone/packages/web/.next/server/app/page/react-loadable-manifest.json +1 -1
  93. package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
  94. package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  95. package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  96. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
  97. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  98. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  99. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
  100. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
  101. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
  102. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
  103. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
  104. package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
  105. package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +0 -1
  106. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1ed2e6c1._.js +1 -1
  107. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3d6b30a3._.js +1 -1
  108. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a0b6570d._.js +3 -0
  109. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0e4dc4f7.js +2 -1
  110. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__2814b563._.js → [root-of-the-server]__1c826f12._.js} +2 -2
  111. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__6622b514._.js +1 -1
  112. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__869d9ac0._.js +1 -1
  113. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__9dc23e5a._.js +1 -1
  114. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__b388693f._.js +1 -1
  115. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  116. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{_b0abbdd9._.js → _20a4007d._.js} +2 -2
  117. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
  118. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
  119. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b6d31783._.js +1 -1
  120. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{_0973acf3._.js → _b88bcf2c._.js} +2 -2
  121. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  122. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  123. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_270cb834._.js +1 -1
  124. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_@clerk_nextjs_dist_esm_app-router_ae92e1b6._.js → node_modules_@clerk_nextjs_dist_esm_app-router_2985ec6c._.js} +2 -2
  125. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_ad33a435._.js +3 -0
  126. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_f2ebd7a9._.js +1 -1
  127. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_79316445._.js +1 -1
  128. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_a078c137._.js +1 -1
  129. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_app_page_tsx_cd282e82._.js +1 -1
  130. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{packages_web_src_components_f2c9e753._.js → packages_web_src_components_6cec00fe._.js} +1 -1
  131. package/web/.next/standalone/packages/web/.next/server/functions-config-manifest.json +1 -2
  132. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  133. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  134. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  135. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  136. package/web/.next/standalone/packages/web/.next/static/chunks/062888342200567f.js +1 -0
  137. package/web/.next/standalone/packages/web/.next/static/chunks/695b7cb206c6dadd.js +1 -0
  138. package/web/.next/standalone/packages/web/.next/static/chunks/6b43741a27171ff7.js +1 -0
  139. package/web/.next/standalone/packages/web/.next/static/chunks/8221b78965a50858.js +1 -0
  140. package/web/.next/standalone/packages/web/.next/static/chunks/858f7ae5c23d7b37.js +1 -0
  141. package/web/.next/standalone/packages/web/.next/static/chunks/8de84e208e201d72.css +3 -0
  142. package/web/.next/standalone/packages/web/.next/static/chunks/{9785347bf1d88302.js → ad82e3dcd5fe1a50.js} +3 -3
  143. package/web/.next/standalone/packages/web/.next/static/chunks/c4ea57fb949fb623.js +1 -0
  144. package/web/.next/standalone/packages/web/.next/static/chunks/{4c566fd1e4a92935.js → d9d05e7b540400af.js} +1 -1
  145. package/web/.next/{static/chunks/719697e99b51d55b.js → standalone/packages/web/.next/static/chunks/f5d9ad0f62ede339.js} +1 -1
  146. package/web/.next/standalone/packages/web/src/app/page.tsx +3 -4555
  147. package/web/.next/standalone/packages/web/src/app/sessions/[id]/page.tsx +2 -115
  148. package/web/.next/standalone/packages/web/src/components/layout/AppShell.tsx +62 -2
  149. package/web/.next/standalone/packages/web/src/components/layout/TopBar.tsx +19 -19
  150. package/web/.next/standalone/packages/web/src/components/layout/WorkspaceSidebarPanel.tsx +68 -10
  151. package/web/.next/standalone/packages/web/src/features/dashboard/DashboardClient.tsx +4587 -0
  152. package/web/.next/standalone/packages/web/src/features/dashboard/components/WorkspaceOverview.tsx +296 -0
  153. package/web/.next/standalone/packages/web/src/features/sessions/SessionPageClient.tsx +135 -0
  154. package/web/.next/standalone/packages/web/src/hooks/useSessionFeed.ts +17 -13
  155. package/web/.next/standalone/packages/web/src/hooks/useSessions.ts +37 -7
  156. package/web/.next/standalone/packages/web/src/proxy.ts +1 -0
  157. package/web/.next/static/chunks/062888342200567f.js +1 -0
  158. package/web/.next/static/chunks/695b7cb206c6dadd.js +1 -0
  159. package/web/.next/static/chunks/6b43741a27171ff7.js +1 -0
  160. package/web/.next/static/chunks/8221b78965a50858.js +1 -0
  161. package/web/.next/static/chunks/858f7ae5c23d7b37.js +1 -0
  162. package/web/.next/static/chunks/8de84e208e201d72.css +3 -0
  163. package/web/.next/static/chunks/{9785347bf1d88302.js → ad82e3dcd5fe1a50.js} +3 -3
  164. package/web/.next/static/chunks/c4ea57fb949fb623.js +1 -0
  165. package/web/.next/static/chunks/{4c566fd1e4a92935.js → d9d05e7b540400af.js} +1 -1
  166. package/web/.next/{standalone/packages/web/.next/static/chunks/719697e99b51d55b.js → static/chunks/f5d9ad0f62ede339.js} +1 -1
  167. package/dist/commands/watch.d.ts +0 -12
  168. package/dist/commands/watch.d.ts.map +0 -1
  169. package/dist/commands/watch.js +0 -84
  170. package/dist/commands/watch.js.map +0 -1
  171. package/dist/commands/webhook.d.ts +0 -12
  172. package/dist/commands/webhook.d.ts.map +0 -1
  173. package/dist/commands/webhook.js +0 -59
  174. package/dist/commands/webhook.js.map +0 -1
  175. package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts +0 -28
  176. package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts.map +0 -1
  177. package/node_modules/@conductor-oss/plugin-webhook/dist/index.js +0 -295
  178. package/node_modules/@conductor-oss/plugin-webhook/dist/index.js.map +0 -1
  179. package/node_modules/@conductor-oss/plugin-webhook/package.json +0 -11
  180. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/app-paths-manifest.json +0 -3
  181. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/build-manifest.json +0 -11
  182. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/server-reference-manifest.json +0 -4
  183. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +0 -8
  184. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.map +0 -5
  185. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +0 -1
  186. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route_client-reference-manifest.js +0 -2
  187. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7633d324._.js +0 -4
  188. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bf8faac8._.js +0 -4
  189. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_agents_route_actions_29063d1a.js +0 -3
  190. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_39a27288._.js +0 -3
  191. package/web/.next/standalone/packages/web/.next/static/chunks/1867dba46fcc022e.js +0 -1
  192. package/web/.next/standalone/packages/web/.next/static/chunks/995a5af4e8529901.js +0 -1
  193. package/web/.next/standalone/packages/web/.next/static/chunks/ab8ef6c7dfc78082.js +0 -1
  194. package/web/.next/standalone/packages/web/.next/static/chunks/b5e2d1ef92e508a0.js +0 -1
  195. package/web/.next/standalone/packages/web/.next/static/chunks/dc65fd7512517f7d.js +0 -1
  196. package/web/.next/standalone/packages/web/.next/static/chunks/e8283780c26eaa91.js +0 -1
  197. package/web/.next/standalone/packages/web/.next/static/chunks/fe557eb4039d8a8d.css +0 -3
  198. package/web/.next/standalone/packages/web/src/app/api/agents/route.ts +0 -223
  199. package/web/.next/static/chunks/1867dba46fcc022e.js +0 -1
  200. package/web/.next/static/chunks/995a5af4e8529901.js +0 -1
  201. package/web/.next/static/chunks/ab8ef6c7dfc78082.js +0 -1
  202. package/web/.next/static/chunks/b5e2d1ef92e508a0.js +0 -1
  203. package/web/.next/static/chunks/dc65fd7512517f7d.js +0 -1
  204. package/web/.next/static/chunks/e8283780c26eaa91.js +0 -1
  205. package/web/.next/static/chunks/fe557eb4039d8a8d.css +0 -3
  206. /package/web/.next/standalone/packages/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_buildManifest.js +0 -0
  207. /package/web/.next/standalone/packages/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_clientMiddlewareManifest.json +0 -0
  208. /package/web/.next/standalone/packages/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_ssgManifest.js +0 -0
  209. /package/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_buildManifest.js +0 -0
  210. /package/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_clientMiddlewareManifest.json +0 -0
  211. /package/web/.next/static/{U7zhuWy5hxiSO9ZXtDOfi → UGPSYGs8x8jahGnVodP_c}/_ssgManifest.js +0 -0
@@ -1,22 +1,19 @@
1
1
  /**
2
2
  * `co start`
3
3
  *
4
- * Starts the lifecycle manager and web dashboard.
5
- * Runs in the foreground -- designed for LaunchAgent / systemd usage.
6
- *
7
- * The lifecycle manager polls sessions periodically, advancing their
8
- * state machine (checking CI, reviews, merging, sending reactions, etc.).
9
- * The web dashboard provides a browser UI for monitoring and interaction.
4
+ * Starts the Rust backend and web dashboard in the foreground.
5
+ * The JS launcher is intentionally thin: it resolves paths, launches
6
+ * the Rust backend, and wires the frontend to it.
10
7
  */
11
8
  import { spawn, spawnSync } from "node:child_process";
12
9
  import { randomBytes } from "node:crypto";
13
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
11
  import { homedir } from "node:os";
15
- import { dirname, join, resolve } from "node:path";
12
+ import { basename, dirname, join, resolve } from "node:path";
16
13
  import chalk from "chalk";
17
14
  import ora from "ora";
15
+ import { parse as parseYaml } from "yaml";
18
16
  import { buildConductorBoard, buildConductorYaml } from "@conductor-oss/core";
19
- import { createServices, loadConfig } from "../services.js";
20
17
  function commandExists(command) {
21
18
  const checker = process.platform === "win32" ? "where" : "which";
22
19
  const result = spawnSync(checker, [command], { stdio: "ignore" });
@@ -46,14 +43,43 @@ function resolveBuiltinRemoteAuth(enabled) {
46
43
  sessionSecret,
47
44
  };
48
45
  }
46
+ function asObject(value) {
47
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
48
+ return {};
49
+ }
50
+ return value;
51
+ }
52
+ function asTrimmedString(value) {
53
+ if (typeof value !== "string")
54
+ return null;
55
+ const trimmed = value.trim();
56
+ return trimmed.length > 0 ? trimmed : null;
57
+ }
58
+ function asBoolean(value) {
59
+ return value === true;
60
+ }
61
+ function coercePort(value, fallback) {
62
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) {
63
+ return value;
64
+ }
65
+ if (typeof value === "string") {
66
+ const parsed = parseInt(value.trim(), 10);
67
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
68
+ return parsed;
69
+ }
70
+ }
71
+ return fallback;
72
+ }
49
73
  function resolveTrustedHeaderAuth(config) {
74
+ const access = asObject(config["access"]);
75
+ const trustedHeaders = asObject(access["trustedHeaders"]);
50
76
  return {
51
- enabled: config.access?.trustedHeaders?.enabled === true,
52
- provider: config.access?.trustedHeaders?.provider === "generic" ? "generic" : "cloudflare-access",
53
- emailHeader: config.access?.trustedHeaders?.emailHeader?.trim() || "Cf-Access-Authenticated-User-Email",
54
- jwtHeader: config.access?.trustedHeaders?.jwtHeader?.trim() || "Cf-Access-Jwt-Assertion",
55
- teamDomain: config.access?.trustedHeaders?.teamDomain?.trim() || null,
56
- audience: config.access?.trustedHeaders?.audience?.trim() || null,
77
+ enabled: asBoolean(trustedHeaders["enabled"]),
78
+ provider: trustedHeaders["provider"] === "generic" ? "generic" : "cloudflare-access",
79
+ emailHeader: asTrimmedString(trustedHeaders["emailHeader"]) || "Cf-Access-Authenticated-User-Email",
80
+ jwtHeader: asTrimmedString(trustedHeaders["jwtHeader"]) || "Cf-Access-Jwt-Assertion",
81
+ teamDomain: asTrimmedString(trustedHeaders["teamDomain"]),
82
+ audience: asTrimmedString(trustedHeaders["audience"]),
57
83
  };
58
84
  }
59
85
  export function extractCloudflareTunnelUrl(output) {
@@ -162,13 +188,40 @@ async function waitForDashboard(url, timeoutMs = 15_000) {
162
188
  catch {
163
189
  // Dashboard is still starting.
164
190
  }
165
- await new Promise((resolve) => setTimeout(resolve, 500));
191
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
166
192
  }
167
193
  return false;
168
194
  }
169
195
  function getDefaultLauncherWorkspace() {
170
196
  return resolve(homedir(), ".openclaw", "workspace");
171
197
  }
198
+ function resolveWorkspacePathHint(configHint) {
199
+ if (!configHint) {
200
+ return getDefaultLauncherWorkspace();
201
+ }
202
+ const resolved = resolve(configHint);
203
+ if (existsSync(resolved) && basename(resolved).match(/^conductor\.ya?ml$/i)) {
204
+ return dirname(resolved);
205
+ }
206
+ return resolved;
207
+ }
208
+ function findConfigFile(startDir) {
209
+ const baseDir = resolveWorkspacePathHint(startDir);
210
+ let currentDir = baseDir;
211
+ for (;;) {
212
+ for (const filename of ["conductor.yaml", "conductor.yml"]) {
213
+ const candidate = join(currentDir, filename);
214
+ if (existsSync(candidate)) {
215
+ return candidate;
216
+ }
217
+ }
218
+ const parentDir = dirname(currentDir);
219
+ if (parentDir === currentDir) {
220
+ return null;
221
+ }
222
+ currentDir = parentDir;
223
+ }
224
+ }
172
225
  function ensureDashboardBootstrapWorkspace() {
173
226
  const workspacePath = getDefaultLauncherWorkspace();
174
227
  const configPath = join(workspacePath, "conductor.yaml");
@@ -190,55 +243,223 @@ function ensureDashboardBootstrapWorkspace() {
190
243
  }
191
244
  return { workspacePath, configPath };
192
245
  }
246
+ function loadLauncherSettings(configHint) {
247
+ const explicitConfigPath = configHint && basename(configHint).match(/^conductor\.ya?ml$/i)
248
+ ? resolve(configHint)
249
+ : null;
250
+ const configPath = explicitConfigPath ?? findConfigFile(configHint ?? undefined);
251
+ if (!configPath) {
252
+ const bootstrap = ensureDashboardBootstrapWorkspace();
253
+ return {
254
+ workspacePath: bootstrap.workspacePath,
255
+ configPath: bootstrap.configPath,
256
+ dashboardPort: 4747,
257
+ backendPort: 4748,
258
+ access: {
259
+ requireAuth: false,
260
+ defaultRole: "operator",
261
+ trustedHeaders: resolveTrustedHeaderAuth({}),
262
+ },
263
+ };
264
+ }
265
+ const parsed = parseYaml(readFileSync(configPath, "utf8"));
266
+ const config = asObject(parsed);
267
+ const access = asObject(config["access"]);
268
+ const server = asObject(config["server"]);
269
+ return {
270
+ workspacePath: dirname(configPath),
271
+ configPath,
272
+ dashboardPort: 4747,
273
+ backendPort: coercePort(server["port"], coercePort(config["port"], 4748)),
274
+ access: {
275
+ requireAuth: asBoolean(access["requireAuth"]),
276
+ defaultRole: asTrimmedString(access["defaultRole"]),
277
+ trustedHeaders: resolveTrustedHeaderAuth(config),
278
+ },
279
+ };
280
+ }
281
+ function resolveBackendPort(cliValue, configuredPort) {
282
+ const raw = cliValue?.trim() || process.env["CONDUCTOR_BACKEND_PORT"]?.trim();
283
+ if (!raw)
284
+ return configuredPort;
285
+ return coercePort(raw, configuredPort);
286
+ }
287
+ function resolveFrontendPort(cliValue, configuredPort) {
288
+ const raw = cliValue?.trim() || process.env["PORT"]?.trim();
289
+ if (!raw)
290
+ return configuredPort;
291
+ return coercePort(raw, configuredPort);
292
+ }
293
+ function resolveRepoCargoRoot(workspacePath) {
294
+ const candidate = resolve(workspacePath);
295
+ if (existsSync(join(candidate, "Cargo.toml"))
296
+ && existsSync(join(candidate, "crates", "conductor-cli", "Cargo.toml"))) {
297
+ return candidate;
298
+ }
299
+ return null;
300
+ }
301
+ function resolveBundledRustBinary() {
302
+ const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
303
+ const cliDir = new URL(".", import.meta.url).pathname;
304
+ const candidates = [
305
+ resolve(cliDir, "..", "..", "native", binaryName),
306
+ resolve(cliDir, "..", "..", "..", "native", binaryName),
307
+ ];
308
+ for (const candidate of candidates) {
309
+ if (existsSync(candidate)) {
310
+ return candidate;
311
+ }
312
+ }
313
+ return null;
314
+ }
315
+ function resolveRustBackendLaunch(workspacePath, configPath, backendPort) {
316
+ const bundledBinary = resolveBundledRustBinary();
317
+ if (bundledBinary) {
318
+ return {
319
+ cmd: bundledBinary,
320
+ args: [
321
+ "--workspace",
322
+ workspacePath,
323
+ "--config",
324
+ configPath,
325
+ "start",
326
+ "--host",
327
+ "127.0.0.1",
328
+ "--port",
329
+ String(backendPort),
330
+ ],
331
+ cwd: workspacePath,
332
+ label: "bundled Rust backend",
333
+ };
334
+ }
335
+ const repoCargoRoot = resolveRepoCargoRoot(workspacePath);
336
+ if (!repoCargoRoot) {
337
+ return null;
338
+ }
339
+ const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
340
+ const prebuiltCandidates = [
341
+ join(repoCargoRoot, "target", "release", binaryName),
342
+ join(repoCargoRoot, "target", "debug", binaryName),
343
+ ];
344
+ for (const candidate of prebuiltCandidates) {
345
+ if (existsSync(candidate)) {
346
+ return {
347
+ cmd: candidate,
348
+ args: [
349
+ "--workspace",
350
+ workspacePath,
351
+ "--config",
352
+ configPath,
353
+ "start",
354
+ "--host",
355
+ "127.0.0.1",
356
+ "--port",
357
+ String(backendPort),
358
+ ],
359
+ cwd: repoCargoRoot,
360
+ label: "prebuilt Rust backend",
361
+ };
362
+ }
363
+ }
364
+ return {
365
+ cmd: "cargo",
366
+ args: [
367
+ "run",
368
+ "-p",
369
+ "conductor-cli",
370
+ "--",
371
+ "--workspace",
372
+ workspacePath,
373
+ "--config",
374
+ configPath,
375
+ "start",
376
+ "--host",
377
+ "127.0.0.1",
378
+ "--port",
379
+ String(backendPort),
380
+ ],
381
+ cwd: repoCargoRoot,
382
+ label: "cargo-run Rust backend",
383
+ };
384
+ }
385
+ async function waitForHttpService(url, timeoutMs = 15_000) {
386
+ const startedAt = Date.now();
387
+ while (Date.now() - startedAt < timeoutMs) {
388
+ try {
389
+ const response = await fetch(url, { redirect: "manual" });
390
+ if (response.ok || response.status < 500) {
391
+ return true;
392
+ }
393
+ }
394
+ catch {
395
+ // Service is still starting.
396
+ }
397
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
398
+ }
399
+ return false;
400
+ }
401
+ async function killStalePortListener(port) {
402
+ try {
403
+ const { execSync } = await import("node:child_process");
404
+ const pids = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" }).trim();
405
+ if (!pids) {
406
+ return;
407
+ }
408
+ for (const pid of pids.split("\n").filter(Boolean)) {
409
+ if (pid !== String(process.pid)) {
410
+ process.kill(Number(pid), "SIGTERM");
411
+ console.log(`[port] Killed stale process ${pid} on port ${port}`);
412
+ }
413
+ }
414
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
415
+ }
416
+ catch {
417
+ // no stale process — expected
418
+ }
419
+ }
420
+ function resolveDashboardWebMode(mode) {
421
+ switch (mode?.trim().toLowerCase()) {
422
+ case "dev":
423
+ return "dev";
424
+ case "production":
425
+ case "prod":
426
+ return "production";
427
+ case "standalone":
428
+ return "standalone";
429
+ default:
430
+ return "auto";
431
+ }
432
+ }
193
433
  export function registerStart(program) {
194
434
  program
195
435
  .command("start")
196
- .description("Start lifecycle manager + board watcher + web dashboard (foreground)")
436
+ .description("Start the Rust backend and web dashboard (foreground)")
197
437
  .option("--no-dashboard", "Skip starting the web dashboard")
198
- .option("--no-watcher", "Skip starting the board watcher")
438
+ .option("--no-watcher", "Deprecated. Rust backend startup no longer uses the JS watcher")
199
439
  .option("--open", "Open the dashboard in your default browser")
200
440
  .option("--tunnel", "Expose the dashboard on a free public Cloudflare Quick Tunnel")
201
441
  .option("--host <host>", "Dashboard bind host. Defaults to 127.0.0.1 for local-only access")
202
442
  .option("-p, --port <port>", "Dashboard port override")
203
- .option("-w, --workspace <path>", "Obsidian workspace path")
443
+ .option("--no-backend", "Do not launch a separate local Rust backend")
444
+ .option("--backend-port <port>", "Rust backend port override (default: from config or 4748)")
445
+ .option("-w, --workspace <path>", "Workspace path or conductor.yaml path")
204
446
  .action(async (opts) => {
205
447
  try {
206
- const explicitWorkspaceHint = opts.workspace ?? process.env["CONDUCTOR_WORKSPACE"];
207
- let config;
208
- if (explicitWorkspaceHint) {
209
- config = await loadConfig(explicitWorkspaceHint);
210
- }
211
- else {
212
- try {
213
- config = await loadConfig();
214
- }
215
- catch (err) {
216
- const message = err instanceof Error ? err.message : String(err);
217
- if (!/No conductor\.ya?ml found/i.test(message)) {
218
- throw err;
219
- }
220
- const bootstrap = ensureDashboardBootstrapWorkspace();
221
- process.env["CONDUCTOR_WORKSPACE"] = bootstrap.workspacePath;
222
- process.env["CO_CONFIG_PATH"] = bootstrap.configPath;
223
- config = await loadConfig(bootstrap.workspacePath);
224
- }
225
- }
226
- const workspacePath = opts.workspace
227
- ?? process.env["CONDUCTOR_WORKSPACE"]
228
- ?? (config.configPath ? dirname(config.configPath) : `${process.env["HOME"]}/.conductor/workspace`);
229
- if (!process.env["CONDUCTOR_WORKSPACE"]) {
230
- process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
231
- }
232
- if (!process.env["CO_CONFIG_PATH"] && config.configPath) {
233
- process.env["CO_CONFIG_PATH"] = config.configPath;
234
- }
235
- const { sessionManager, registry } = await createServices(config);
236
- const supportedAgents = registry.list("agent").map((agent) => agent.name);
237
- // Mutable ref — set after boardWatcher is created, used by lifecycle callback
238
- let boardWatcherRef = null;
239
- const port = opts.port ? parseInt(opts.port, 10) : (config.port ?? 3000);
448
+ const configHint = opts.workspace
449
+ || process.env["CO_CONFIG_PATH"]?.trim()
450
+ || process.env["CONDUCTOR_WORKSPACE"]
451
+ || null;
452
+ const settings = loadLauncherSettings(configHint);
453
+ const workspacePath = settings.workspacePath;
454
+ const configPath = settings.configPath;
455
+ const dashboardPort = resolveFrontendPort(opts.port, settings.dashboardPort);
456
+ const backendPort = resolveBackendPort(opts.backendPort, settings.backendPort);
457
+ const explicitBackendUrl = process.env["CONDUCTOR_BACKEND_URL"]?.trim() || null;
458
+ const bindHost = opts.host?.trim() || "127.0.0.1";
240
459
  const shutdownTasks = [];
241
460
  let isShuttingDown = false;
461
+ process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
462
+ process.env["CO_CONFIG_PATH"] = configPath;
242
463
  const requestShutdown = () => {
243
464
  void (async () => {
244
465
  if (isShuttingDown)
@@ -248,8 +469,8 @@ export function registerStart(program) {
248
469
  try {
249
470
  await task();
250
471
  }
251
- catch (err) {
252
- console.error(err);
472
+ catch (error) {
473
+ console.error(error);
253
474
  }
254
475
  }
255
476
  process.exit(0);
@@ -262,100 +483,77 @@ export function registerStart(program) {
262
483
  console.log(chalk.bold.cyan(" Conductor -- Starting"));
263
484
  console.log(chalk.dim(line));
264
485
  console.log();
265
- // ---- Start lifecycle manager ----
266
- const spinner = ora("Starting lifecycle manager").start();
267
- const core = await import("@conductor-oss/core");
268
- core.syncWorkspaceSupportFiles(config, {
269
- workspacePath,
270
- agentNames: supportedAgents,
271
- });
272
- // Startup config sync: regenerate drifted project-local conductor.yaml mirrors
273
- if (typeof core.startupConfigSync === "function") {
274
- const syncResult = core.startupConfigSync(config);
275
- if (syncResult.fixed > 0) {
276
- console.log(chalk.dim(` Synced ${syncResult.fixed} project-local config(s) from canonical`));
277
- }
486
+ if (opts.watcher === false) {
487
+ console.log(chalk.dim(" JS watcher flag ignored: runtime ownership has moved to the Rust backend path."));
278
488
  }
279
- if (typeof core.createLifecycleManager !== "function") {
280
- spinner.warn("Lifecycle manager not yet implemented in @conductor-oss/core");
281
- }
282
- else {
283
- const lifecycle = core.createLifecycleManager({
284
- config,
285
- sessionManager,
286
- onStatusChange: (sessionId, newStatus, projectId) => {
287
- console.log(`[lifecycle] Status change: ${sessionId} ${newStatus} (${projectId})`);
288
- // Trigger immediate board sync
289
- boardWatcherRef?.updateNow();
290
- },
291
- });
292
- lifecycle.start(10_000); // Poll every 10s (was 30s)
293
- spinner.succeed("Lifecycle manager running");
294
- // Graceful shutdown
295
- shutdownTasks.push(() => {
296
- console.log(chalk.dim("\nShutting down lifecycle manager..."));
297
- lifecycle.stop();
298
- });
299
- }
300
- // ---- Start board watcher ----
301
- if (opts.watcher !== false) {
302
- const watchSpinner = ora("Starting board watcher").start();
303
- try {
304
- const boardPatternsOrConfig = config.boards?.length ? config.boards : config;
305
- const boards = core.discoverBoards(workspacePath, boardPatternsOrConfig);
306
- if (boards.length === 0) {
307
- watchSpinner.warn("No CONDUCTOR.md boards found");
308
- }
309
- else {
310
- const boardProjectMap = core.buildBoardProjectMap(boards, config);
311
- const boardWatcher = core.createBoardWatcher({
312
- config,
313
- sessionManager,
314
- agentNames: supportedAgents,
315
- boardPaths: boards,
316
- boardProjectMap,
317
- pollIntervalMs: 5000,
318
- workspacePath,
319
- onDispatch: (projectId, sessionId, task) => {
320
- console.log(`[board-watcher] Dispatched ${sessionId} -> ${projectId}: "${task}"`);
489
+ // ---- Start Rust backend ----
490
+ let backendProcess = null;
491
+ const shouldLaunchBackend = opts.backend !== false && !explicitBackendUrl;
492
+ const backendUrl = explicitBackendUrl ?? (shouldLaunchBackend ? `http://127.0.0.1:${backendPort}` : null);
493
+ if (shouldLaunchBackend) {
494
+ const backendSpinner = ora(`Starting Rust backend on http://127.0.0.1:${backendPort}`).start();
495
+ const launch = resolveRustBackendLaunch(workspacePath, configPath, backendPort);
496
+ if (!launch) {
497
+ backendSpinner.warn("Rust backend binary was not found. Build or package the Rust backend first.");
498
+ }
499
+ else {
500
+ try {
501
+ await killStalePortListener(backendPort);
502
+ backendProcess = spawn(launch.cmd, launch.args, {
503
+ cwd: launch.cwd,
504
+ stdio: "inherit",
505
+ detached: false,
506
+ env: {
507
+ ...process.env,
321
508
  },
322
509
  });
323
- boardWatcher.start();
324
- boardWatcherRef = boardWatcher;
325
- shutdownTasks.push(() => boardWatcher.stop());
326
- watchSpinner.succeed(`Board watcher running (${boards.length} boards)`);
510
+ backendProcess.on("error", () => {
511
+ backendSpinner.warn("Rust backend failed to start.");
512
+ });
513
+ const backendReady = await waitForHttpService(`${backendUrl}/api/health`);
514
+ if (backendReady) {
515
+ backendSpinner.succeed(`Rust backend running on ${backendUrl} (${launch.label})`);
516
+ }
517
+ else {
518
+ backendSpinner.warn(`Rust backend did not become ready at ${backendUrl} in time.`);
519
+ }
520
+ shutdownTasks.push(() => {
521
+ if (backendProcess && backendProcess.exitCode === null) {
522
+ backendProcess.kill("SIGTERM");
523
+ }
524
+ });
525
+ }
526
+ catch (error) {
527
+ backendSpinner.warn(`Rust backend failed: ${error}`);
327
528
  }
328
- }
329
- catch (err) {
330
- watchSpinner.warn(`Board watcher failed: ${err}`);
331
529
  }
332
530
  }
531
+ else if (explicitBackendUrl) {
532
+ console.log(chalk.dim(` Backend: using existing Rust backend at ${explicitBackendUrl}`));
533
+ }
534
+ else {
535
+ console.log(chalk.yellow(" Backend: not launched; frontend API requests will fail without CONDUCTOR_BACKEND_URL."));
536
+ }
333
537
  // ---- Start web dashboard ----
334
538
  let dashboardProcess = null;
335
539
  let publicDashboardUrl = null;
336
540
  let unlockDashboardUrl = null;
337
- const bindHost = opts.host?.trim() || "127.0.0.1";
338
541
  const externalAccessRequested = opts.tunnel === true || !isLoopbackHost(bindHost);
339
542
  const clerkConfigured = Boolean(process.env["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"] && process.env["CLERK_SECRET_KEY"]);
340
- const trustedHeaderAuth = resolveTrustedHeaderAuth(config);
543
+ const trustedHeaderAuth = settings.access.trustedHeaders;
341
544
  const builtinRemoteAuth = resolveBuiltinRemoteAuth(externalAccessRequested && !clerkConfigured);
342
545
  if (opts.dashboard !== false) {
343
546
  const dashSpinner = ora("Starting web dashboard").start();
344
547
  try {
345
548
  const cliDir = new URL(".", import.meta.url).pathname;
346
- const { dirname, join, resolve } = await import("node:path");
347
- const { cpSync, existsSync, mkdirSync } = await import("node:fs");
348
- // Search order:
349
- // 1. Standalone build inside CLI package (npm install -g)
350
- // 2. Sibling packages/web in monorepo dev setup
351
- // 3. config.configPath-relative monorepo root
549
+ const { cpSync, readdirSync, statSync } = await import("node:fs");
352
550
  let webDir = null;
353
551
  const candidates = [
354
- resolve(cliDir, "..", "web"), // npm: cli/dist/../web
355
- resolve(cliDir, "..", "..", "..", "web"), // npm: cli/dist/../../web
356
- resolve(cliDir, "..", "..", "web"), // monorepo: packages/cli/dist/../../web = packages/web
357
- config.configPath ? resolve(config.configPath, "..", "packages", "web") : null,
358
- ].filter(Boolean);
552
+ resolve(cliDir, "..", "web"),
553
+ resolve(cliDir, "..", "..", "..", "web"),
554
+ resolve(cliDir, "..", "..", "web"),
555
+ resolve(dirname(configPath), "packages", "web"),
556
+ ];
359
557
  for (const candidate of candidates) {
360
558
  if (existsSync(join(candidate, "package.json"))) {
361
559
  webDir = candidate;
@@ -366,52 +564,49 @@ export function registerStart(program) {
366
564
  dashSpinner.warn("Dashboard not found. Run: pnpm --filter @conductor-oss/web build");
367
565
  return;
368
566
  }
369
- // Kill any stale process holding the dashboard port (prevents EADDRINUSE on restart)
370
- try {
371
- const { execSync } = await import("node:child_process");
372
- const pids = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" }).trim();
373
- if (pids) {
374
- for (const pid of pids.split("\n").filter(Boolean)) {
375
- if (pid !== String(process.pid)) {
376
- process.kill(Number(pid), "SIGTERM");
377
- console.log(`[dashboard] Killed stale process ${pid} on port ${port}`);
378
- }
379
- }
380
- await new Promise(r => setTimeout(r, 1000));
381
- }
382
- }
383
- catch { /* no stale process — expected */ }
384
- // Prefer standalone build (output: standalone in next.config), then production, then dev
385
- // Find standalone server.js (location varies by monorepo nesting)
386
- const { readdirSync, statSync: fsStat } = await import("node:fs");
567
+ await killStalePortListener(dashboardPort);
568
+ const webMode = resolveDashboardWebMode(process.env["CONDUCTOR_WEB_MODE"]);
569
+ const isSourceCheckout = existsSync(join(webDir, "src", "app", "page.tsx"))
570
+ && existsSync(join(webDir, "next.config.ts"));
571
+ const preferDevServer = webMode === "dev" || (webMode === "auto" && isSourceCheckout);
387
572
  const standaloneDir = join(webDir, ".next", "standalone");
573
+ const hasNextBuild = existsSync(join(webDir, ".next"));
388
574
  let standaloneServer = null;
389
575
  const searchQueue = [standaloneDir];
390
- for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth++) {
576
+ for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth += 1) {
391
577
  const nextQueue = [];
392
- for (const d of searchQueue) {
393
- const candidate = join(d, "server.js");
578
+ for (const currentDir of searchQueue) {
579
+ const candidate = join(currentDir, "server.js");
394
580
  if (existsSync(candidate)) {
395
581
  standaloneServer = candidate;
396
582
  break;
397
583
  }
398
584
  try {
399
- for (const entry of readdirSync(d)) {
400
- const full = join(d, String(entry));
401
- if (fsStat(full).isDirectory() && entry !== "node_modules") {
585
+ for (const entry of readdirSync(currentDir)) {
586
+ const full = join(currentDir, String(entry));
587
+ if (statSync(full).isDirectory() && entry !== "node_modules") {
402
588
  nextQueue.push(full);
403
589
  }
404
590
  }
405
591
  }
406
- catch { /* ignore */ }
592
+ catch {
593
+ // ignore
594
+ }
407
595
  }
408
596
  searchQueue.splice(0, searchQueue.length, ...nextQueue);
409
597
  }
410
- const hasNextBuild = existsSync(join(webDir, ".next"));
411
598
  let cmd;
412
599
  let args;
413
600
  let dashboardCwd = webDir;
414
- if (standaloneServer) {
601
+ if (preferDevServer) {
602
+ cmd = "pnpm";
603
+ args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
604
+ }
605
+ else if (webMode === "production" && hasNextBuild) {
606
+ cmd = "pnpm";
607
+ args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
608
+ }
609
+ else if (standaloneServer) {
415
610
  const standaloneAppDir = dirname(standaloneServer);
416
611
  const standaloneStaticDir = join(standaloneAppDir, ".next", "static");
417
612
  const sourceStaticDir = join(webDir, ".next", "static");
@@ -429,27 +624,12 @@ export function registerStart(program) {
429
624
  dashboardCwd = standaloneDir;
430
625
  }
431
626
  else if (hasNextBuild) {
432
- // Use pnpm run start (next start) — reliable, serves static assets correctly
433
627
  cmd = "pnpm";
434
- args = [
435
- "run",
436
- "start",
437
- "--hostname",
438
- bindHost,
439
- "--port",
440
- String(port),
441
- ];
628
+ args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
442
629
  }
443
630
  else {
444
631
  cmd = "pnpm";
445
- args = [
446
- "run",
447
- "dev",
448
- "--hostname",
449
- bindHost,
450
- "--port",
451
- String(port),
452
- ];
632
+ args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
453
633
  }
454
634
  dashboardProcess = spawn(cmd, args, {
455
635
  cwd: dashboardCwd,
@@ -457,10 +637,16 @@ export function registerStart(program) {
457
637
  detached: false,
458
638
  env: {
459
639
  ...process.env,
460
- PORT: String(port),
640
+ PORT: String(dashboardPort),
461
641
  HOSTNAME: bindHost,
462
642
  CONDUCTOR_WORKSPACE: workspacePath,
463
- CO_CONFIG_PATH: config.configPath,
643
+ CO_CONFIG_PATH: configPath,
644
+ ...(backendUrl
645
+ ? {
646
+ CONDUCTOR_BACKEND_URL: backendUrl,
647
+ CONDUCTOR_BACKEND_PORT: String(backendPort),
648
+ }
649
+ : {}),
464
650
  ...(builtinRemoteAuth
465
651
  ? {
466
652
  CONDUCTOR_REMOTE_ACCESS_TOKEN: builtinRemoteAuth.accessToken,
@@ -481,25 +667,21 @@ export function registerStart(program) {
481
667
  : {}),
482
668
  }
483
669
  : {}),
484
- ...(config.access?.requireAuth
485
- ? {
486
- CONDUCTOR_REQUIRE_AUTH: "true",
487
- }
670
+ ...(settings.access.requireAuth
671
+ ? { CONDUCTOR_REQUIRE_AUTH: "true" }
488
672
  : {}),
489
- ...(config.access?.defaultRole
490
- ? {
491
- CONDUCTOR_ACCESS_DEFAULT_ROLE: config.access.defaultRole,
492
- }
673
+ ...(settings.access.defaultRole
674
+ ? { CONDUCTOR_ACCESS_DEFAULT_ROLE: settings.access.defaultRole }
493
675
  : {}),
494
676
  },
495
677
  });
496
678
  dashboardProcess.on("error", () => {
497
679
  dashSpinner.warn("Dashboard failed to start. Try: cd packages/web && pnpm build");
498
680
  });
499
- const dashboardInternalUrl = `http://127.0.0.1:${port}`;
681
+ const dashboardInternalUrl = `http://127.0.0.1:${dashboardPort}`;
500
682
  const dashboardUrl = isLoopbackHost(bindHost)
501
- ? `http://localhost:${port}`
502
- : `http://${bindHost}:${port}`;
683
+ ? `http://localhost:${dashboardPort}`
684
+ : `http://${bindHost}:${dashboardPort}`;
503
685
  if (builtinRemoteAuth) {
504
686
  unlockDashboardUrl = buildRemoteUnlockUrl(dashboardUrl, builtinRemoteAuth.accessToken);
505
687
  }
@@ -518,7 +700,6 @@ export function registerStart(program) {
518
700
  }
519
701
  });
520
702
  publicDashboardUrl = await tunnel.url;
521
- config.dashboardUrl = publicDashboardUrl;
522
703
  if (builtinRemoteAuth) {
523
704
  unlockDashboardUrl = buildRemoteUnlockUrl(publicDashboardUrl, builtinRemoteAuth.accessToken);
524
705
  }
@@ -546,33 +727,22 @@ export function registerStart(program) {
546
727
  });
547
728
  }
548
729
  }
549
- catch {
550
- dashSpinner.warn("Could not start dashboard.");
551
- }
552
- }
553
- // ---- Start webhook server (if enabled in config) ----
554
- if (config.webhook?.enabled) {
555
- const webhookSpinner = ora("Starting webhook server").start();
556
- try {
557
- const { createWebhookServer } = await import("@conductor-oss/plugin-webhook");
558
- const webhookServer = createWebhookServer(config, config.webhook);
559
- await webhookServer.start();
560
- shutdownTasks.push(() => webhookServer.stop());
561
- webhookSpinner.succeed(`Webhook server running on port ${config.webhook.port}`);
562
- }
563
- catch (err) {
564
- webhookSpinner.warn(`Webhook server failed to start: ${err}`);
730
+ catch (error) {
731
+ dashSpinner.warn(`Could not start dashboard: ${error}`);
565
732
  }
566
733
  }
567
734
  // ---- Summary ----
568
735
  console.log();
569
736
  console.log(chalk.bold.green("Conductor is running."));
570
- console.log(chalk.dim(` Config: ${config.configPath}`));
737
+ console.log(chalk.dim(` Config: ${configPath}`));
571
738
  if (opts.dashboard !== false) {
572
739
  const dashboardSummaryUrl = isLoopbackHost(bindHost)
573
- ? `http://localhost:${port}`
574
- : `http://${bindHost}:${port}`;
740
+ ? `http://localhost:${dashboardPort}`
741
+ : `http://${bindHost}:${dashboardPort}`;
575
742
  console.log(chalk.dim(` Dashboard: ${dashboardSummaryUrl}`));
743
+ if (backendUrl) {
744
+ console.log(chalk.dim(` Backend: ${backendUrl}`));
745
+ }
576
746
  if (publicDashboardUrl) {
577
747
  console.log(chalk.dim(` Public: ${publicDashboardUrl}`));
578
748
  }
@@ -594,30 +764,29 @@ export function registerStart(program) {
594
764
  }
595
765
  }
596
766
  }
597
- if (opts.watcher !== false) {
598
- console.log(chalk.dim(" Watcher: Obsidian CONDUCTOR.md boards"));
599
- }
600
- if (config.webhook?.enabled) {
601
- console.log(chalk.dim(` Webhook: http://localhost:${config.webhook.port}/api/webhook`));
602
- }
767
+ console.log(chalk.dim(" Runtime: Rust backend + Next frontend"));
603
768
  console.log(chalk.dim(" Press Ctrl-C to stop.\n"));
604
- // Keep process alive. Dashboard is optional for orchestrator health.
605
- // If it crashes (e.g. EADDRINUSE), keep lifecycle + board watcher running.
606
769
  if (dashboardProcess) {
607
770
  dashboardProcess.on("exit", (code, signal) => {
608
771
  if (code !== 0 && code !== null) {
609
772
  console.error(chalk.yellow(`Dashboard exited with code ${code}${signal ? ` (signal ${signal})` : ""}. ` +
610
- "Keeping orchestrator core services running."));
773
+ "Keeping the Rust backend running."));
774
+ }
775
+ });
776
+ }
777
+ if (backendProcess) {
778
+ backendProcess.on("exit", (code, signal) => {
779
+ if (code !== 0 && code !== null) {
780
+ console.error(chalk.red(`Rust backend exited with code ${code}${signal ? ` (signal ${signal})` : ""}.`));
611
781
  }
612
782
  });
613
783
  }
614
- // Always keep process alive via interval heartbeat.
615
784
  setInterval(() => {
616
- // heartbeat -- lifecycle manager / watcher run on their own intervals
785
+ // Keep the launcher attached while child processes run.
617
786
  }, 60_000);
618
787
  }
619
- catch (err) {
620
- console.error(chalk.red(`Failed to start: ${err}`));
788
+ catch (error) {
789
+ console.error(chalk.red(`Failed to start: ${error}`));
621
790
  process.exit(1);
622
791
  }
623
792
  });