@agimon-ai/doompi-web 0.0.1-alpha.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 (344) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +196 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/bin/serve.cjs +3 -0
  5. package/dist/bin/serve.cjs.map +1 -0
  6. package/dist/bin/serve.d.cts +1 -0
  7. package/dist/bin/serve.d.mts +1 -0
  8. package/dist/bin/serve.mjs +3 -0
  9. package/dist/bin/serve.mjs.map +1 -0
  10. package/dist/bundler.cjs +1 -0
  11. package/dist/bundler.d.cts +2 -0
  12. package/dist/bundler.d.mts +2 -0
  13. package/dist/bundler.mjs +1 -0
  14. package/dist/index.cjs +1 -0
  15. package/dist/index.d.cts +7 -0
  16. package/dist/index.d.mts +7 -0
  17. package/dist/index.mjs +1 -0
  18. package/dist/src/adapters/authRoutes.cjs +2 -0
  19. package/dist/src/adapters/authRoutes.cjs.map +1 -0
  20. package/dist/src/adapters/authRoutes.mjs +2 -0
  21. package/dist/src/adapters/authRoutes.mjs.map +1 -0
  22. package/dist/src/adapters/bundledServer.cjs +2 -0
  23. package/dist/src/adapters/bundledServer.cjs.map +1 -0
  24. package/dist/src/adapters/bundledServer.mjs +2 -0
  25. package/dist/src/adapters/bundledServer.mjs.map +1 -0
  26. package/dist/src/adapters/directoryListing.cjs +2 -0
  27. package/dist/src/adapters/directoryListing.cjs.map +1 -0
  28. package/dist/src/adapters/directoryListing.mjs +2 -0
  29. package/dist/src/adapters/directoryListing.mjs.map +1 -0
  30. package/dist/src/adapters/directorySearch.cjs +3 -0
  31. package/dist/src/adapters/directorySearch.cjs.map +1 -0
  32. package/dist/src/adapters/directorySearch.mjs +3 -0
  33. package/dist/src/adapters/directorySearch.mjs.map +1 -0
  34. package/dist/src/adapters/gitStatus.cjs +2 -0
  35. package/dist/src/adapters/gitStatus.cjs.map +1 -0
  36. package/dist/src/adapters/gitStatus.mjs +2 -0
  37. package/dist/src/adapters/gitStatus.mjs.map +1 -0
  38. package/dist/src/adapters/httpServer.cjs +2 -0
  39. package/dist/src/adapters/httpServer.cjs.map +1 -0
  40. package/dist/src/adapters/httpServer.d.cts +14 -0
  41. package/dist/src/adapters/httpServer.d.cts.map +1 -0
  42. package/dist/src/adapters/httpServer.d.mts +14 -0
  43. package/dist/src/adapters/httpServer.d.mts.map +1 -0
  44. package/dist/src/adapters/httpServer.mjs +2 -0
  45. package/dist/src/adapters/httpServer.mjs.map +1 -0
  46. package/dist/src/adapters/hubProbe.cjs +2 -0
  47. package/dist/src/adapters/hubProbe.cjs.map +1 -0
  48. package/dist/src/adapters/hubProbe.mjs +2 -0
  49. package/dist/src/adapters/hubProbe.mjs.map +1 -0
  50. package/dist/src/adapters/packageApiProxy.cjs +2 -0
  51. package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
  52. package/dist/src/adapters/packageApiProxy.mjs +2 -0
  53. package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
  54. package/dist/src/adapters/piHubService.cjs +2 -0
  55. package/dist/src/adapters/piHubService.cjs.map +1 -0
  56. package/dist/src/adapters/piHubService.mjs +2 -0
  57. package/dist/src/adapters/piHubService.mjs.map +1 -0
  58. package/dist/src/adapters/piWebSocketListener.cjs +2 -0
  59. package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
  60. package/dist/src/adapters/piWebSocketListener.mjs +2 -0
  61. package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
  62. package/dist/src/adapters/providerAuth.cjs +2 -0
  63. package/dist/src/adapters/providerAuth.cjs.map +1 -0
  64. package/dist/src/adapters/providerAuth.mjs +2 -0
  65. package/dist/src/adapters/providerAuth.mjs.map +1 -0
  66. package/dist/src/adapters/registryWatcher.cjs +2 -0
  67. package/dist/src/adapters/registryWatcher.cjs.map +1 -0
  68. package/dist/src/adapters/registryWatcher.mjs +2 -0
  69. package/dist/src/adapters/registryWatcher.mjs.map +1 -0
  70. package/dist/src/adapters/serverSpawner.cjs +2 -0
  71. package/dist/src/adapters/serverSpawner.cjs.map +1 -0
  72. package/dist/src/adapters/serverSpawner.mjs +2 -0
  73. package/dist/src/adapters/serverSpawner.mjs.map +1 -0
  74. package/dist/src/adapters/sessionFiles.cjs +3 -0
  75. package/dist/src/adapters/sessionFiles.cjs.map +1 -0
  76. package/dist/src/adapters/sessionFiles.mjs +3 -0
  77. package/dist/src/adapters/sessionFiles.mjs.map +1 -0
  78. package/dist/src/adapters/sessionHub.cjs +2 -0
  79. package/dist/src/adapters/sessionHub.cjs.map +1 -0
  80. package/dist/src/adapters/sessionHub.mjs +2 -0
  81. package/dist/src/adapters/sessionHub.mjs.map +1 -0
  82. package/dist/src/adapters/sessionSocketClient.cjs +2 -0
  83. package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
  84. package/dist/src/adapters/sessionSocketClient.mjs +2 -0
  85. package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
  86. package/dist/src/adapters/syncGuard.cjs +2 -0
  87. package/dist/src/adapters/syncGuard.cjs.map +1 -0
  88. package/dist/src/adapters/syncGuard.mjs +2 -0
  89. package/dist/src/adapters/syncGuard.mjs.map +1 -0
  90. package/dist/src/adapters/threadJournals.cjs +2 -0
  91. package/dist/src/adapters/threadJournals.cjs.map +1 -0
  92. package/dist/src/adapters/threadJournals.mjs +2 -0
  93. package/dist/src/adapters/threadJournals.mjs.map +1 -0
  94. package/dist/src/adapters/webBundler.cjs +2 -0
  95. package/dist/src/adapters/webBundler.cjs.map +1 -0
  96. package/dist/src/adapters/webBundler.d.cts +28 -0
  97. package/dist/src/adapters/webBundler.d.cts.map +1 -0
  98. package/dist/src/adapters/webBundler.d.mts +28 -0
  99. package/dist/src/adapters/webBundler.d.mts.map +1 -0
  100. package/dist/src/adapters/webBundler.mjs +2 -0
  101. package/dist/src/adapters/webBundler.mjs.map +1 -0
  102. package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
  103. package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
  104. package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
  105. package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
  106. package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
  107. package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
  108. package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
  109. package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
  110. package/dist/src/adapters/webPluginGenerate.cjs +4 -0
  111. package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
  112. package/dist/src/adapters/webPluginGenerate.mjs +4 -0
  113. package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
  114. package/dist/src/adapters/webPluginScan.cjs +2 -0
  115. package/dist/src/adapters/webPluginScan.cjs.map +1 -0
  116. package/dist/src/adapters/webPluginScan.mjs +2 -0
  117. package/dist/src/adapters/webPluginScan.mjs.map +1 -0
  118. package/dist/src/adapters/webPluginVite.cjs +2 -0
  119. package/dist/src/adapters/webPluginVite.cjs.map +1 -0
  120. package/dist/src/adapters/webPluginVite.mjs +2 -0
  121. package/dist/src/adapters/webPluginVite.mjs.map +1 -0
  122. package/dist/src/services/directoryMatch.cjs +2 -0
  123. package/dist/src/services/directoryMatch.cjs.map +1 -0
  124. package/dist/src/services/directoryMatch.mjs +2 -0
  125. package/dist/src/services/directoryMatch.mjs.map +1 -0
  126. package/dist/src/services/fileMatch.cjs +2 -0
  127. package/dist/src/services/fileMatch.cjs.map +1 -0
  128. package/dist/src/services/fileMatch.mjs +2 -0
  129. package/dist/src/services/fileMatch.mjs.map +1 -0
  130. package/dist/src/services/fileMedia.cjs +2 -0
  131. package/dist/src/services/fileMedia.cjs.map +1 -0
  132. package/dist/src/services/fileMedia.mjs +2 -0
  133. package/dist/src/services/fileMedia.mjs.map +1 -0
  134. package/dist/src/services/frameRing.cjs +2 -0
  135. package/dist/src/services/frameRing.cjs.map +1 -0
  136. package/dist/src/services/frameRing.mjs +2 -0
  137. package/dist/src/services/frameRing.mjs.map +1 -0
  138. package/dist/src/services/journalTail.cjs +3 -0
  139. package/dist/src/services/journalTail.cjs.map +1 -0
  140. package/dist/src/services/journalTail.mjs +3 -0
  141. package/dist/src/services/journalTail.mjs.map +1 -0
  142. package/dist/src/services/loginFlow.cjs +2 -0
  143. package/dist/src/services/loginFlow.cjs.map +1 -0
  144. package/dist/src/services/loginFlow.mjs +2 -0
  145. package/dist/src/services/loginFlow.mjs.map +1 -0
  146. package/dist/src/services/registryStore.cjs +2 -0
  147. package/dist/src/services/registryStore.cjs.map +1 -0
  148. package/dist/src/services/registryStore.mjs +2 -0
  149. package/dist/src/services/registryStore.mjs.map +1 -0
  150. package/dist/src/services/retryPolicy.cjs +2 -0
  151. package/dist/src/services/retryPolicy.cjs.map +1 -0
  152. package/dist/src/services/retryPolicy.mjs +2 -0
  153. package/dist/src/services/retryPolicy.mjs.map +1 -0
  154. package/dist/src/services/serveOptions.cjs +2 -0
  155. package/dist/src/services/serveOptions.cjs.map +1 -0
  156. package/dist/src/services/serveOptions.d.cts +21 -0
  157. package/dist/src/services/serveOptions.d.cts.map +1 -0
  158. package/dist/src/services/serveOptions.d.mts +21 -0
  159. package/dist/src/services/serveOptions.d.mts.map +1 -0
  160. package/dist/src/services/serveOptions.mjs +2 -0
  161. package/dist/src/services/serveOptions.mjs.map +1 -0
  162. package/dist/src/services/sessionFraming.cjs +4 -0
  163. package/dist/src/services/sessionFraming.cjs.map +1 -0
  164. package/dist/src/services/sessionFraming.mjs +4 -0
  165. package/dist/src/services/sessionFraming.mjs.map +1 -0
  166. package/dist/src/services/sessionPresence.cjs +2 -0
  167. package/dist/src/services/sessionPresence.cjs.map +1 -0
  168. package/dist/src/services/sessionPresence.mjs +2 -0
  169. package/dist/src/services/sessionPresence.mjs.map +1 -0
  170. package/dist/src/services/staticAssets.cjs +2 -0
  171. package/dist/src/services/staticAssets.cjs.map +1 -0
  172. package/dist/src/services/staticAssets.mjs +2 -0
  173. package/dist/src/services/staticAssets.mjs.map +1 -0
  174. package/dist/src/services/webDevRoots.cjs +2 -0
  175. package/dist/src/services/webDevRoots.cjs.map +1 -0
  176. package/dist/src/services/webDevRoots.mjs +2 -0
  177. package/dist/src/services/webDevRoots.mjs.map +1 -0
  178. package/dist/src/services/webPluginManifest.cjs +2 -0
  179. package/dist/src/services/webPluginManifest.cjs.map +1 -0
  180. package/dist/src/services/webPluginManifest.mjs +2 -0
  181. package/dist/src/services/webPluginManifest.mjs.map +1 -0
  182. package/dist/src/types/auth.cjs +2 -0
  183. package/dist/src/types/auth.cjs.map +1 -0
  184. package/dist/src/types/auth.d.cts +94 -0
  185. package/dist/src/types/auth.d.cts.map +1 -0
  186. package/dist/src/types/auth.d.mts +94 -0
  187. package/dist/src/types/auth.d.mts.map +1 -0
  188. package/dist/src/types/auth.mjs +2 -0
  189. package/dist/src/types/auth.mjs.map +1 -0
  190. package/dist/src/types/bridge.d.cts +31 -0
  191. package/dist/src/types/bridge.d.cts.map +1 -0
  192. package/dist/src/types/bridge.d.mts +31 -0
  193. package/dist/src/types/bridge.d.mts.map +1 -0
  194. package/dist/src/types/hub.cjs +2 -0
  195. package/dist/src/types/hub.cjs.map +1 -0
  196. package/dist/src/types/hub.d.cts +58 -0
  197. package/dist/src/types/hub.d.cts.map +1 -0
  198. package/dist/src/types/hub.d.mts +58 -0
  199. package/dist/src/types/hub.d.mts.map +1 -0
  200. package/dist/src/types/hub.mjs +2 -0
  201. package/dist/src/types/hub.mjs.map +1 -0
  202. package/dist/src/types/media.cjs +2 -0
  203. package/dist/src/types/media.cjs.map +1 -0
  204. package/dist/src/types/media.mjs +2 -0
  205. package/dist/src/types/media.mjs.map +1 -0
  206. package/dist/src/types/registry.cjs +1 -0
  207. package/dist/src/types/registry.d.cts +2 -0
  208. package/dist/src/types/registry.d.mts +2 -0
  209. package/dist/src/types/registry.mjs +1 -0
  210. package/dist/src/types/session.cjs +2 -0
  211. package/dist/src/types/session.cjs.map +1 -0
  212. package/dist/src/types/session.d.cts +24 -0
  213. package/dist/src/types/session.d.cts.map +1 -0
  214. package/dist/src/types/session.d.mts +24 -0
  215. package/dist/src/types/session.d.mts.map +1 -0
  216. package/dist/src/types/session.mjs +2 -0
  217. package/dist/src/types/session.mjs.map +1 -0
  218. package/dist/web/apple-touch-icon.png +0 -0
  219. package/dist/web/assets/index-Cg52ngKv.css +2 -0
  220. package/dist/web/assets/index-DQO7NTIP.js +90 -0
  221. package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
  222. package/dist/web/favicon-16.png +0 -0
  223. package/dist/web/favicon-32.png +0 -0
  224. package/dist/web/favicon.ico +0 -0
  225. package/dist/web/favicon.svg +16 -0
  226. package/dist/web/icon-512.png +0 -0
  227. package/dist/web/index.html +17 -0
  228. package/llms.txt +8 -0
  229. package/package.json +106 -0
  230. package/src/adapters/authRoutes.ts +95 -0
  231. package/src/adapters/bundledServer.ts +64 -0
  232. package/src/adapters/directoryListing.ts +127 -0
  233. package/src/adapters/directorySearch.ts +124 -0
  234. package/src/adapters/gitStatus.ts +27 -0
  235. package/src/adapters/httpServer.ts +556 -0
  236. package/src/adapters/hubProbe.ts +27 -0
  237. package/src/adapters/packageApiProxy.ts +60 -0
  238. package/src/adapters/piHubService.ts +129 -0
  239. package/src/adapters/piWebSocketListener.ts +98 -0
  240. package/src/adapters/providerAuth.ts +181 -0
  241. package/src/adapters/registryWatcher.ts +112 -0
  242. package/src/adapters/serverSpawner.ts +181 -0
  243. package/src/adapters/sessionFiles.ts +99 -0
  244. package/src/adapters/sessionHub.ts +567 -0
  245. package/src/adapters/sessionSocketClient.ts +105 -0
  246. package/src/adapters/syncGuard.ts +119 -0
  247. package/src/adapters/threadJournals.ts +184 -0
  248. package/src/adapters/webBundler.ts +83 -0
  249. package/src/adapters/webHubPluginLoader.ts +67 -0
  250. package/src/adapters/webHubPlugins.generated.ts +4 -0
  251. package/src/adapters/webPluginGenerate.ts +172 -0
  252. package/src/adapters/webPluginScan.ts +62 -0
  253. package/src/adapters/webPluginVite.ts +51 -0
  254. package/src/bin/serve.ts +64 -0
  255. package/src/exports/bundler.ts +1 -0
  256. package/src/exports/index.ts +9 -0
  257. package/src/services/directoryMatch.ts +71 -0
  258. package/src/services/fileMatch.ts +37 -0
  259. package/src/services/fileMedia.ts +27 -0
  260. package/src/services/frameRing.ts +33 -0
  261. package/src/services/journalTail.ts +47 -0
  262. package/src/services/loginFlow.ts +144 -0
  263. package/src/services/registryStore.ts +83 -0
  264. package/src/services/retryPolicy.ts +15 -0
  265. package/src/services/serveOptions.ts +63 -0
  266. package/src/services/sessionFraming.ts +32 -0
  267. package/src/services/sessionPresence.ts +131 -0
  268. package/src/services/staticAssets.ts +37 -0
  269. package/src/services/webDevRoots.ts +36 -0
  270. package/src/services/webPluginManifest.ts +149 -0
  271. package/src/types/auth.ts +118 -0
  272. package/src/types/bridge.ts +41 -0
  273. package/src/types/hub.ts +290 -0
  274. package/src/types/media.ts +38 -0
  275. package/src/types/registry.ts +2 -0
  276. package/src/types/session.ts +36 -0
  277. package/src/web/app/Providers.tsx +46 -0
  278. package/src/web/app/protocolRuntime.ts +106 -0
  279. package/src/web/app/sessionRuntime.ts +189 -0
  280. package/src/web/app/webPlugins.generated.ts +4 -0
  281. package/src/web/components/MascotMark.tsx +46 -0
  282. package/src/web/components/PluginSurface.tsx +10 -0
  283. package/src/web/components/ToolRendererBoundary.tsx +32 -0
  284. package/src/web/features/activity/ActivityDock.tsx +125 -0
  285. package/src/web/features/connection/RefusedCard.tsx +63 -0
  286. package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
  287. package/src/web/features/leader/CommandPalette.tsx +386 -0
  288. package/src/web/features/selection/SelectionBar.tsx +529 -0
  289. package/src/web/features/session/Composer.tsx +368 -0
  290. package/src/web/features/session/MentionPreviews.tsx +58 -0
  291. package/src/web/features/session/ThreadView.tsx +35 -0
  292. package/src/web/features/session/Timeline.tsx +382 -0
  293. package/src/web/features/session/ToolCard.tsx +79 -0
  294. package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
  295. package/src/web/features/sessions/SessionRail.tsx +411 -0
  296. package/src/web/features/settings/AppearanceSettings.tsx +88 -0
  297. package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
  298. package/src/web/features/settings/PluginSettings.tsx +92 -0
  299. package/src/web/features/settings/ProviderSettings.tsx +238 -0
  300. package/src/web/features/settings/SettingsMenu.tsx +32 -0
  301. package/src/web/features/status/TopBar.tsx +208 -0
  302. package/src/web/index.html +16 -0
  303. package/src/web/lib/authApi.ts +90 -0
  304. package/src/web/lib/commands.ts +61 -0
  305. package/src/web/lib/composition.ts +225 -0
  306. package/src/web/lib/fileMentions.ts +32 -0
  307. package/src/web/lib/hubApi.ts +86 -0
  308. package/src/web/lib/leaderTree.ts +159 -0
  309. package/src/web/lib/piTransport.ts +61 -0
  310. package/src/web/lib/pluginRegistry.ts +488 -0
  311. package/src/web/lib/pluginSlotProps.ts +50 -0
  312. package/src/web/lib/promptFocus.ts +25 -0
  313. package/src/web/lib/protocolTimeline.ts +73 -0
  314. package/src/web/lib/sessionModel.ts +673 -0
  315. package/src/web/lib/sessionSummary.ts +58 -0
  316. package/src/web/lib/settingsSections.ts +21 -0
  317. package/src/web/lib/statusLine.ts +90 -0
  318. package/src/web/lib/threadRenderer.ts +25 -0
  319. package/src/web/lib/toolMessageProps.ts +21 -0
  320. package/src/web/lib/transport.ts +30 -0
  321. package/src/web/lib/wsClient.ts +73 -0
  322. package/src/web/main.tsx +17 -0
  323. package/src/web/public/apple-touch-icon.png +0 -0
  324. package/src/web/public/favicon-16.png +0 -0
  325. package/src/web/public/favicon-32.png +0 -0
  326. package/src/web/public/favicon.ico +0 -0
  327. package/src/web/public/favicon.svg +16 -0
  328. package/src/web/public/icon-512.png +0 -0
  329. package/src/web/routes/CockpitPage.tsx +98 -0
  330. package/src/web/routes/SettingsPage.tsx +60 -0
  331. package/src/web/routes/routeTree.tsx +49 -0
  332. package/src/web/stores/menuStore.ts +78 -0
  333. package/src/web/stores/paletteStore.ts +25 -0
  334. package/src/web/stores/sessionStore.ts +303 -0
  335. package/src/web/stores/sessionsStore.ts +155 -0
  336. package/src/web/stores/themeStore.ts +52 -0
  337. package/src/web/stores/threadStore.ts +68 -0
  338. package/src/web/stores/transientTabsStore.ts +63 -0
  339. package/src/web/stores/uiStore.ts +28 -0
  340. package/src/web/stores/useOpenTab.ts +19 -0
  341. package/src/web/stores/usePluginSlotProps.ts +22 -0
  342. package/src/web/styles/app.css +15 -0
  343. package/src/web/styles/webPluginSources.generated.css +1 -0
  344. package/src/web/vite-env.d.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverSpawner.mjs","names":[],"sources":["../../../src/adapters/serverSpawner.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { sessionRecordPath } from '../services/registryStore.ts';\nimport { type BundledServerLaunch, defaultServerLaunch } from './bundledServer.ts';\n\n/** Names the server in messages; the resolved command is a node path nobody would recognise. */\nconst SERVER_LABEL = 'doompi-server';\nconst SPAWNED_SEGMENT = 'spawned';\nconst SOCKET_FILE = 's.sock';\nconst TOKEN_FILE = 'token';\nconst LOG_FILE = 'server.log';\nconst DIR_NAME_PREFIX_LENGTH = 8;\nconst OWNER_ONLY_FILE = 0o600;\nconst OWNER_ONLY_DIR = 0o700;\nconst TOKEN_BYTES = 32;\n// macOS caps sun_path around 104 bytes; failing here beats a cryptic bind error.\nconst MAX_SOCKET_PATH = 103;\nconst RECORD_WAIT_MS = 10_000;\nconst RECORD_POLL_MS = 100;\nconst LOG_TAIL_CHARS = 400;\n\nexport type SpawnOutcome =\n | { ok: true; sessionId: string }\n | { ok: false; code: 'invalid_request' | 'spawn_failed'; error: string };\n\nexport interface SpawnSessionInput {\n cwd: string;\n name?: string;\n}\n\n/** Launches doompi-server processes for sessions created from the page. */\nexport interface SessionSpawner {\n spawn(input: SpawnSessionInput): Promise<SpawnOutcome>;\n}\n\nexport interface ServerSpawnerOptions {\n registryDir: string;\n /** Overridable so tests can stand in a fake server; defaults to the installation's own doompi-server. */\n command?: string;\n onNotice?: (message: string) => void;\n}\n\nfunction logTail(logPath: string): string {\n try {\n return fs.readFileSync(logPath, 'utf8').slice(-LOG_TAIL_CHARS).trim();\n } catch {\n return '';\n }\n}\n\n/**\n * Creates sessions the way a terminal would: by starting a doompi-server.\n *\n * The spawned server is detached and owns its own lifetime; it registers\n * itself like any other, which is why success is defined as \"its record\n * appeared\", not as anything about the child process. The hub stopping later\n * does not take created sessions down with it.\n *\n * Without an explicit command the server comes from this installation rather\n * than from PATH, so the cockpit needs no flag to launch the build it belongs\n * to. The resolution happens per session because it reads the session's own\n * working directory.\n */\nexport function createServerSpawner(options: ServerSpawnerOptions): SessionSpawner {\n return {\n spawn(input) {\n let stats: fs.Stats;\n try {\n stats = fs.statSync(input.cwd);\n } catch {\n return Promise.resolve({ ok: false, code: 'invalid_request', error: `No such directory: ${input.cwd}` });\n }\n if (!path.isAbsolute(input.cwd) || !stats.isDirectory()) {\n return Promise.resolve({\n ok: false,\n code: 'invalid_request',\n error: `The working directory must be an absolute path to a directory, received \"${input.cwd}\".`,\n });\n }\n\n // Resolved before anything is written, so a broken installation fails\n // without leaving a session directory nobody will ever use.\n let launch: BundledServerLaunch;\n try {\n launch = options.command\n ? { command: options.command, args: [], environment: process.env }\n : defaultServerLaunch(input.cwd, process.env);\n } catch (error) {\n return Promise.resolve({\n ok: false,\n code: 'spawn_failed',\n error: `Could not locate ${SERVER_LABEL}: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n const command = options.command ?? SERVER_LABEL;\n\n const sessionId = crypto.randomUUID();\n // The directory borrows only a prefix of the id: sun_path is capped\n // around 104 bytes and every byte of the session dir counts against it.\n const spawnedRoot = path.join(options.registryDir, SPAWNED_SEGMENT);\n let dirName = sessionId.slice(0, DIR_NAME_PREFIX_LENGTH);\n while (fs.existsSync(path.join(spawnedRoot, dirName)) && dirName.length < sessionId.length) {\n dirName = sessionId.slice(0, dirName.length + DIR_NAME_PREFIX_LENGTH);\n }\n const sessionDir = path.join(spawnedRoot, dirName);\n const socketPath = path.join(sessionDir, SOCKET_FILE);\n if (socketPath.length > MAX_SOCKET_PATH) {\n return Promise.resolve({\n ok: false,\n code: 'spawn_failed',\n error: `The socket path would exceed the unix limit (${socketPath.length} > ${MAX_SOCKET_PATH}); use a shorter --registry-dir.`,\n });\n }\n const tokenFile = path.join(sessionDir, TOKEN_FILE);\n const logPath = path.join(sessionDir, LOG_FILE);\n fs.mkdirSync(sessionDir, { recursive: true, mode: OWNER_ONLY_DIR });\n fs.writeFileSync(tokenFile, crypto.randomBytes(TOKEN_BYTES).toString('hex'), { mode: OWNER_ONLY_FILE });\n\n const log = fs.openSync(logPath, 'a', OWNER_ONLY_FILE);\n const args = [\n '--listen',\n socketPath,\n '--auth-token-file',\n tokenFile,\n '--session-id',\n sessionId,\n '--name',\n input.name ?? 'untitled',\n '--registry-dir',\n options.registryDir,\n ];\n const child = spawn(launch.command, [...launch.args, ...args], {\n cwd: input.cwd,\n detached: true,\n stdio: ['ignore', log, log],\n env: launch.environment,\n });\n fs.closeSync(log);\n\n return new Promise((resolve) => {\n let settled = false;\n const finish = (outcome: SpawnOutcome): void => {\n if (settled) return;\n settled = true;\n clearInterval(pollTimer);\n clearTimeout(deadline);\n if (outcome.ok) options.onNotice?.(`created session ${sessionId} in ${input.cwd}`);\n resolve(outcome);\n };\n\n child.once('error', (error) =>\n finish({ ok: false, code: 'spawn_failed', error: `Could not start ${command}: ${error.message}` }),\n );\n child.once('exit', (code) => {\n const tail = logTail(logPath);\n finish({\n ok: false,\n code: 'spawn_failed',\n error: `${command} exited with code ${code ?? 'unknown'} before registering.${tail ? ` Log: ${tail}` : ''}`,\n });\n });\n child.unref();\n\n const recordFile = sessionRecordPath(options.registryDir, sessionId);\n const pollTimer = setInterval(() => {\n if (fs.existsSync(recordFile)) finish({ ok: true, sessionId });\n }, RECORD_POLL_MS);\n const deadline = setTimeout(() => {\n const tail = logTail(logPath);\n finish({\n ok: false,\n code: 'spawn_failed',\n error: `${command} did not register within ${RECORD_WAIT_MS / 1000}s.${tail ? ` Log: ${tail}` : ''}`,\n });\n }, RECORD_WAIT_MS);\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/fileMatch.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("node:child_process");const a=(0,require("node:util").promisify)(i.execFile),o=4e3,s=new Set([`.git`,`node_modules`,`dist`,`coverage`,`.next`,`.pnpm`]);async function c(e){try{let{stdout:t}=await a(`git`,[`ls-files`,`-co`,`--exclude-standard`],{cwd:e,timeout:2e3,maxBuffer:8388608}),n=t.split(`
2
+ `).filter(e=>e!==``);return n.length>0?n:void 0}catch{return}}function l(e){let t=[],i=(a,c)=>{if(t.length>=o||c>6)return;let l;try{l=n.default.readdirSync(a,{withFileTypes:!0})}catch{return}for(let n of l){if(t.length>=o)return;if(n.name.startsWith(`.`)&&n.name!==`.doom`)continue;let l=r.default.join(a,n.name);n.isDirectory()?s.has(n.name)||i(l,c+1):n.isFile()&&t.push(r.default.relative(e,l))}};return i(e,0),t}async function u(e,n,r){let i=await c(e)??l(e);return t.rankFileMatches(i,n,r)}function d(e,t){return t===e||t.startsWith(`${e}${r.default.sep}`)}async function f(e,t,i){if(!t||t.includes(`\0`)||r.default.isAbsolute(t))return{status:`forbidden`};let a=r.default.resolve(e),o=r.default.resolve(a,t);if(!d(a,o))return{status:`forbidden`};let s,c;try{s=await n.default.promises.realpath(o),c=await n.default.promises.stat(s)}catch{return{status:`not-found`}}return d(await n.default.promises.realpath(a),s)?c.isFile()?c.size>i?{status:`too-large`}:{status:`ok`,body:await n.default.promises.readFile(s)}:{status:`not-found`}:{status:`forbidden`}}exports.listSessionFiles=u,exports.readSessionFile=f;
3
+ //# sourceMappingURL=sessionFiles.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionFiles.cjs","names":["promisify","execFile","fs","path","rankFileMatches"],"sources":["../../../src/adapters/sessionFiles.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport { rankFileMatches } from '../services/fileMatch.ts';\n\nconst execFileAsync = promisify(execFile);\n\nconst GIT_TIMEOUT_MS = 2000;\nconst GIT_MAX_BUFFER = 8 * 1024 * 1024;\n/** The walk fallback stops collecting past this many entries. */\nconst WALK_LIMIT = 4000;\nconst WALK_DEPTH = 6;\nconst SKIPPED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.pnpm']);\n\nasync function gitFiles(cwd: string): Promise<string[] | undefined> {\n try {\n // Tracked plus untracked-but-not-ignored: what a person means by \"the files\".\n const { stdout } = await execFileAsync('git', ['ls-files', '-co', '--exclude-standard'], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: GIT_MAX_BUFFER,\n });\n const files = stdout.split('\\n').filter((line) => line !== '');\n return files.length > 0 ? files : undefined;\n } catch {\n return undefined; // Not a repository, or git is unavailable; the walk covers it.\n }\n}\n\nfunction walkFiles(cwd: string): string[] {\n const files: string[] = [];\n const visit = (dir: string, depth: number): void => {\n if (files.length >= WALK_LIMIT || depth > WALK_DEPTH) return;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (files.length >= WALK_LIMIT) return;\n if (entry.name.startsWith('.') && entry.name !== '.doom') continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!SKIPPED_DIRECTORIES.has(entry.name)) visit(full, depth + 1);\n } else if (entry.isFile()) {\n files.push(path.relative(cwd, full));\n }\n }\n };\n visit(cwd, 0);\n return files;\n}\n\n/**\n * Repository-relative files under a session's working directory matching a\n * query, for the composer's @ completion. Git's index is the source when\n * available (it already understands ignores); a bounded walk covers plain\n * directories.\n */\nexport async function listSessionFiles(cwd: string, query: string, limit: number): Promise<string[]> {\n const files = (await gitFiles(cwd)) ?? walkFiles(cwd);\n return rankFileMatches(files, query, limit);\n}\n\nexport type SessionFileResult =\n | { status: 'ok'; body: Buffer }\n | { status: 'not-found' }\n | { status: 'forbidden' }\n | { status: 'too-large' };\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * One file under a session's working directory, for the timeline's mention\n * previews. Containment is checked on the real path as well as the lexical\n * one, so a symlink inside the tree cannot hand out something beyond it.\n */\nexport async function readSessionFile(cwd: string, relativePath: string, maxBytes: number): Promise<SessionFileResult> {\n if (!relativePath || relativePath.includes('\\0') || path.isAbsolute(relativePath)) return { status: 'forbidden' };\n const root = path.resolve(cwd);\n const candidate = path.resolve(root, relativePath);\n if (!isInside(root, candidate)) return { status: 'forbidden' };\n let real: string;\n let stats: fs.Stats;\n try {\n real = await fs.promises.realpath(candidate);\n stats = await fs.promises.stat(real);\n } catch {\n return { status: 'not-found' };\n }\n if (!isInside(await fs.promises.realpath(root), real)) return { status: 'forbidden' };\n if (!stats.isFile()) return { status: 'not-found' };\n if (stats.size > maxBytes) return { status: 'too-large' };\n return { status: 'ok', body: await fs.promises.readFile(real) };\n}\n"],"mappings":"yNAMA,MAAM,GAAA,qBAAgBA,CAAAA,CAAAA,UAAAA,CAAUC,EAAAA,QAAQ,EAKlC,EAAa,IAEb,EAAsB,IAAI,IAAI,CAAC,OAAQ,eAAgB,OAAQ,WAAY,QAAS,OAAO,CAAC,EAElG,eAAe,EAAS,EAA4C,CAClE,GAAI,CAEF,GAAM,CAAE,UAAW,MAAM,EAAc,MAAO,CAAC,WAAY,MAAO,oBAAoB,EAAG,CACvF,MACA,QAAS,IACT,UAAW,OACb,CAAC,EACK,EAAQ,EAAO,MAAM;CAAI,CAAC,CAAC,OAAQ,GAAS,IAAS,EAAE,EAC7D,OAAO,EAAM,OAAS,EAAI,EAAQ,IAAA,EACpC,MAAQ,CACN,MACF,CACF,CAEA,SAAS,EAAU,EAAuB,CACxC,IAAM,EAAkB,CAAC,EACnB,GAAS,EAAa,IAAwB,CAClD,GAAI,EAAM,QAAU,GAAc,EAAQ,EAAY,OACtD,IAAI,EACJ,GAAI,CACF,EAAUC,EAAAA,QAAG,YAAY,EAAK,CAAE,cAAe,EAAK,CAAC,CACvD,MAAQ,CACN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,EAAM,QAAU,EAAY,OAChC,GAAI,EAAM,KAAK,WAAW,GAAG,GAAK,EAAM,OAAS,QAAS,SAC1D,IAAM,EAAOC,EAAAA,QAAK,KAAK,EAAK,EAAM,IAAI,EAClC,EAAM,YAAY,EACf,EAAoB,IAAI,EAAM,IAAI,GAAG,EAAM,EAAM,EAAQ,CAAC,EACtD,EAAM,OAAO,GACtB,EAAM,KAAKA,EAAAA,QAAK,SAAS,EAAK,CAAI,CAAC,CAEvC,CACF,EAEA,OADA,EAAM,EAAK,CAAC,EACL,CACT,CAQA,eAAsB,EAAiB,EAAa,EAAe,EAAkC,CACnG,IAAM,EAAS,MAAM,EAAS,CAAG,GAAM,EAAU,CAAG,EACpD,OAAOC,EAAAA,gBAAgB,EAAO,EAAO,CAAK,CAC5C,CAQA,SAAS,EAAS,EAAc,EAA4B,CAC1D,OAAO,IAAc,GAAQ,EAAU,WAAW,GAAG,IAAOD,EAAAA,QAAK,KAAK,CACxE,CAOA,eAAsB,EAAgB,EAAa,EAAsB,EAA8C,CACrH,GAAI,CAAC,GAAgB,EAAa,SAAS,IAAI,GAAKA,EAAAA,QAAK,WAAW,CAAY,EAAG,MAAO,CAAE,OAAQ,WAAY,EAChH,IAAM,EAAOA,EAAAA,QAAK,QAAQ,CAAG,EACvB,EAAYA,EAAAA,QAAK,QAAQ,EAAM,CAAY,EACjD,GAAI,CAAC,EAAS,EAAM,CAAS,EAAG,MAAO,CAAE,OAAQ,WAAY,EAC7D,IAAI,EACA,EACJ,GAAI,CACF,EAAO,MAAMD,EAAAA,QAAG,SAAS,SAAS,CAAS,EAC3C,EAAQ,MAAMA,EAAAA,QAAG,SAAS,KAAK,CAAI,CACrC,MAAQ,CACN,MAAO,CAAE,OAAQ,WAAY,CAC/B,CAIA,OAHK,EAAS,MAAMA,EAAAA,QAAG,SAAS,SAAS,CAAI,EAAG,CAAI,EAC/C,EAAM,OAAO,EACd,EAAM,KAAO,EAAiB,CAAE,OAAQ,WAAY,EACjD,CAAE,OAAQ,KAAM,KAAM,MAAMA,EAAAA,QAAG,SAAS,SAAS,CAAI,CAAE,EAFlC,CAAE,OAAQ,WAAY,EADY,CAAE,OAAQ,WAAY,CAItF"}
@@ -0,0 +1,3 @@
1
+ import{rankFileMatches as e}from"../services/fileMatch.mjs";import t from"node:fs";import n from"node:path";import{execFile as r}from"node:child_process";import{promisify as i}from"node:util";const a=i(r),o=4e3,s=new Set([`.git`,`node_modules`,`dist`,`coverage`,`.next`,`.pnpm`]);async function c(e){try{let{stdout:t}=await a(`git`,[`ls-files`,`-co`,`--exclude-standard`],{cwd:e,timeout:2e3,maxBuffer:8388608}),n=t.split(`
2
+ `).filter(e=>e!==``);return n.length>0?n:void 0}catch{return}}function l(e){let r=[],i=(a,c)=>{if(r.length>=o||c>6)return;let l;try{l=t.readdirSync(a,{withFileTypes:!0})}catch{return}for(let t of l){if(r.length>=o)return;if(t.name.startsWith(`.`)&&t.name!==`.doom`)continue;let l=n.join(a,t.name);t.isDirectory()?s.has(t.name)||i(l,c+1):t.isFile()&&r.push(n.relative(e,l))}};return i(e,0),r}async function u(t,n,r){let i=await c(t)??l(t);return e(i,n,r)}function d(e,t){return t===e||t.startsWith(`${e}${n.sep}`)}async function f(e,r,i){if(!r||r.includes(`\0`)||n.isAbsolute(r))return{status:`forbidden`};let a=n.resolve(e),o=n.resolve(a,r);if(!d(a,o))return{status:`forbidden`};let s,c;try{s=await t.promises.realpath(o),c=await t.promises.stat(s)}catch{return{status:`not-found`}}return d(await t.promises.realpath(a),s)?c.isFile()?c.size>i?{status:`too-large`}:{status:`ok`,body:await t.promises.readFile(s)}:{status:`not-found`}:{status:`forbidden`}}export{u as listSessionFiles,f as readSessionFile};
3
+ //# sourceMappingURL=sessionFiles.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionFiles.mjs","names":[],"sources":["../../../src/adapters/sessionFiles.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\nimport { rankFileMatches } from '../services/fileMatch.ts';\n\nconst execFileAsync = promisify(execFile);\n\nconst GIT_TIMEOUT_MS = 2000;\nconst GIT_MAX_BUFFER = 8 * 1024 * 1024;\n/** The walk fallback stops collecting past this many entries. */\nconst WALK_LIMIT = 4000;\nconst WALK_DEPTH = 6;\nconst SKIPPED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.pnpm']);\n\nasync function gitFiles(cwd: string): Promise<string[] | undefined> {\n try {\n // Tracked plus untracked-but-not-ignored: what a person means by \"the files\".\n const { stdout } = await execFileAsync('git', ['ls-files', '-co', '--exclude-standard'], {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n maxBuffer: GIT_MAX_BUFFER,\n });\n const files = stdout.split('\\n').filter((line) => line !== '');\n return files.length > 0 ? files : undefined;\n } catch {\n return undefined; // Not a repository, or git is unavailable; the walk covers it.\n }\n}\n\nfunction walkFiles(cwd: string): string[] {\n const files: string[] = [];\n const visit = (dir: string, depth: number): void => {\n if (files.length >= WALK_LIMIT || depth > WALK_DEPTH) return;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (files.length >= WALK_LIMIT) return;\n if (entry.name.startsWith('.') && entry.name !== '.doom') continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!SKIPPED_DIRECTORIES.has(entry.name)) visit(full, depth + 1);\n } else if (entry.isFile()) {\n files.push(path.relative(cwd, full));\n }\n }\n };\n visit(cwd, 0);\n return files;\n}\n\n/**\n * Repository-relative files under a session's working directory matching a\n * query, for the composer's @ completion. Git's index is the source when\n * available (it already understands ignores); a bounded walk covers plain\n * directories.\n */\nexport async function listSessionFiles(cwd: string, query: string, limit: number): Promise<string[]> {\n const files = (await gitFiles(cwd)) ?? walkFiles(cwd);\n return rankFileMatches(files, query, limit);\n}\n\nexport type SessionFileResult =\n | { status: 'ok'; body: Buffer }\n | { status: 'not-found' }\n | { status: 'forbidden' }\n | { status: 'too-large' };\n\nfunction isInside(root: string, candidate: string): boolean {\n return candidate === root || candidate.startsWith(`${root}${path.sep}`);\n}\n\n/**\n * One file under a session's working directory, for the timeline's mention\n * previews. Containment is checked on the real path as well as the lexical\n * one, so a symlink inside the tree cannot hand out something beyond it.\n */\nexport async function readSessionFile(cwd: string, relativePath: string, maxBytes: number): Promise<SessionFileResult> {\n if (!relativePath || relativePath.includes('\\0') || path.isAbsolute(relativePath)) return { status: 'forbidden' };\n const root = path.resolve(cwd);\n const candidate = path.resolve(root, relativePath);\n if (!isInside(root, candidate)) return { status: 'forbidden' };\n let real: string;\n let stats: fs.Stats;\n try {\n real = await fs.promises.realpath(candidate);\n stats = await fs.promises.stat(real);\n } catch {\n return { status: 'not-found' };\n }\n if (!isInside(await fs.promises.realpath(root), real)) return { status: 'forbidden' };\n if (!stats.isFile()) return { status: 'not-found' };\n if (stats.size > maxBytes) return { status: 'too-large' };\n return { status: 'ok', body: await fs.promises.readFile(real) };\n}\n"],"mappings":"gMAMA,MAAM,EAAgB,EAAU,CAAQ,EAKlC,EAAa,IAEb,EAAsB,IAAI,IAAI,CAAC,OAAQ,eAAgB,OAAQ,WAAY,QAAS,OAAO,CAAC,EAElG,eAAe,EAAS,EAA4C,CAClE,GAAI,CAEF,GAAM,CAAE,UAAW,MAAM,EAAc,MAAO,CAAC,WAAY,MAAO,oBAAoB,EAAG,CACvF,MACA,QAAS,IACT,UAAW,OACb,CAAC,EACK,EAAQ,EAAO,MAAM;CAAI,CAAC,CAAC,OAAQ,GAAS,IAAS,EAAE,EAC7D,OAAO,EAAM,OAAS,EAAI,EAAQ,IAAA,EACpC,MAAQ,CACN,MACF,CACF,CAEA,SAAS,EAAU,EAAuB,CACxC,IAAM,EAAkB,CAAC,EACnB,GAAS,EAAa,IAAwB,CAClD,GAAI,EAAM,QAAU,GAAc,EAAQ,EAAY,OACtD,IAAI,EACJ,GAAI,CACF,EAAU,EAAG,YAAY,EAAK,CAAE,cAAe,EAAK,CAAC,CACvD,MAAQ,CACN,MACF,CACA,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,EAAM,QAAU,EAAY,OAChC,GAAI,EAAM,KAAK,WAAW,GAAG,GAAK,EAAM,OAAS,QAAS,SAC1D,IAAM,EAAO,EAAK,KAAK,EAAK,EAAM,IAAI,EAClC,EAAM,YAAY,EACf,EAAoB,IAAI,EAAM,IAAI,GAAG,EAAM,EAAM,EAAQ,CAAC,EACtD,EAAM,OAAO,GACtB,EAAM,KAAK,EAAK,SAAS,EAAK,CAAI,CAAC,CAEvC,CACF,EAEA,OADA,EAAM,EAAK,CAAC,EACL,CACT,CAQA,eAAsB,EAAiB,EAAa,EAAe,EAAkC,CACnG,IAAM,EAAS,MAAM,EAAS,CAAG,GAAM,EAAU,CAAG,EACpD,OAAO,EAAgB,EAAO,EAAO,CAAK,CAC5C,CAQA,SAAS,EAAS,EAAc,EAA4B,CAC1D,OAAO,IAAc,GAAQ,EAAU,WAAW,GAAG,IAAO,EAAK,KAAK,CACxE,CAOA,eAAsB,EAAgB,EAAa,EAAsB,EAA8C,CACrH,GAAI,CAAC,GAAgB,EAAa,SAAS,IAAI,GAAK,EAAK,WAAW,CAAY,EAAG,MAAO,CAAE,OAAQ,WAAY,EAChH,IAAM,EAAO,EAAK,QAAQ,CAAG,EACvB,EAAY,EAAK,QAAQ,EAAM,CAAY,EACjD,GAAI,CAAC,EAAS,EAAM,CAAS,EAAG,MAAO,CAAE,OAAQ,WAAY,EAC7D,IAAI,EACA,EACJ,GAAI,CACF,EAAO,MAAM,EAAG,SAAS,SAAS,CAAS,EAC3C,EAAQ,MAAM,EAAG,SAAS,KAAK,CAAI,CACrC,MAAQ,CACN,MAAO,CAAE,OAAQ,WAAY,CAC/B,CAIA,OAHK,EAAS,MAAM,EAAG,SAAS,SAAS,CAAI,EAAG,CAAI,EAC/C,EAAM,OAAO,EACd,EAAM,KAAO,EAAiB,CAAE,OAAQ,WAAY,EACjD,CAAE,OAAQ,KAAM,KAAM,MAAM,EAAG,SAAS,SAAS,CAAI,CAAE,EAFlC,CAAE,OAAQ,WAAY,EADY,CAAE,OAAQ,WAAY,CAItF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../types/hub.cjs"),n=require("../services/frameRing.cjs"),r=require("../services/sessionPresence.cjs"),i=require("./sessionSocketClient.cjs");let a=require("node:fs");a=e.__toESM(a,1);const o=`get_entries`,s=`entry_appended`;function c(e,t){let n=e.data;if(typeof n!=`object`||!n)return[];let r=n.entries;if(!Array.isArray(r))return[];let i=[],a;for(let e of r){if(typeof e!=`object`||!e)continue;let t=e;t.type===`message`?i.push(t):t.type===`custom`&&t.customType===`doom-minor-modes`&&(a=t)}let o=i.length>t?i.slice(-t):i;return a===void 0?o:[...o,a]}function l(e){let t=e.data;if(typeof t!=`object`||!t)return[];let n=t.entries;if(!Array.isArray(n))return[];let r=[];for(let e of n){if(typeof e!=`object`||!e)continue;let t=e;t.type===`message`&&r.push(t)}return r}function u(e){if(e.type===`custom`)return e.customType===t.MINOR_MODE_ENTRY_TYPE;let n=e.message;return typeof n!=`object`||!n?!1:n.role===`user`}function d(e,t){return e.findIndex(e=>e.id===t)}function f(e){return a.default.readFileSync(e.tokenFile,`utf8`).trim()}function p(e,t){return e.socketPath!==t.socketPath||e.tokenFile!==t.tokenFile||e.pid!==t.pid}function m(e){return{sessionId:e.id,cwd:e.cwd}}function h(e){let a=new Map,h=new Set,g=e.readGit,_=e.restoreLimit??300,v=e.journalLimit??5e3,y=e.signal??(e=>{process.kill(e,`SIGTERM`)}),b=!1,x=e=>{for(let t of h)t(e)},S=(e.channels??[]).map(t=>({frameType:t.frameType,source:t.start({sessions:()=>[...a.values()].map(e=>m(e.record)),publish:(e,n)=>{b||x({kind:`channel`,frameType:t.frameType,sessionId:e,payload:n})},onNotice:t=>e.onNotice?.(t)})})),C=e=>({id:e.record.id,name:e.presence.sessionName??e.record.name,cwd:e.record.cwd,createdAt:e.record.createdAt,updatedAt:e.presence.updatedAt,phase:e.presence.phase,phaseSince:e.presence.phaseSince,attach:e.attach,...e.attachReason===void 0?{}:{attachReason:e.attachReason},pendingMessageCount:e.presence.pendingMessageCount,everPrompted:e.presence.everPrompted,awaitingInput:e.presence.awaitingInput,...e.presence.lastSettledAt===void 0?{}:{lastSettledAt:e.presence.lastSettledAt},socketPath:e.record.socketPath,...e.record.apiSocketPath===void 0?{}:{apiSocketPath:e.record.apiSocketPath},...e.git===void 0?{}:{git:e.git}}),w=e=>{let t=C(e),n=JSON.stringify(t);n!==e.lastSummaryJson&&(e.lastSummaryJson=n,x({kind:`upsert`,session:t}))},T=e=>{g&&g(e.record.cwd).then(t=>{b||a.get(e.record.id)!==e||(e.git=t,w(e))})},E=e=>{let t;try{t=f(e.record)}catch(t){e.attachment=void 0,e.attach=`closed`,e.attachReason=`The token file is unreadable: ${t instanceof Error?t.message:String(t)}`,w(e);return}e.attachment=i.attachToSession({socketPath:e.record.socketPath,...e.record.apiSocketPath===void 0?{}:{apiSocketPath:e.record.apiSocketPath},token:t,handlers:{onFrame:t=>{if(t.type===`response`&&t.command===o){let n=l(t);e.journal=n.length>v?n.slice(-v):n;let i=new Date().toISOString(),a=e.restoredJournal;e.restoredJournal=!0;let o=!1;for(let n of c(t,_)){if(a&&!u(n))continue;let t=typeof n.id==`string`?n.id:void 0;if(t!==void 0){if(e.emittedEntryIds.has(t))continue;e.emittedEntryIds.add(t)}let c={type:s,entry:n};e.ring.record(c),x({kind:`frame`,sessionId:e.record.id,frame:c});let l=r.presenceAfterRestoredEntry(e.presence,n,i);o||=l!==e.presence,e.presence=l}o&&w(e);return}let n=e.presence.phase;e.ring.record(t);let i=r.reducePresence(e.presence,t,new Date().toISOString()),a=i!==e.presence;e.presence=i,x({kind:`frame`,sessionId:e.record.id,frame:t}),a&&w(e),n!==`idle`&&i.phase===`idle`&&T(e),n!==i.phase&&e.attachment?.send({type:o})},onStatus:t=>{e.attach=t.state,e.attachReason=t.reason,t.state===`attached`&&(e.attachment?.send({type:`get_state`}),e.attachment?.send({type:o})),w(e)}}})},D=t=>{let i={record:t,ring:n.createFrameRing(e.ringLimit),journal:[],presence:r.initialPresence(new Date().toISOString()),attach:`connecting`,emittedEntryIds:new Set,restoredJournal:!1};a.set(t.id,i),e.onNotice?.(`session ${t.id} (${t.name}) appeared`),E(i),w(i),T(i);for(let e of S)e.source.sessionAdded?.(m(t))};e.source.subscribe(t=>{let n=new Set;for(let e of t){n.add(e.id);let t=a.get(e.id);if(!t){D(e);continue}let r=p(t.record,e);t.record=e,r?(t.attachment?.close(),E(t)):t.attachment||E(t),w(t)}for(let[t,r]of a)if(!n.has(t)){r.attachment?.close(),a.delete(t);for(let e of S)e.source.sessionRemoved?.(t);e.onNotice?.(`session ${t} left`),x({kind:`removed`,sessionId:t})}});let O=g?setInterval(()=>{for(let e of a.values())T(e)},e.gitRefreshMs??1e4):void 0;return{records(){return[...a.values()].map(e=>e.record)},snapshot(){return[...a.values()].sort((e,t)=>e.record.createdAt.localeCompare(t.record.createdAt)||e.record.id.localeCompare(t.record.id)).map(C)},stop(t){let n=a.get(t);if(!n)return{ok:!1,code:`unknown`,error:`Unknown session.`};if(n.record.pid===process.pid)return{ok:!1,code:`self`,error:`This session hosts the cockpit; stop it from its own terminal.`};try{y(n.record.pid)}catch(e){return{ok:!1,code:`signal_failed`,error:`Could not signal the session server: ${e instanceof Error?e.message:String(e)}`}}return e.onNotice?.(`asked session ${t} to stop`),{ok:!0}},onEvent(e){return h.add(e),()=>h.delete(e)},backlog(e){let n=a.get(e);if(!n)return;let{frames:r,dropped:i}=n.ring.snapshot();return{type:t.SESSION_BACKLOG_TYPE,sessionId:e,frames:r,dropped:i}},history(e,n){let r=a.get(e);if(!r)return;let i=Math.max(1,Math.min(n.limit??100,100)),o=n.before===void 0?r.journal.length:d(r.journal,n.before),c=o<0?r.journal.length:o,l=Math.max(0,c-i),u=r.journal.slice(l,c),f=u[0];return{type:t.HISTORY_PAGE_TYPE,sessionId:e,frames:u.map(e=>({type:s,entry:e})),cursor:typeof f?.id==`string`?f.id:null,hasMore:l>0,...n.before===void 0?{}:{before:n.before}}},channelTypes(){return S.map(e=>e.frameType)},channelFrames(e){let t=a.get(e);if(!t)return[];let n=m(t.record),r=[];for(let t of S){let i=t.source.payloadFor(n);i!==void 0&&r.push({type:t.frameType,sessionId:e,payload:i})}return r},threadJournal(e,t){let n=a.get(e);if(!n)return;let r=m(n.record);for(let e of S){let n=e.source.threadJournal?.(r,t);if(n!==void 0)return n}},command(e,n){let i=a.get(e);if(!i)return;if(i.attachment?.send(n),n.type===`extension_ui_response`&&typeof n.id==`string`){let e={type:t.DIALOG_ANSWERED_TYPE,id:n.id};i.ring.record(e),x({kind:`frame`,sessionId:i.record.id,frame:e})}let o=r.presenceAfterCommand(i.presence,n,new Date().toISOString());o!==i.presence&&(i.presence=o,w(i))},create(t){return e.spawner?e.spawner.spawn(t):Promise.resolve({ok:!1,code:`invalid_request`,error:`This cockpit serves a fixed session and cannot create new ones.`})},close(){b=!0,e.source.close();for(let e of S)e.source.close();O&&clearInterval(O);for(let e of a.values())e.attachment?.close();a.clear(),h.clear()}}}exports.createSessionHub=h;
2
+ //# sourceMappingURL=sessionHub.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionHub.cjs","names":["MINOR_MODE_ENTRY_TYPE","fs","attachToSession","presenceAfterRestoredEntry","reducePresence","createFrameRing","initialPresence","SESSION_BACKLOG_TYPE","HISTORY_PAGE_TYPE","DIALOG_ANSWERED_TYPE","presenceAfterCommand"],"sources":["../../../src/adapters/sessionHub.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { ChannelFrame, HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { createFrameRing, type FrameRing } from '../services/frameRing.ts';\nimport {\n initialPresence,\n presenceAfterCommand,\n presenceAfterRestoredEntry,\n reducePresence,\n type SessionPresence,\n} from '../services/sessionPresence.ts';\nimport type { SessionAttachment } from '../types/bridge.ts';\nimport {\n DIALOG_ANSWERED_TYPE,\n HISTORY_PAGE_SIZE,\n HISTORY_PAGE_TYPE,\n type HistoryPageFrame,\n MINOR_MODE_ENTRY_TYPE,\n SESSION_BACKLOG_TYPE,\n type SessionBacklogFrame,\n type SessionGitStatus,\n type SessionSummary,\n} from '../types/hub.ts';\nimport type { SessionRecord } from '../types/registry.ts';\nimport type { BridgeState, SessionFrame } from '../types/session.ts';\nimport type { RecordSource } from './registryWatcher.ts';\nimport type { SessionSpawner, SpawnOutcome, SpawnSessionInput } from './serverSpawner.ts';\nimport { attachToSession } from './sessionSocketClient.ts';\n\nconst GIT_REFRESH_MS = 10_000;\nconst GET_ENTRIES_COMMAND = 'get_entries';\n/**\n * How much of a long transcript a fresh attach restores. The ring holds 512\n * frames and live ones have to fit beside the history, so a session with\n * thousands of messages gives up its oldest rather than its newest.\n */\nconst DEFAULT_RESTORE_LIMIT = 300;\n/**\n * Journalled messages retained per session for paging back through. Well past\n * any transcript a reader scrolls by hand, and still bounded: a hub watching a\n * dozen long sessions holds a list, not a session's whole life.\n */\nconst DEFAULT_JOURNAL_LIMIT = 5000;\nconst ENTRY_APPENDED_TYPE = 'entry_appended';\n\nexport type HubEvent =\n | { kind: 'upsert'; session: SessionSummary }\n | { kind: 'removed'; sessionId: string }\n | { kind: 'frame'; sessionId: string; frame: SessionFrame }\n | { kind: 'channel'; frameType: string; sessionId: string; payload: unknown };\n\nexport interface SessionHubOptions {\n source: RecordSource;\n /** Absent only in tests; without one, creating sessions is refused. */\n spawner?: SessionSpawner;\n /** Injectable for tests; defaults to asking git about the session cwd. */\n readGit?: (cwd: string) => Promise<SessionGitStatus | undefined>;\n /** The hub's data channels (built-in and plugin-provided sources). */\n channels?: readonly WebHubChannel[];\n /** Injectable for tests; defaults to SIGTERM, which doompi-server treats as a clean stop. */\n signal?: (pid: number) => void;\n ringLimit?: number;\n /** Journalled messages restored per session on attach; the rest is kept for paging back through. */\n restoreLimit?: number;\n /** Journalled messages retained per session for paging; the oldest are forgotten first. */\n journalLimit?: number;\n gitRefreshMs?: number;\n onNotice?: (message: string) => void;\n}\n\nexport type StopOutcome = { ok: true } | { ok: false; code: 'unknown' | 'self' | 'signal_failed'; error: string };\n\nexport interface SessionHub {\n snapshot(): SessionSummary[];\n /** The registry records behind the live sessions, for clients that dial them directly. */\n records(): SessionRecord[];\n /** Asks a session's server to exit; the session leaves once its record is withdrawn. */\n stop(sessionId: string): StopOutcome;\n /** Streams every change; pages filter frame events by their subscriptions. */\n onEvent(listener: (event: HubEvent) => void): () => void;\n /** Recent history for one session, or undefined for an unknown id. */\n backlog(sessionId: string): SessionBacklogFrame | undefined;\n /** One older window of a session's transcript, for a page scrolling back. */\n history(sessionId: string, request: { before?: string; limit?: number }): HistoryPageFrame | undefined;\n /** Frame types of the loaded data channels, for the hello frame. */\n channelTypes(): string[];\n /** Every channel's subscribe-time snapshot for one session; empty for an unknown id. */\n channelFrames(sessionId: string): ChannelFrame[];\n /** The journal behind one thread of a session, from the first data channel that names it; undefined until one does. */\n threadJournal(sessionId: string, threadId: string): string | undefined;\n command(sessionId: string, frame: SessionFrame): void;\n create(input: SpawnSessionInput): Promise<SpawnOutcome>;\n close(): void;\n}\n\ninterface ManagedSession {\n record: SessionRecord;\n attachment?: SessionAttachment;\n ring: FrameRing;\n presence: SessionPresence;\n attach: BridgeState;\n attachReason?: string;\n git?: SessionGitStatus;\n lastSummaryJson?: string;\n /**\n * Journal entry ids this hub has already published. Pi reports a message\n * entry only when the journal is read, never as it is written, so the hub\n * re-reads on each run boundary; remembering what it sent is what keeps that\n * re-read from replaying the whole transcript every time.\n */\n emittedEntryIds: Set<string>;\n /** Whether the first journal read has been published; later reads are refreshes. */\n restoredJournal: boolean;\n /**\n * Every journalled message the session has reported, oldest first.\n *\n * The attach path publishes only the newest slice, so without this the rest\n * of a long transcript would exist only on disk and a reader scrolling up\n * would find nothing. Bounded, because a hub watching many long sessions\n * should not grow without limit; the oldest go first, which is also the\n * order a reader runs out of interest in them.\n */\n journal: Record<string, unknown>[];\n}\n\ninterface StartedChannel {\n frameType: string;\n source: HubChannelSource;\n}\n\n/**\n * The journal entries a cockpit can render, oldest first.\n *\n * A session outlives every hub that watches it: it may have been driven from\n * the TUI for an hour before a cockpit existed, and a hub restart forgets the\n * live ring entirely. Without this the timeline of a long-running session\n * opens empty, which reads as \"the agent has done nothing\" rather than \"this\n * page arrived late\". The journal is the session's own record, so replaying it\n * is the only honest way to show what came before.\n *\n * Two kinds survive the filter: the messages that are the transcript, and the\n * newest minor-mode catalog entry, which the runtime journals as a custom\n * entry and which a late hub would otherwise never see.\n */\nfunction renderableJournalEntries(frame: SessionFrame, limit: number): Record<string, unknown>[] {\n const data = frame.data;\n if (typeof data !== 'object' || data === null) return [];\n const entries = (data as { entries?: unknown }).entries;\n if (!Array.isArray(entries)) return [];\n\n const messages: Record<string, unknown>[] = [];\n let minorModes: Record<string, unknown> | undefined;\n for (const entry of entries) {\n if (typeof entry !== 'object' || entry === null) continue;\n const candidate = entry as Record<string, unknown>;\n if (candidate.type === 'message') messages.push(candidate);\n else if (candidate.type === 'custom' && candidate.customType === MINOR_MODE_ENTRY_TYPE) minorModes = candidate;\n }\n\n // The ring is bounded and live frames must still fit, so only the tail of a\n // long transcript is published on attach; the rest is retained for the page\n // to page back through rather than dropped on the floor.\n const kept = messages.length > limit ? messages.slice(-limit) : messages;\n return minorModes === undefined ? kept : [...kept, minorModes];\n}\n\n/**\n * Every message a journal read carries, oldest first, with no tail limit.\n *\n * This is what the page pages back through. The attach path publishes only the\n * newest slice, because the ring is bounded and the live stream has to fit\n * beside it, but a reader scrolling up wants what came before, and the session\n * already told the hub all of it.\n */\nfunction journalMessages(frame: SessionFrame): Record<string, unknown>[] {\n const data = frame.data;\n if (typeof data !== 'object' || data === null) return [];\n const entries = (data as { entries?: unknown }).entries;\n if (!Array.isArray(entries)) return [];\n const messages: Record<string, unknown>[] = [];\n for (const entry of entries) {\n if (typeof entry !== 'object' || entry === null) continue;\n const candidate = entry as Record<string, unknown>;\n if (candidate.type === 'message') messages.push(candidate);\n }\n return messages;\n}\n\n/**\n * Whether a journal entry is one no live frame reports.\n *\n * Pi publishes a frame for everything the agent does, but a user message only\n * ever reaches the journal: the agent did not produce it. That is invisible\n * when something other than a cockpit sends it, which is how autonomous voice\n * prompts the agent with what it heard. The catalog entry is here because\n * re-reading it only replaces a projection, so a refresh cannot double it.\n */\nfunction isUnreportedEntry(entry: Record<string, unknown>): boolean {\n if (entry.type === 'custom') return entry.customType === MINOR_MODE_ENTRY_TYPE;\n const message = entry.message;\n if (typeof message !== 'object' || message === null) return false;\n return (message as { role?: unknown }).role === 'user';\n}\n\n/** Where an entry sits in the retained journal, or -1 when this hub never held it. */\nfunction indexOfEntry(journal: readonly Record<string, unknown>[], id: string): number {\n return journal.findIndex((entry) => entry.id === id);\n}\n\nfunction readTokenFile(record: SessionRecord): string {\n return fs.readFileSync(record.tokenFile, 'utf8').trim();\n}\n\nfunction attachRelevantChanged(previous: SessionRecord, next: SessionRecord): boolean {\n return previous.socketPath !== next.socketPath || previous.tokenFile !== next.tokenFile || previous.pid !== next.pid;\n}\n\nfunction scopeOf(record: SessionRecord): HubSessionScope {\n return { sessionId: record.id, cwd: record.cwd };\n}\n\n/**\n * Holds the hub's live view of every registered session.\n *\n * One attachment per session: the hub is \"the one client\" each socket allows,\n * and every page multiplexes behind it. Presence is reduced from the frame\n * stream so the rail describes sessions nobody is viewing, and a bounded ring\n * per session gives late pages their history, since a permanently attached\n * socket never fills the server-side backlog. Session data beyond the agent\n * stream comes from channels: each one watches its own source and publishes\n * per-session payloads the hub fans out by frame type.\n */\nexport function createSessionHub(options: SessionHubOptions): SessionHub {\n const sessions = new Map<string, ManagedSession>();\n const listeners = new Set<(event: HubEvent) => void>();\n const readGit = options.readGit;\n const restoreLimit = options.restoreLimit ?? DEFAULT_RESTORE_LIMIT;\n const journalLimit = options.journalLimit ?? DEFAULT_JOURNAL_LIMIT;\n const signal =\n options.signal ??\n ((pid: number): void => {\n process.kill(pid, 'SIGTERM');\n });\n let closed = false;\n\n const emit = (event: HubEvent): void => {\n for (const listener of listeners) listener(event);\n };\n\n const startedChannels: StartedChannel[] = (options.channels ?? []).map((channel) => ({\n frameType: channel.frameType,\n source: channel.start({\n sessions: () => [...sessions.values()].map((managed) => scopeOf(managed.record)),\n publish: (sessionId, payload) => {\n if (closed) return;\n emit({ kind: 'channel', frameType: channel.frameType, sessionId, payload });\n },\n onNotice: (message) => options.onNotice?.(message),\n }),\n }));\n\n const toSummary = (managed: ManagedSession): SessionSummary => ({\n id: managed.record.id,\n name: managed.presence.sessionName ?? managed.record.name,\n cwd: managed.record.cwd,\n createdAt: managed.record.createdAt,\n updatedAt: managed.presence.updatedAt,\n phase: managed.presence.phase,\n phaseSince: managed.presence.phaseSince,\n attach: managed.attach,\n ...(managed.attachReason === undefined ? {} : { attachReason: managed.attachReason }),\n pendingMessageCount: managed.presence.pendingMessageCount,\n everPrompted: managed.presence.everPrompted,\n awaitingInput: managed.presence.awaitingInput,\n ...(managed.presence.lastSettledAt === undefined ? {} : { lastSettledAt: managed.presence.lastSettledAt }),\n socketPath: managed.record.socketPath,\n ...(managed.record.apiSocketPath === undefined ? {} : { apiSocketPath: managed.record.apiSocketPath }),\n ...(managed.git === undefined ? {} : { git: managed.git }),\n });\n\n const pushSummary = (managed: ManagedSession): void => {\n const summary = toSummary(managed);\n const json = JSON.stringify(summary);\n if (json === managed.lastSummaryJson) return;\n managed.lastSummaryJson = json;\n emit({ kind: 'upsert', session: summary });\n };\n\n const refreshGit = (managed: ManagedSession): void => {\n if (!readGit) return;\n void readGit(managed.record.cwd).then((git) => {\n if (closed || sessions.get(managed.record.id) !== managed) return;\n managed.git = git;\n pushSummary(managed);\n });\n };\n\n const startAttachment = (managed: ManagedSession): void => {\n let token: string;\n try {\n token = readTokenFile(managed.record);\n } catch (error) {\n // The record beat the token file, or perms are off; the registry poll\n // re-runs reconcile, which retries this until it works.\n managed.attachment = undefined;\n managed.attach = 'closed';\n managed.attachReason = `The token file is unreadable: ${error instanceof Error ? error.message : String(error)}`;\n pushSummary(managed);\n return;\n }\n managed.attachment = attachToSession({\n socketPath: managed.record.socketPath,\n ...(managed.record.apiSocketPath === undefined ? {} : { apiSocketPath: managed.record.apiSocketPath }),\n token,\n handlers: {\n onFrame: (frame) => {\n // The journal answer is the session's whole history. Each entry it\n // carries is re-emitted as the append it once was, so it travels the\n // same path as a live publish: into the ring for pages that\n // subscribe later, and straight out to the ones already watching.\n if (frame.type === 'response' && frame.command === GET_ENTRIES_COMMAND) {\n // Keep the whole transcript before publishing any of it: the page\n // pages back through this, and a read that arrives after the first\n // one is the newest picture of the same history.\n const whole = journalMessages(frame);\n managed.journal = whole.length > journalLimit ? whole.slice(-journalLimit) : whole;\n const restoredAt = new Date().toISOString();\n const refreshing = managed.restoredJournal;\n managed.restoredJournal = true;\n let presenceChanged = false;\n for (const entry of renderableJournalEntries(frame, restoreLimit)) {\n // Once the transcript is on the page the live stream carries\n // everything the agent does, and it carries it with no journal id\n // to match on. Publishing an answer twice is the cost of getting\n // that wrong, so a refresh only adds what no frame can carry.\n if (refreshing && !isUnreportedEntry(entry)) continue;\n const entryId = typeof entry.id === 'string' ? entry.id : undefined;\n if (entryId !== undefined) {\n if (managed.emittedEntryIds.has(entryId)) continue;\n managed.emittedEntryIds.add(entryId);\n }\n const restored = { type: ENTRY_APPENDED_TYPE, entry };\n managed.ring.record(restored);\n emit({ kind: 'frame', sessionId: managed.record.id, frame: restored });\n const next = presenceAfterRestoredEntry(managed.presence, entry, restoredAt);\n presenceChanged ||= next !== managed.presence;\n managed.presence = next;\n }\n // The rail introduces a session by what it has done, so a restored\n // transcript has to reach the summary as well as the timeline.\n if (presenceChanged) pushSummary(managed);\n return;\n }\n const wasPhase = managed.presence.phase;\n managed.ring.record(frame);\n const next = reducePresence(managed.presence, frame, new Date().toISOString());\n const changed = next !== managed.presence;\n managed.presence = next;\n emit({ kind: 'frame', sessionId: managed.record.id, frame });\n if (changed) pushSummary(managed);\n // A finished run is when the tree most plausibly changed.\n if (wasPhase !== 'idle' && next.phase === 'idle') refreshGit(managed);\n // A run boundary is the one moment a message can have been journalled\n // without any frame carrying it: an extension that prompts the agent\n // (autonomous voice dictating what it heard) sends the message\n // itself, so the only report of it is the journal. Re-reading here is\n // what puts it on the page; entries already published are skipped.\n if (wasPhase !== next.phase) managed.attachment?.send({ type: GET_ENTRIES_COMMAND });\n },\n onStatus: (status) => {\n managed.attach = status.state;\n managed.attachReason = status.reason;\n // A fresh attach is the moment to ask for the facts events do not\n // carry (name, pending count, streaming flags) and for the journal,\n // whose minor-mode entry this hub missed if the session predates it.\n if (status.state === 'attached') {\n managed.attachment?.send({ type: 'get_state' });\n managed.attachment?.send({ type: GET_ENTRIES_COMMAND });\n }\n pushSummary(managed);\n },\n },\n });\n };\n\n const startSession = (record: SessionRecord): void => {\n const managed: ManagedSession = {\n record,\n ring: createFrameRing(options.ringLimit),\n journal: [],\n presence: initialPresence(new Date().toISOString()),\n attach: 'connecting',\n emittedEntryIds: new Set<string>(),\n restoredJournal: false,\n };\n sessions.set(record.id, managed);\n options.onNotice?.(`session ${record.id} (${record.name}) appeared`);\n startAttachment(managed);\n pushSummary(managed);\n refreshGit(managed);\n for (const channel of startedChannels) channel.source.sessionAdded?.(scopeOf(record));\n };\n\n const reconcile = (records: SessionRecord[]): void => {\n const seen = new Set<string>();\n for (const record of records) {\n seen.add(record.id);\n const managed = sessions.get(record.id);\n if (!managed) {\n startSession(record);\n continue;\n }\n const reattach = attachRelevantChanged(managed.record, record);\n managed.record = record;\n if (reattach) {\n // A new pid means a restarted server, possibly with a rotated token.\n managed.attachment?.close();\n startAttachment(managed);\n } else if (!managed.attachment) {\n startAttachment(managed);\n }\n pushSummary(managed);\n }\n for (const [id, managed] of sessions) {\n if (seen.has(id)) continue;\n managed.attachment?.close();\n sessions.delete(id);\n for (const channel of startedChannels) channel.source.sessionRemoved?.(id);\n options.onNotice?.(`session ${id} left`);\n emit({ kind: 'removed', sessionId: id });\n }\n };\n\n options.source.subscribe(reconcile);\n const gitTimer = readGit\n ? setInterval(() => {\n for (const managed of sessions.values()) refreshGit(managed);\n }, options.gitRefreshMs ?? GIT_REFRESH_MS)\n : undefined;\n\n return {\n records() {\n return [...sessions.values()].map((managed) => managed.record);\n },\n snapshot() {\n return [...sessions.values()]\n .sort(\n (left, right) =>\n left.record.createdAt.localeCompare(right.record.createdAt) ||\n left.record.id.localeCompare(right.record.id),\n )\n .map(toSummary);\n },\n stop(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return { ok: false, code: 'unknown', error: 'Unknown session.' };\n // Single-session mode records this very process; killing it would take\n // the cockpit down with the session.\n if (managed.record.pid === process.pid) {\n return { ok: false, code: 'self', error: 'This session hosts the cockpit; stop it from its own terminal.' };\n }\n try {\n signal(managed.record.pid);\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n return { ok: false, code: 'signal_failed', error: `Could not signal the session server: ${reason}` };\n }\n options.onNotice?.(`asked session ${sessionId} to stop`);\n return { ok: true };\n },\n onEvent(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n backlog(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const { frames, dropped } = managed.ring.snapshot();\n return { type: SESSION_BACKLOG_TYPE, sessionId, frames, dropped };\n },\n history(sessionId, request) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const limit = Math.max(1, Math.min(request.limit ?? HISTORY_PAGE_SIZE, HISTORY_PAGE_SIZE));\n // The page names the oldest entry it holds, so the window ends where the\n // page begins. An unknown cursor means the page holds something this hub\n // never sent, which a re-read can cause; answering from the end is the\n // honest fallback and the page reconciles by id.\n const end = request.before === undefined ? managed.journal.length : indexOfEntry(managed.journal, request.before);\n const stop = end < 0 ? managed.journal.length : end;\n const start = Math.max(0, stop - limit);\n const window = managed.journal.slice(start, stop);\n const oldest = window[0];\n return {\n type: HISTORY_PAGE_TYPE,\n sessionId,\n frames: window.map((entry) => ({ type: ENTRY_APPENDED_TYPE, entry })),\n cursor: typeof oldest?.id === 'string' ? oldest.id : null,\n hasMore: start > 0,\n ...(request.before === undefined ? {} : { before: request.before }),\n };\n },\n channelTypes() {\n return startedChannels.map((channel) => channel.frameType);\n },\n channelFrames(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return [];\n const scope = scopeOf(managed.record);\n const frames: ChannelFrame[] = [];\n for (const channel of startedChannels) {\n const payload = channel.source.payloadFor(scope);\n if (payload !== undefined) frames.push({ type: channel.frameType, sessionId, payload });\n }\n return frames;\n },\n threadJournal(sessionId, threadId) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const scope = scopeOf(managed.record);\n for (const channel of startedChannels) {\n const journal = channel.source.threadJournal?.(scope, threadId);\n if (journal !== undefined) return journal;\n }\n return undefined;\n },\n command(sessionId, frame) {\n const managed = sessions.get(sessionId);\n if (!managed) return;\n managed.attachment?.send(frame);\n // The agent never announces that a dialog was answered, so an\n // extension_ui_request in the ring would reopen on every replay. This\n // synthetic close travels the same path as agent frames: it closes the\n // dialog on other live tabs now and on backlog replays forever after.\n if (frame.type === 'extension_ui_response' && typeof frame.id === 'string') {\n const closed = { type: DIALOG_ANSWERED_TYPE, id: frame.id };\n managed.ring.record(closed);\n emit({ kind: 'frame', sessionId: managed.record.id, frame: closed });\n }\n const next = presenceAfterCommand(managed.presence, frame, new Date().toISOString());\n if (next !== managed.presence) {\n managed.presence = next;\n pushSummary(managed);\n }\n },\n create(input) {\n if (!options.spawner) {\n return Promise.resolve({\n ok: false,\n code: 'invalid_request',\n error: 'This cockpit serves a fixed session and cannot create new ones.',\n });\n }\n return options.spawner.spawn(input);\n },\n close() {\n closed = true;\n options.source.close();\n for (const channel of startedChannels) channel.source.close();\n if (gitTimer) clearInterval(gitTimer);\n for (const managed of sessions.values()) {\n managed.attachment?.close();\n }\n sessions.clear();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{DIALOG_ANSWERED_TYPE as e,HISTORY_PAGE_TYPE as t,MINOR_MODE_ENTRY_TYPE as n,SESSION_BACKLOG_TYPE as r}from"../types/hub.mjs";import{createFrameRing as i}from"../services/frameRing.mjs";import{initialPresence as a,presenceAfterCommand as o,presenceAfterRestoredEntry as s,reducePresence as c}from"../services/sessionPresence.mjs";import{attachToSession as l}from"./sessionSocketClient.mjs";import u from"node:fs";const d=`get_entries`,f=`entry_appended`;function p(e,t){let n=e.data;if(typeof n!=`object`||!n)return[];let r=n.entries;if(!Array.isArray(r))return[];let i=[],a;for(let e of r){if(typeof e!=`object`||!e)continue;let t=e;t.type===`message`?i.push(t):t.type===`custom`&&t.customType===`doom-minor-modes`&&(a=t)}let o=i.length>t?i.slice(-t):i;return a===void 0?o:[...o,a]}function m(e){let t=e.data;if(typeof t!=`object`||!t)return[];let n=t.entries;if(!Array.isArray(n))return[];let r=[];for(let e of n){if(typeof e!=`object`||!e)continue;let t=e;t.type===`message`&&r.push(t)}return r}function h(e){if(e.type===`custom`)return e.customType===n;let t=e.message;return typeof t!=`object`||!t?!1:t.role===`user`}function g(e,t){return e.findIndex(e=>e.id===t)}function _(e){return u.readFileSync(e.tokenFile,`utf8`).trim()}function v(e,t){return e.socketPath!==t.socketPath||e.tokenFile!==t.tokenFile||e.pid!==t.pid}function y(e){return{sessionId:e.id,cwd:e.cwd}}function b(n){let u=new Map,b=new Set,x=n.readGit,S=n.restoreLimit??300,C=n.journalLimit??5e3,w=n.signal??(e=>{process.kill(e,`SIGTERM`)}),T=!1,E=e=>{for(let t of b)t(e)},D=(n.channels??[]).map(e=>({frameType:e.frameType,source:e.start({sessions:()=>[...u.values()].map(e=>y(e.record)),publish:(t,n)=>{T||E({kind:`channel`,frameType:e.frameType,sessionId:t,payload:n})},onNotice:e=>n.onNotice?.(e)})})),O=e=>({id:e.record.id,name:e.presence.sessionName??e.record.name,cwd:e.record.cwd,createdAt:e.record.createdAt,updatedAt:e.presence.updatedAt,phase:e.presence.phase,phaseSince:e.presence.phaseSince,attach:e.attach,...e.attachReason===void 0?{}:{attachReason:e.attachReason},pendingMessageCount:e.presence.pendingMessageCount,everPrompted:e.presence.everPrompted,awaitingInput:e.presence.awaitingInput,...e.presence.lastSettledAt===void 0?{}:{lastSettledAt:e.presence.lastSettledAt},socketPath:e.record.socketPath,...e.record.apiSocketPath===void 0?{}:{apiSocketPath:e.record.apiSocketPath},...e.git===void 0?{}:{git:e.git}}),k=e=>{let t=O(e),n=JSON.stringify(t);n!==e.lastSummaryJson&&(e.lastSummaryJson=n,E({kind:`upsert`,session:t}))},A=e=>{x&&x(e.record.cwd).then(t=>{T||u.get(e.record.id)!==e||(e.git=t,k(e))})},j=e=>{let t;try{t=_(e.record)}catch(t){e.attachment=void 0,e.attach=`closed`,e.attachReason=`The token file is unreadable: ${t instanceof Error?t.message:String(t)}`,k(e);return}e.attachment=l({socketPath:e.record.socketPath,...e.record.apiSocketPath===void 0?{}:{apiSocketPath:e.record.apiSocketPath},token:t,handlers:{onFrame:t=>{if(t.type===`response`&&t.command===d){let n=m(t);e.journal=n.length>C?n.slice(-C):n;let r=new Date().toISOString(),i=e.restoredJournal;e.restoredJournal=!0;let a=!1;for(let n of p(t,S)){if(i&&!h(n))continue;let t=typeof n.id==`string`?n.id:void 0;if(t!==void 0){if(e.emittedEntryIds.has(t))continue;e.emittedEntryIds.add(t)}let o={type:f,entry:n};e.ring.record(o),E({kind:`frame`,sessionId:e.record.id,frame:o});let c=s(e.presence,n,r);a||=c!==e.presence,e.presence=c}a&&k(e);return}let n=e.presence.phase;e.ring.record(t);let r=c(e.presence,t,new Date().toISOString()),i=r!==e.presence;e.presence=r,E({kind:`frame`,sessionId:e.record.id,frame:t}),i&&k(e),n!==`idle`&&r.phase===`idle`&&A(e),n!==r.phase&&e.attachment?.send({type:d})},onStatus:t=>{e.attach=t.state,e.attachReason=t.reason,t.state===`attached`&&(e.attachment?.send({type:`get_state`}),e.attachment?.send({type:d})),k(e)}}})},M=e=>{let t={record:e,ring:i(n.ringLimit),journal:[],presence:a(new Date().toISOString()),attach:`connecting`,emittedEntryIds:new Set,restoredJournal:!1};u.set(e.id,t),n.onNotice?.(`session ${e.id} (${e.name}) appeared`),j(t),k(t),A(t);for(let t of D)t.source.sessionAdded?.(y(e))};n.source.subscribe(e=>{let t=new Set;for(let n of e){t.add(n.id);let e=u.get(n.id);if(!e){M(n);continue}let r=v(e.record,n);e.record=n,r?(e.attachment?.close(),j(e)):e.attachment||j(e),k(e)}for(let[e,r]of u)if(!t.has(e)){r.attachment?.close(),u.delete(e);for(let t of D)t.source.sessionRemoved?.(e);n.onNotice?.(`session ${e} left`),E({kind:`removed`,sessionId:e})}});let N=x?setInterval(()=>{for(let e of u.values())A(e)},n.gitRefreshMs??1e4):void 0;return{records(){return[...u.values()].map(e=>e.record)},snapshot(){return[...u.values()].sort((e,t)=>e.record.createdAt.localeCompare(t.record.createdAt)||e.record.id.localeCompare(t.record.id)).map(O)},stop(e){let t=u.get(e);if(!t)return{ok:!1,code:`unknown`,error:`Unknown session.`};if(t.record.pid===process.pid)return{ok:!1,code:`self`,error:`This session hosts the cockpit; stop it from its own terminal.`};try{w(t.record.pid)}catch(e){return{ok:!1,code:`signal_failed`,error:`Could not signal the session server: ${e instanceof Error?e.message:String(e)}`}}return n.onNotice?.(`asked session ${e} to stop`),{ok:!0}},onEvent(e){return b.add(e),()=>b.delete(e)},backlog(e){let t=u.get(e);if(!t)return;let{frames:n,dropped:i}=t.ring.snapshot();return{type:r,sessionId:e,frames:n,dropped:i}},history(e,n){let r=u.get(e);if(!r)return;let i=Math.max(1,Math.min(n.limit??100,100)),a=n.before===void 0?r.journal.length:g(r.journal,n.before),o=a<0?r.journal.length:a,s=Math.max(0,o-i),c=r.journal.slice(s,o),l=c[0];return{type:t,sessionId:e,frames:c.map(e=>({type:f,entry:e})),cursor:typeof l?.id==`string`?l.id:null,hasMore:s>0,...n.before===void 0?{}:{before:n.before}}},channelTypes(){return D.map(e=>e.frameType)},channelFrames(e){let t=u.get(e);if(!t)return[];let n=y(t.record),r=[];for(let t of D){let i=t.source.payloadFor(n);i!==void 0&&r.push({type:t.frameType,sessionId:e,payload:i})}return r},threadJournal(e,t){let n=u.get(e);if(!n)return;let r=y(n.record);for(let e of D){let n=e.source.threadJournal?.(r,t);if(n!==void 0)return n}},command(t,n){let r=u.get(t);if(!r)return;if(r.attachment?.send(n),n.type===`extension_ui_response`&&typeof n.id==`string`){let t={type:e,id:n.id};r.ring.record(t),E({kind:`frame`,sessionId:r.record.id,frame:t})}let i=o(r.presence,n,new Date().toISOString());i!==r.presence&&(r.presence=i,k(r))},create(e){return n.spawner?n.spawner.spawn(e):Promise.resolve({ok:!1,code:`invalid_request`,error:`This cockpit serves a fixed session and cannot create new ones.`})},close(){T=!0,n.source.close();for(let e of D)e.source.close();N&&clearInterval(N);for(let e of u.values())e.attachment?.close();u.clear(),b.clear()}}}export{b as createSessionHub};
2
+ //# sourceMappingURL=sessionHub.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionHub.mjs","names":[],"sources":["../../../src/adapters/sessionHub.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { ChannelFrame, HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { createFrameRing, type FrameRing } from '../services/frameRing.ts';\nimport {\n initialPresence,\n presenceAfterCommand,\n presenceAfterRestoredEntry,\n reducePresence,\n type SessionPresence,\n} from '../services/sessionPresence.ts';\nimport type { SessionAttachment } from '../types/bridge.ts';\nimport {\n DIALOG_ANSWERED_TYPE,\n HISTORY_PAGE_SIZE,\n HISTORY_PAGE_TYPE,\n type HistoryPageFrame,\n MINOR_MODE_ENTRY_TYPE,\n SESSION_BACKLOG_TYPE,\n type SessionBacklogFrame,\n type SessionGitStatus,\n type SessionSummary,\n} from '../types/hub.ts';\nimport type { SessionRecord } from '../types/registry.ts';\nimport type { BridgeState, SessionFrame } from '../types/session.ts';\nimport type { RecordSource } from './registryWatcher.ts';\nimport type { SessionSpawner, SpawnOutcome, SpawnSessionInput } from './serverSpawner.ts';\nimport { attachToSession } from './sessionSocketClient.ts';\n\nconst GIT_REFRESH_MS = 10_000;\nconst GET_ENTRIES_COMMAND = 'get_entries';\n/**\n * How much of a long transcript a fresh attach restores. The ring holds 512\n * frames and live ones have to fit beside the history, so a session with\n * thousands of messages gives up its oldest rather than its newest.\n */\nconst DEFAULT_RESTORE_LIMIT = 300;\n/**\n * Journalled messages retained per session for paging back through. Well past\n * any transcript a reader scrolls by hand, and still bounded: a hub watching a\n * dozen long sessions holds a list, not a session's whole life.\n */\nconst DEFAULT_JOURNAL_LIMIT = 5000;\nconst ENTRY_APPENDED_TYPE = 'entry_appended';\n\nexport type HubEvent =\n | { kind: 'upsert'; session: SessionSummary }\n | { kind: 'removed'; sessionId: string }\n | { kind: 'frame'; sessionId: string; frame: SessionFrame }\n | { kind: 'channel'; frameType: string; sessionId: string; payload: unknown };\n\nexport interface SessionHubOptions {\n source: RecordSource;\n /** Absent only in tests; without one, creating sessions is refused. */\n spawner?: SessionSpawner;\n /** Injectable for tests; defaults to asking git about the session cwd. */\n readGit?: (cwd: string) => Promise<SessionGitStatus | undefined>;\n /** The hub's data channels (built-in and plugin-provided sources). */\n channels?: readonly WebHubChannel[];\n /** Injectable for tests; defaults to SIGTERM, which doompi-server treats as a clean stop. */\n signal?: (pid: number) => void;\n ringLimit?: number;\n /** Journalled messages restored per session on attach; the rest is kept for paging back through. */\n restoreLimit?: number;\n /** Journalled messages retained per session for paging; the oldest are forgotten first. */\n journalLimit?: number;\n gitRefreshMs?: number;\n onNotice?: (message: string) => void;\n}\n\nexport type StopOutcome = { ok: true } | { ok: false; code: 'unknown' | 'self' | 'signal_failed'; error: string };\n\nexport interface SessionHub {\n snapshot(): SessionSummary[];\n /** The registry records behind the live sessions, for clients that dial them directly. */\n records(): SessionRecord[];\n /** Asks a session's server to exit; the session leaves once its record is withdrawn. */\n stop(sessionId: string): StopOutcome;\n /** Streams every change; pages filter frame events by their subscriptions. */\n onEvent(listener: (event: HubEvent) => void): () => void;\n /** Recent history for one session, or undefined for an unknown id. */\n backlog(sessionId: string): SessionBacklogFrame | undefined;\n /** One older window of a session's transcript, for a page scrolling back. */\n history(sessionId: string, request: { before?: string; limit?: number }): HistoryPageFrame | undefined;\n /** Frame types of the loaded data channels, for the hello frame. */\n channelTypes(): string[];\n /** Every channel's subscribe-time snapshot for one session; empty for an unknown id. */\n channelFrames(sessionId: string): ChannelFrame[];\n /** The journal behind one thread of a session, from the first data channel that names it; undefined until one does. */\n threadJournal(sessionId: string, threadId: string): string | undefined;\n command(sessionId: string, frame: SessionFrame): void;\n create(input: SpawnSessionInput): Promise<SpawnOutcome>;\n close(): void;\n}\n\ninterface ManagedSession {\n record: SessionRecord;\n attachment?: SessionAttachment;\n ring: FrameRing;\n presence: SessionPresence;\n attach: BridgeState;\n attachReason?: string;\n git?: SessionGitStatus;\n lastSummaryJson?: string;\n /**\n * Journal entry ids this hub has already published. Pi reports a message\n * entry only when the journal is read, never as it is written, so the hub\n * re-reads on each run boundary; remembering what it sent is what keeps that\n * re-read from replaying the whole transcript every time.\n */\n emittedEntryIds: Set<string>;\n /** Whether the first journal read has been published; later reads are refreshes. */\n restoredJournal: boolean;\n /**\n * Every journalled message the session has reported, oldest first.\n *\n * The attach path publishes only the newest slice, so without this the rest\n * of a long transcript would exist only on disk and a reader scrolling up\n * would find nothing. Bounded, because a hub watching many long sessions\n * should not grow without limit; the oldest go first, which is also the\n * order a reader runs out of interest in them.\n */\n journal: Record<string, unknown>[];\n}\n\ninterface StartedChannel {\n frameType: string;\n source: HubChannelSource;\n}\n\n/**\n * The journal entries a cockpit can render, oldest first.\n *\n * A session outlives every hub that watches it: it may have been driven from\n * the TUI for an hour before a cockpit existed, and a hub restart forgets the\n * live ring entirely. Without this the timeline of a long-running session\n * opens empty, which reads as \"the agent has done nothing\" rather than \"this\n * page arrived late\". The journal is the session's own record, so replaying it\n * is the only honest way to show what came before.\n *\n * Two kinds survive the filter: the messages that are the transcript, and the\n * newest minor-mode catalog entry, which the runtime journals as a custom\n * entry and which a late hub would otherwise never see.\n */\nfunction renderableJournalEntries(frame: SessionFrame, limit: number): Record<string, unknown>[] {\n const data = frame.data;\n if (typeof data !== 'object' || data === null) return [];\n const entries = (data as { entries?: unknown }).entries;\n if (!Array.isArray(entries)) return [];\n\n const messages: Record<string, unknown>[] = [];\n let minorModes: Record<string, unknown> | undefined;\n for (const entry of entries) {\n if (typeof entry !== 'object' || entry === null) continue;\n const candidate = entry as Record<string, unknown>;\n if (candidate.type === 'message') messages.push(candidate);\n else if (candidate.type === 'custom' && candidate.customType === MINOR_MODE_ENTRY_TYPE) minorModes = candidate;\n }\n\n // The ring is bounded and live frames must still fit, so only the tail of a\n // long transcript is published on attach; the rest is retained for the page\n // to page back through rather than dropped on the floor.\n const kept = messages.length > limit ? messages.slice(-limit) : messages;\n return minorModes === undefined ? kept : [...kept, minorModes];\n}\n\n/**\n * Every message a journal read carries, oldest first, with no tail limit.\n *\n * This is what the page pages back through. The attach path publishes only the\n * newest slice, because the ring is bounded and the live stream has to fit\n * beside it, but a reader scrolling up wants what came before, and the session\n * already told the hub all of it.\n */\nfunction journalMessages(frame: SessionFrame): Record<string, unknown>[] {\n const data = frame.data;\n if (typeof data !== 'object' || data === null) return [];\n const entries = (data as { entries?: unknown }).entries;\n if (!Array.isArray(entries)) return [];\n const messages: Record<string, unknown>[] = [];\n for (const entry of entries) {\n if (typeof entry !== 'object' || entry === null) continue;\n const candidate = entry as Record<string, unknown>;\n if (candidate.type === 'message') messages.push(candidate);\n }\n return messages;\n}\n\n/**\n * Whether a journal entry is one no live frame reports.\n *\n * Pi publishes a frame for everything the agent does, but a user message only\n * ever reaches the journal: the agent did not produce it. That is invisible\n * when something other than a cockpit sends it, which is how autonomous voice\n * prompts the agent with what it heard. The catalog entry is here because\n * re-reading it only replaces a projection, so a refresh cannot double it.\n */\nfunction isUnreportedEntry(entry: Record<string, unknown>): boolean {\n if (entry.type === 'custom') return entry.customType === MINOR_MODE_ENTRY_TYPE;\n const message = entry.message;\n if (typeof message !== 'object' || message === null) return false;\n return (message as { role?: unknown }).role === 'user';\n}\n\n/** Where an entry sits in the retained journal, or -1 when this hub never held it. */\nfunction indexOfEntry(journal: readonly Record<string, unknown>[], id: string): number {\n return journal.findIndex((entry) => entry.id === id);\n}\n\nfunction readTokenFile(record: SessionRecord): string {\n return fs.readFileSync(record.tokenFile, 'utf8').trim();\n}\n\nfunction attachRelevantChanged(previous: SessionRecord, next: SessionRecord): boolean {\n return previous.socketPath !== next.socketPath || previous.tokenFile !== next.tokenFile || previous.pid !== next.pid;\n}\n\nfunction scopeOf(record: SessionRecord): HubSessionScope {\n return { sessionId: record.id, cwd: record.cwd };\n}\n\n/**\n * Holds the hub's live view of every registered session.\n *\n * One attachment per session: the hub is \"the one client\" each socket allows,\n * and every page multiplexes behind it. Presence is reduced from the frame\n * stream so the rail describes sessions nobody is viewing, and a bounded ring\n * per session gives late pages their history, since a permanently attached\n * socket never fills the server-side backlog. Session data beyond the agent\n * stream comes from channels: each one watches its own source and publishes\n * per-session payloads the hub fans out by frame type.\n */\nexport function createSessionHub(options: SessionHubOptions): SessionHub {\n const sessions = new Map<string, ManagedSession>();\n const listeners = new Set<(event: HubEvent) => void>();\n const readGit = options.readGit;\n const restoreLimit = options.restoreLimit ?? DEFAULT_RESTORE_LIMIT;\n const journalLimit = options.journalLimit ?? DEFAULT_JOURNAL_LIMIT;\n const signal =\n options.signal ??\n ((pid: number): void => {\n process.kill(pid, 'SIGTERM');\n });\n let closed = false;\n\n const emit = (event: HubEvent): void => {\n for (const listener of listeners) listener(event);\n };\n\n const startedChannels: StartedChannel[] = (options.channels ?? []).map((channel) => ({\n frameType: channel.frameType,\n source: channel.start({\n sessions: () => [...sessions.values()].map((managed) => scopeOf(managed.record)),\n publish: (sessionId, payload) => {\n if (closed) return;\n emit({ kind: 'channel', frameType: channel.frameType, sessionId, payload });\n },\n onNotice: (message) => options.onNotice?.(message),\n }),\n }));\n\n const toSummary = (managed: ManagedSession): SessionSummary => ({\n id: managed.record.id,\n name: managed.presence.sessionName ?? managed.record.name,\n cwd: managed.record.cwd,\n createdAt: managed.record.createdAt,\n updatedAt: managed.presence.updatedAt,\n phase: managed.presence.phase,\n phaseSince: managed.presence.phaseSince,\n attach: managed.attach,\n ...(managed.attachReason === undefined ? {} : { attachReason: managed.attachReason }),\n pendingMessageCount: managed.presence.pendingMessageCount,\n everPrompted: managed.presence.everPrompted,\n awaitingInput: managed.presence.awaitingInput,\n ...(managed.presence.lastSettledAt === undefined ? {} : { lastSettledAt: managed.presence.lastSettledAt }),\n socketPath: managed.record.socketPath,\n ...(managed.record.apiSocketPath === undefined ? {} : { apiSocketPath: managed.record.apiSocketPath }),\n ...(managed.git === undefined ? {} : { git: managed.git }),\n });\n\n const pushSummary = (managed: ManagedSession): void => {\n const summary = toSummary(managed);\n const json = JSON.stringify(summary);\n if (json === managed.lastSummaryJson) return;\n managed.lastSummaryJson = json;\n emit({ kind: 'upsert', session: summary });\n };\n\n const refreshGit = (managed: ManagedSession): void => {\n if (!readGit) return;\n void readGit(managed.record.cwd).then((git) => {\n if (closed || sessions.get(managed.record.id) !== managed) return;\n managed.git = git;\n pushSummary(managed);\n });\n };\n\n const startAttachment = (managed: ManagedSession): void => {\n let token: string;\n try {\n token = readTokenFile(managed.record);\n } catch (error) {\n // The record beat the token file, or perms are off; the registry poll\n // re-runs reconcile, which retries this until it works.\n managed.attachment = undefined;\n managed.attach = 'closed';\n managed.attachReason = `The token file is unreadable: ${error instanceof Error ? error.message : String(error)}`;\n pushSummary(managed);\n return;\n }\n managed.attachment = attachToSession({\n socketPath: managed.record.socketPath,\n ...(managed.record.apiSocketPath === undefined ? {} : { apiSocketPath: managed.record.apiSocketPath }),\n token,\n handlers: {\n onFrame: (frame) => {\n // The journal answer is the session's whole history. Each entry it\n // carries is re-emitted as the append it once was, so it travels the\n // same path as a live publish: into the ring for pages that\n // subscribe later, and straight out to the ones already watching.\n if (frame.type === 'response' && frame.command === GET_ENTRIES_COMMAND) {\n // Keep the whole transcript before publishing any of it: the page\n // pages back through this, and a read that arrives after the first\n // one is the newest picture of the same history.\n const whole = journalMessages(frame);\n managed.journal = whole.length > journalLimit ? whole.slice(-journalLimit) : whole;\n const restoredAt = new Date().toISOString();\n const refreshing = managed.restoredJournal;\n managed.restoredJournal = true;\n let presenceChanged = false;\n for (const entry of renderableJournalEntries(frame, restoreLimit)) {\n // Once the transcript is on the page the live stream carries\n // everything the agent does, and it carries it with no journal id\n // to match on. Publishing an answer twice is the cost of getting\n // that wrong, so a refresh only adds what no frame can carry.\n if (refreshing && !isUnreportedEntry(entry)) continue;\n const entryId = typeof entry.id === 'string' ? entry.id : undefined;\n if (entryId !== undefined) {\n if (managed.emittedEntryIds.has(entryId)) continue;\n managed.emittedEntryIds.add(entryId);\n }\n const restored = { type: ENTRY_APPENDED_TYPE, entry };\n managed.ring.record(restored);\n emit({ kind: 'frame', sessionId: managed.record.id, frame: restored });\n const next = presenceAfterRestoredEntry(managed.presence, entry, restoredAt);\n presenceChanged ||= next !== managed.presence;\n managed.presence = next;\n }\n // The rail introduces a session by what it has done, so a restored\n // transcript has to reach the summary as well as the timeline.\n if (presenceChanged) pushSummary(managed);\n return;\n }\n const wasPhase = managed.presence.phase;\n managed.ring.record(frame);\n const next = reducePresence(managed.presence, frame, new Date().toISOString());\n const changed = next !== managed.presence;\n managed.presence = next;\n emit({ kind: 'frame', sessionId: managed.record.id, frame });\n if (changed) pushSummary(managed);\n // A finished run is when the tree most plausibly changed.\n if (wasPhase !== 'idle' && next.phase === 'idle') refreshGit(managed);\n // A run boundary is the one moment a message can have been journalled\n // without any frame carrying it: an extension that prompts the agent\n // (autonomous voice dictating what it heard) sends the message\n // itself, so the only report of it is the journal. Re-reading here is\n // what puts it on the page; entries already published are skipped.\n if (wasPhase !== next.phase) managed.attachment?.send({ type: GET_ENTRIES_COMMAND });\n },\n onStatus: (status) => {\n managed.attach = status.state;\n managed.attachReason = status.reason;\n // A fresh attach is the moment to ask for the facts events do not\n // carry (name, pending count, streaming flags) and for the journal,\n // whose minor-mode entry this hub missed if the session predates it.\n if (status.state === 'attached') {\n managed.attachment?.send({ type: 'get_state' });\n managed.attachment?.send({ type: GET_ENTRIES_COMMAND });\n }\n pushSummary(managed);\n },\n },\n });\n };\n\n const startSession = (record: SessionRecord): void => {\n const managed: ManagedSession = {\n record,\n ring: createFrameRing(options.ringLimit),\n journal: [],\n presence: initialPresence(new Date().toISOString()),\n attach: 'connecting',\n emittedEntryIds: new Set<string>(),\n restoredJournal: false,\n };\n sessions.set(record.id, managed);\n options.onNotice?.(`session ${record.id} (${record.name}) appeared`);\n startAttachment(managed);\n pushSummary(managed);\n refreshGit(managed);\n for (const channel of startedChannels) channel.source.sessionAdded?.(scopeOf(record));\n };\n\n const reconcile = (records: SessionRecord[]): void => {\n const seen = new Set<string>();\n for (const record of records) {\n seen.add(record.id);\n const managed = sessions.get(record.id);\n if (!managed) {\n startSession(record);\n continue;\n }\n const reattach = attachRelevantChanged(managed.record, record);\n managed.record = record;\n if (reattach) {\n // A new pid means a restarted server, possibly with a rotated token.\n managed.attachment?.close();\n startAttachment(managed);\n } else if (!managed.attachment) {\n startAttachment(managed);\n }\n pushSummary(managed);\n }\n for (const [id, managed] of sessions) {\n if (seen.has(id)) continue;\n managed.attachment?.close();\n sessions.delete(id);\n for (const channel of startedChannels) channel.source.sessionRemoved?.(id);\n options.onNotice?.(`session ${id} left`);\n emit({ kind: 'removed', sessionId: id });\n }\n };\n\n options.source.subscribe(reconcile);\n const gitTimer = readGit\n ? setInterval(() => {\n for (const managed of sessions.values()) refreshGit(managed);\n }, options.gitRefreshMs ?? GIT_REFRESH_MS)\n : undefined;\n\n return {\n records() {\n return [...sessions.values()].map((managed) => managed.record);\n },\n snapshot() {\n return [...sessions.values()]\n .sort(\n (left, right) =>\n left.record.createdAt.localeCompare(right.record.createdAt) ||\n left.record.id.localeCompare(right.record.id),\n )\n .map(toSummary);\n },\n stop(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return { ok: false, code: 'unknown', error: 'Unknown session.' };\n // Single-session mode records this very process; killing it would take\n // the cockpit down with the session.\n if (managed.record.pid === process.pid) {\n return { ok: false, code: 'self', error: 'This session hosts the cockpit; stop it from its own terminal.' };\n }\n try {\n signal(managed.record.pid);\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n return { ok: false, code: 'signal_failed', error: `Could not signal the session server: ${reason}` };\n }\n options.onNotice?.(`asked session ${sessionId} to stop`);\n return { ok: true };\n },\n onEvent(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n backlog(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const { frames, dropped } = managed.ring.snapshot();\n return { type: SESSION_BACKLOG_TYPE, sessionId, frames, dropped };\n },\n history(sessionId, request) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const limit = Math.max(1, Math.min(request.limit ?? HISTORY_PAGE_SIZE, HISTORY_PAGE_SIZE));\n // The page names the oldest entry it holds, so the window ends where the\n // page begins. An unknown cursor means the page holds something this hub\n // never sent, which a re-read can cause; answering from the end is the\n // honest fallback and the page reconciles by id.\n const end = request.before === undefined ? managed.journal.length : indexOfEntry(managed.journal, request.before);\n const stop = end < 0 ? managed.journal.length : end;\n const start = Math.max(0, stop - limit);\n const window = managed.journal.slice(start, stop);\n const oldest = window[0];\n return {\n type: HISTORY_PAGE_TYPE,\n sessionId,\n frames: window.map((entry) => ({ type: ENTRY_APPENDED_TYPE, entry })),\n cursor: typeof oldest?.id === 'string' ? oldest.id : null,\n hasMore: start > 0,\n ...(request.before === undefined ? {} : { before: request.before }),\n };\n },\n channelTypes() {\n return startedChannels.map((channel) => channel.frameType);\n },\n channelFrames(sessionId) {\n const managed = sessions.get(sessionId);\n if (!managed) return [];\n const scope = scopeOf(managed.record);\n const frames: ChannelFrame[] = [];\n for (const channel of startedChannels) {\n const payload = channel.source.payloadFor(scope);\n if (payload !== undefined) frames.push({ type: channel.frameType, sessionId, payload });\n }\n return frames;\n },\n threadJournal(sessionId, threadId) {\n const managed = sessions.get(sessionId);\n if (!managed) return undefined;\n const scope = scopeOf(managed.record);\n for (const channel of startedChannels) {\n const journal = channel.source.threadJournal?.(scope, threadId);\n if (journal !== undefined) return journal;\n }\n return undefined;\n },\n command(sessionId, frame) {\n const managed = sessions.get(sessionId);\n if (!managed) return;\n managed.attachment?.send(frame);\n // The agent never announces that a dialog was answered, so an\n // extension_ui_request in the ring would reopen on every replay. This\n // synthetic close travels the same path as agent frames: it closes the\n // dialog on other live tabs now and on backlog replays forever after.\n if (frame.type === 'extension_ui_response' && typeof frame.id === 'string') {\n const closed = { type: DIALOG_ANSWERED_TYPE, id: frame.id };\n managed.ring.record(closed);\n emit({ kind: 'frame', sessionId: managed.record.id, frame: closed });\n }\n const next = presenceAfterCommand(managed.presence, frame, new Date().toISOString());\n if (next !== managed.presence) {\n managed.presence = next;\n pushSummary(managed);\n }\n },\n create(input) {\n if (!options.spawner) {\n return Promise.resolve({\n ok: false,\n code: 'invalid_request',\n error: 'This cockpit serves a fixed session and cannot create new ones.',\n });\n }\n return options.spawner.spawn(input);\n },\n close() {\n closed = true;\n options.source.close();\n for (const channel of startedChannels) channel.source.close();\n if (gitTimer) clearInterval(gitTimer);\n for (const managed of sessions.values()) {\n managed.attachment?.close();\n }\n sessions.clear();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../types/session.cjs"),n=require("../services/retryPolicy.cjs"),r=require("../services/sessionFraming.cjs");let i=require("node:net");i=e.__toESM(i,1);function a(e){return typeof e==`number`&&Number.isFinite(e)?e:0}function o(e){let o,s,c=0,l=!1,u=!1,d=[],f=()=>{if(l)return;u=!1,e.handlers.onStatus(t.bridgeStatus(`connecting`));let p=r.createFrameDecoder(),m=i.default.connect(e.socketPath);o=m,m.setEncoding(`utf8`),m.on(`connect`,()=>{m.write(r.encodeFrame({type:t.ATTACH_TYPE,token:e.token}))}),m.on(`data`,n=>{let i;try{i=p(n)}catch{e.handlers.onStatus(t.bridgeStatus(`closed`,{reason:`The session sent a malformed frame.`})),m.destroy();return}for(let n of i){if(n.type===`attached`){u=!0,c=0,e.handlers.onStatus(t.bridgeStatus(`attached`,{replayed:a(n.replayed),dropped:a(n.dropped)}));for(let e of d.splice(0))m.write(r.encodeFrame(e));continue}if(n.type===`attach_error`){let r=typeof n.reason==`string`?n.reason:`The session refused this client.`;e.handlers.onStatus(t.bridgeStatus(`refused`,{reason:r}));continue}e.handlers.onFrame(n)}}),m.on(`close`,()=>{l||(o===m&&(o=void 0),e.handlers.onStatus(t.bridgeStatus(u?`detached`:`connecting`)),c+=1,s=setTimeout(f,n.reattachDelayMs(c)))}),m.on(`error`,()=>{})};return f(),{send(e){if(!l){if(!u||!o){d.push(e),d.length>64&&d.shift();return}o.write(r.encodeFrame(e))}},close(){l=!0,s&&clearTimeout(s),o?.destroy(),o=void 0}}}exports.attachToSession=o;
2
+ //# sourceMappingURL=sessionSocketClient.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionSocketClient.cjs","names":["bridgeStatus","createFrameDecoder","net","encodeFrame","ATTACH_TYPE","reattachDelayMs"],"sources":["../../../src/adapters/sessionSocketClient.ts"],"sourcesContent":["import net from 'node:net';\nimport { reattachDelayMs } from '../services/retryPolicy.ts';\nimport { createFrameDecoder, encodeFrame } from '../services/sessionFraming.ts';\nimport type { AttachOptions, SessionAttachment } from '../types/bridge.ts';\nimport { ATTACH_ERROR_TYPE, ATTACH_TYPE, ATTACHED_TYPE, bridgeStatus, type SessionFrame } from '../types/session.ts';\n\n/** Commands held while the handshake is in flight; beyond this the oldest go. */\nconst PENDING_LIMIT = 64;\n\nfunction asCount(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Holds one authenticated attachment to a doompi-server socket.\n *\n * Reattaching is the normal path, not error handling: the server keeps the\n * agent alive across a dropped client and replays what was missed, so a\n * reloaded page recovers instead of losing the run.\n */\nexport function attachToSession(options: AttachOptions): SessionAttachment {\n let socket: net.Socket | undefined;\n let timer: NodeJS.Timeout | undefined;\n let attempt = 0;\n let stopped = false;\n let authenticated = false;\n const pending: SessionFrame[] = [];\n\n const connect = (): void => {\n if (stopped) return;\n authenticated = false;\n options.handlers.onStatus(bridgeStatus('connecting'));\n const decode = createFrameDecoder();\n const connection = net.connect(options.socketPath);\n socket = connection;\n connection.setEncoding('utf8');\n\n connection.on('connect', () => {\n connection.write(encodeFrame({ type: ATTACH_TYPE, token: options.token }));\n });\n\n connection.on('data', (chunk: string) => {\n let frames: SessionFrame[];\n try {\n frames = decode(chunk);\n } catch {\n options.handlers.onStatus(bridgeStatus('closed', { reason: 'The session sent a malformed frame.' }));\n connection.destroy();\n return;\n }\n for (const frame of frames) {\n if (frame.type === ATTACHED_TYPE) {\n authenticated = true;\n attempt = 0;\n options.handlers.onStatus(\n bridgeStatus('attached', { replayed: asCount(frame.replayed), dropped: asCount(frame.dropped) }),\n );\n for (const queued of pending.splice(0)) connection.write(encodeFrame(queued));\n continue;\n }\n if (frame.type === ATTACH_ERROR_TYPE) {\n const reason = typeof frame.reason === 'string' ? frame.reason : 'The session refused this client.';\n options.handlers.onStatus(bridgeStatus('refused', { reason }));\n continue;\n }\n options.handlers.onFrame(frame);\n }\n });\n\n const retry = (): void => {\n if (stopped) return;\n if (socket === connection) socket = undefined;\n options.handlers.onStatus(bridgeStatus(authenticated ? 'detached' : 'connecting'));\n attempt += 1;\n timer = setTimeout(connect, reattachDelayMs(attempt));\n };\n\n connection.on('close', retry);\n connection.on('error', () => {\n // 'close' always follows, and it owns the retry.\n });\n };\n\n connect();\n\n return {\n send(frame) {\n if (stopped) return;\n if (!authenticated || !socket) {\n // A command that lands while the hub is (re)attaching, typically right\n // after a session restart, waits for the handshake instead of vanishing.\n pending.push(frame);\n if (pending.length > PENDING_LIMIT) pending.shift();\n return;\n }\n socket.write(encodeFrame(frame));\n },\n close() {\n stopped = true;\n if (timer) clearTimeout(timer);\n socket?.destroy();\n socket = undefined;\n },\n };\n}\n"],"mappings":"0NASA,SAAS,EAAQ,EAAwB,CACvC,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,EAAI,EAAQ,CACvE,CASA,SAAgB,EAAgB,EAA2C,CACzE,IAAI,EACA,EACA,EAAU,EACV,EAAU,GACV,EAAgB,GACd,EAA0B,CAAC,EAE3B,MAAsB,CAC1B,GAAI,EAAS,OACb,EAAgB,GAChB,EAAQ,SAAS,SAASA,EAAAA,aAAa,YAAY,CAAC,EACpD,IAAM,EAASC,EAAAA,mBAAmB,EAC5B,EAAaC,EAAAA,QAAI,QAAQ,EAAQ,UAAU,EACjD,EAAS,EACT,EAAW,YAAY,MAAM,EAE7B,EAAW,GAAG,cAAiB,CAC7B,EAAW,MAAMC,EAAAA,YAAY,CAAE,KAAMC,EAAAA,YAAa,MAAO,EAAQ,KAAM,CAAC,CAAC,CAC3E,CAAC,EAED,EAAW,GAAG,OAAS,GAAkB,CACvC,IAAI,EACJ,GAAI,CACF,EAAS,EAAO,CAAK,CACvB,MAAQ,CACN,EAAQ,SAAS,SAASJ,EAAAA,aAAa,SAAU,CAAE,OAAQ,qCAAsC,CAAC,CAAC,EACnG,EAAW,QAAQ,EACnB,MACF,CACA,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,OAAA,WAAwB,CAChC,EAAgB,GAChB,EAAU,EACV,EAAQ,SAAS,SACfA,EAAAA,aAAa,WAAY,CAAE,SAAU,EAAQ,EAAM,QAAQ,EAAG,QAAS,EAAQ,EAAM,OAAO,CAAE,CAAC,CACjG,EACA,IAAK,IAAM,KAAU,EAAQ,OAAO,CAAC,EAAG,EAAW,MAAMG,EAAAA,YAAY,CAAM,CAAC,EAC5E,QACF,CACA,GAAI,EAAM,OAAA,eAA4B,CACpC,IAAM,EAAS,OAAO,EAAM,QAAW,SAAW,EAAM,OAAS,mCACjE,EAAQ,SAAS,SAASH,EAAAA,aAAa,UAAW,CAAE,QAAO,CAAC,CAAC,EAC7D,QACF,CACA,EAAQ,SAAS,QAAQ,CAAK,CAChC,CACF,CAAC,EAUD,EAAW,GAAG,YARY,CACpB,IACA,IAAW,IAAY,EAAS,IAAA,IACpC,EAAQ,SAAS,SAASA,EAAAA,aAAa,EAAgB,WAAa,YAAY,CAAC,EACjF,GAAW,EACX,EAAQ,WAAW,EAASK,EAAAA,gBAAgB,CAAO,CAAC,EACtD,CAE4B,EAC5B,EAAW,GAAG,YAAe,CAE7B,CAAC,CACH,EAIA,OAFA,EAAQ,EAED,CACL,KAAK,EAAO,CACN,MACJ,IAAI,CAAC,GAAiB,CAAC,EAAQ,CAG7B,EAAQ,KAAK,CAAK,EACd,EAAQ,OAAS,IAAe,EAAQ,MAAM,EAClD,MACF,CACA,EAAO,MAAMF,EAAAA,YAAY,CAAK,CAAC,CAD/B,CAEF,EACA,OAAQ,CACN,EAAU,GACN,GAAO,aAAa,CAAK,EAC7B,GAAQ,QAAQ,EAChB,EAAS,IAAA,EACX,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ import{ATTACH_TYPE as e,bridgeStatus as t}from"../types/session.mjs";import{reattachDelayMs as n}from"../services/retryPolicy.mjs";import{createFrameDecoder as r,encodeFrame as i}from"../services/sessionFraming.mjs";import a from"node:net";function o(e){return typeof e==`number`&&Number.isFinite(e)?e:0}function s(s){let c,l,u=0,d=!1,f=!1,p=[],m=()=>{if(d)return;f=!1,s.handlers.onStatus(t(`connecting`));let h=r(),g=a.connect(s.socketPath);c=g,g.setEncoding(`utf8`),g.on(`connect`,()=>{g.write(i({type:e,token:s.token}))}),g.on(`data`,e=>{let n;try{n=h(e)}catch{s.handlers.onStatus(t(`closed`,{reason:`The session sent a malformed frame.`})),g.destroy();return}for(let e of n){if(e.type===`attached`){f=!0,u=0,s.handlers.onStatus(t(`attached`,{replayed:o(e.replayed),dropped:o(e.dropped)}));for(let e of p.splice(0))g.write(i(e));continue}if(e.type===`attach_error`){let n=typeof e.reason==`string`?e.reason:`The session refused this client.`;s.handlers.onStatus(t(`refused`,{reason:n}));continue}s.handlers.onFrame(e)}}),g.on(`close`,()=>{d||(c===g&&(c=void 0),s.handlers.onStatus(t(f?`detached`:`connecting`)),u+=1,l=setTimeout(m,n(u)))}),g.on(`error`,()=>{})};return m(),{send(e){if(!d){if(!f||!c){p.push(e),p.length>64&&p.shift();return}c.write(i(e))}},close(){d=!0,l&&clearTimeout(l),c?.destroy(),c=void 0}}}export{s as attachToSession};
2
+ //# sourceMappingURL=sessionSocketClient.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionSocketClient.mjs","names":[],"sources":["../../../src/adapters/sessionSocketClient.ts"],"sourcesContent":["import net from 'node:net';\nimport { reattachDelayMs } from '../services/retryPolicy.ts';\nimport { createFrameDecoder, encodeFrame } from '../services/sessionFraming.ts';\nimport type { AttachOptions, SessionAttachment } from '../types/bridge.ts';\nimport { ATTACH_ERROR_TYPE, ATTACH_TYPE, ATTACHED_TYPE, bridgeStatus, type SessionFrame } from '../types/session.ts';\n\n/** Commands held while the handshake is in flight; beyond this the oldest go. */\nconst PENDING_LIMIT = 64;\n\nfunction asCount(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Holds one authenticated attachment to a doompi-server socket.\n *\n * Reattaching is the normal path, not error handling: the server keeps the\n * agent alive across a dropped client and replays what was missed, so a\n * reloaded page recovers instead of losing the run.\n */\nexport function attachToSession(options: AttachOptions): SessionAttachment {\n let socket: net.Socket | undefined;\n let timer: NodeJS.Timeout | undefined;\n let attempt = 0;\n let stopped = false;\n let authenticated = false;\n const pending: SessionFrame[] = [];\n\n const connect = (): void => {\n if (stopped) return;\n authenticated = false;\n options.handlers.onStatus(bridgeStatus('connecting'));\n const decode = createFrameDecoder();\n const connection = net.connect(options.socketPath);\n socket = connection;\n connection.setEncoding('utf8');\n\n connection.on('connect', () => {\n connection.write(encodeFrame({ type: ATTACH_TYPE, token: options.token }));\n });\n\n connection.on('data', (chunk: string) => {\n let frames: SessionFrame[];\n try {\n frames = decode(chunk);\n } catch {\n options.handlers.onStatus(bridgeStatus('closed', { reason: 'The session sent a malformed frame.' }));\n connection.destroy();\n return;\n }\n for (const frame of frames) {\n if (frame.type === ATTACHED_TYPE) {\n authenticated = true;\n attempt = 0;\n options.handlers.onStatus(\n bridgeStatus('attached', { replayed: asCount(frame.replayed), dropped: asCount(frame.dropped) }),\n );\n for (const queued of pending.splice(0)) connection.write(encodeFrame(queued));\n continue;\n }\n if (frame.type === ATTACH_ERROR_TYPE) {\n const reason = typeof frame.reason === 'string' ? frame.reason : 'The session refused this client.';\n options.handlers.onStatus(bridgeStatus('refused', { reason }));\n continue;\n }\n options.handlers.onFrame(frame);\n }\n });\n\n const retry = (): void => {\n if (stopped) return;\n if (socket === connection) socket = undefined;\n options.handlers.onStatus(bridgeStatus(authenticated ? 'detached' : 'connecting'));\n attempt += 1;\n timer = setTimeout(connect, reattachDelayMs(attempt));\n };\n\n connection.on('close', retry);\n connection.on('error', () => {\n // 'close' always follows, and it owns the retry.\n });\n };\n\n connect();\n\n return {\n send(frame) {\n if (stopped) return;\n if (!authenticated || !socket) {\n // A command that lands while the hub is (re)attaching, typically right\n // after a session restart, waits for the handshake instead of vanishing.\n pending.push(frame);\n if (pending.length > PENDING_LIMIT) pending.shift();\n return;\n }\n socket.write(encodeFrame(frame));\n },\n close() {\n stopped = true;\n if (timer) clearTimeout(timer);\n socket?.destroy();\n socket = undefined;\n },\n };\n}\n"],"mappings":"gPASA,SAAS,EAAQ,EAAwB,CACvC,OAAO,OAAO,GAAU,UAAY,OAAO,SAAS,CAAK,EAAI,EAAQ,CACvE,CASA,SAAgB,EAAgB,EAA2C,CACzE,IAAI,EACA,EACA,EAAU,EACV,EAAU,GACV,EAAgB,GACd,EAA0B,CAAC,EAE3B,MAAsB,CAC1B,GAAI,EAAS,OACb,EAAgB,GAChB,EAAQ,SAAS,SAAS,EAAa,YAAY,CAAC,EACpD,IAAM,EAAS,EAAmB,EAC5B,EAAa,EAAI,QAAQ,EAAQ,UAAU,EACjD,EAAS,EACT,EAAW,YAAY,MAAM,EAE7B,EAAW,GAAG,cAAiB,CAC7B,EAAW,MAAM,EAAY,CAAE,KAAM,EAAa,MAAO,EAAQ,KAAM,CAAC,CAAC,CAC3E,CAAC,EAED,EAAW,GAAG,OAAS,GAAkB,CACvC,IAAI,EACJ,GAAI,CACF,EAAS,EAAO,CAAK,CACvB,MAAQ,CACN,EAAQ,SAAS,SAAS,EAAa,SAAU,CAAE,OAAQ,qCAAsC,CAAC,CAAC,EACnG,EAAW,QAAQ,EACnB,MACF,CACA,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,OAAA,WAAwB,CAChC,EAAgB,GAChB,EAAU,EACV,EAAQ,SAAS,SACf,EAAa,WAAY,CAAE,SAAU,EAAQ,EAAM,QAAQ,EAAG,QAAS,EAAQ,EAAM,OAAO,CAAE,CAAC,CACjG,EACA,IAAK,IAAM,KAAU,EAAQ,OAAO,CAAC,EAAG,EAAW,MAAM,EAAY,CAAM,CAAC,EAC5E,QACF,CACA,GAAI,EAAM,OAAA,eAA4B,CACpC,IAAM,EAAS,OAAO,EAAM,QAAW,SAAW,EAAM,OAAS,mCACjE,EAAQ,SAAS,SAAS,EAAa,UAAW,CAAE,QAAO,CAAC,CAAC,EAC7D,QACF,CACA,EAAQ,SAAS,QAAQ,CAAK,CAChC,CACF,CAAC,EAUD,EAAW,GAAG,YARY,CACpB,IACA,IAAW,IAAY,EAAS,IAAA,IACpC,EAAQ,SAAS,SAAS,EAAa,EAAgB,WAAa,YAAY,CAAC,EACjF,GAAW,EACX,EAAQ,WAAW,EAAS,EAAgB,CAAO,CAAC,EACtD,CAE4B,EAC5B,EAAW,GAAG,YAAe,CAE7B,CAAC,CACH,EAIA,OAFA,EAAQ,EAED,CACL,KAAK,EAAO,CACN,MACJ,IAAI,CAAC,GAAiB,CAAC,EAAQ,CAG7B,EAAQ,KAAK,CAAK,EACd,EAAQ,OAAS,IAAe,EAAQ,MAAM,EAClD,MACF,CACA,EAAO,MAAM,EAAY,CAAK,CAAC,CAD/B,CAEF,EACA,OAAQ,CACN,EAAU,GACN,GAAO,aAAa,CAAK,EAC7B,GAAQ,QAAQ,EAChB,EAAS,IAAA,EACX,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);let n=require("node:child_process"),r=require("node:module"),i=require("@agimon-ai/doompi/services");const a=[`dist`,`bin`,`cli.mjs`],o=[`sync`];function s(e,i){let s=t.default.join(t.default.dirname((0,r.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`)),...a);return new Promise(t=>{let r=(0,n.spawn)(process.execPath,[s,...o],{cwd:e,stdio:[`ignore`,`pipe`,`pipe`]}),a=[],c=e=>{a.push(e.toString()),a.length>20&&a.shift()};r.stdout?.on(`data`,c),r.stderr?.on(`data`,c),r.once(`error`,e=>{i(`sync could not start: ${e.message}`),t()}),r.once(`exit`,e=>{e!==0&&i(`sync exited ${String(e)}: ${a.join(``).trim().slice(-400)}`),t()})})}function c(e){let t=e.onNotice??(()=>{}),n=e.runSync??(e=>s(e,t)),r=e.readDrift??(e=>(0,i.readSyncDrift)({repoRoot:e})),a,o,c=!1,l=async r=>{t(`syncing: ${(0,i.describeSyncDrift)({fresh:!1,reasons:r.reasons})}`),await n(e.repoRoot),t(`sync complete`)},u=async()=>{if(c)return;if(a)return a;let t=r(e.repoRoot);if(!t.fresh)return a=l(t).finally(()=>{a=void 0}),a};return{ensureSynced:u,watch(t){o||(o=setInterval(()=>{c||a||r(e.repoRoot).fresh||u().then(()=>{c||t()})},e.intervalMs??2e3),o.unref?.())},close(){c=!0,o&&clearInterval(o),o=void 0}}}exports.createSyncGuard=c;
2
+ //# sourceMappingURL=syncGuard.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncGuard.cjs","names":["path","createRequire","spawn","readSyncDrift","describeSyncDrift"],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n\nexport interface SyncGuardOptions {\n repoRoot: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself. */\n runSync?: (repoRoot: string) => Promise<void>;\n /** Test seam for the drift read. */\n readDrift?: (repoRoot: string) => { fresh: boolean; reasons: readonly string[] };\n intervalMs?: number;\n}\n\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string, onNotice: (message: string) => void): Promise<void> {\n const cli = path.join(\n path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)),\n ...CLI_SEGMENTS,\n );\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n child.once('error', (error) => {\n onNotice(`sync could not start: ${error.message}`);\n resolve();\n });\n child.once('exit', (code) => {\n // A failed sync is reported, not thrown: the session the caller wanted\n // still starts, and it starts against whatever the last good sync left.\n if (code !== 0) onNotice(`sync exited ${String(code)}: ${tail.join('').trim().slice(-400)}`);\n resolve();\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n const runSync = options.runSync ?? ((repoRoot: string) => spawnSync(repoRoot, notice));\n const readDrift = options.readDrift ?? ((repoRoot: string) => readSyncDrift({ repoRoot }));\n let inFlight: Promise<void> | undefined;\n let timer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n\n const syncOnce = async (drift: { reasons: readonly string[] }): Promise<void> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n await runSync(options.repoRoot);\n notice('sync complete');\n };\n\n const ensureSynced = async (): Promise<void> => {\n if (closed) return;\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(options.repoRoot);\n if (drift.fresh) return;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n ensureSynced,\n watch(onSynced) {\n if (timer) return;\n timer = setInterval(() => {\n if (closed || inFlight) return;\n const drift = readDrift(options.repoRoot);\n if (drift.fresh) return;\n void ensureSynced().then(() => {\n if (!closed) onSynced();\n });\n }, options.intervalMs ?? WATCH_INTERVAL_MS);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n },\n close() {\n closed = true;\n if (timer) clearInterval(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"yMAKA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EACxC,EAAY,CAAC,MAAM,EA4BzB,SAAS,EAAU,EAAkB,EAAoD,CACvF,IAAM,EAAMA,EAAAA,QAAK,KACfA,EAAAA,QAAK,SAAA,EAAQC,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACrF,GAAG,CACL,EACA,OAAO,IAAI,QAAS,GAAY,CAC9B,IAAM,GAAA,EAAQC,EAAAA,MAAAA,CAAM,QAAQ,SAAU,CAAC,EAAK,GAAG,CAAS,EAAG,CAAE,IAAK,EAAU,MAAO,CAAC,SAAU,OAAQ,MAAM,CAAE,CAAC,EACzG,EAAiB,CAAC,EAClB,EAAQ,GAAwB,CACpC,EAAK,KAAK,EAAM,SAAS,CAAC,EACtB,EAAK,OAAS,IAAI,EAAK,MAAM,CACnC,EACA,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAC7B,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAC7B,EAAM,KAAK,QAAU,GAAU,CAC7B,EAAS,yBAAyB,EAAM,SAAS,EACjD,EAAQ,CACV,CAAC,EACD,EAAM,KAAK,OAAS,GAAS,CAGvB,IAAS,GAAG,EAAS,eAAe,OAAO,CAAI,EAAE,IAAI,EAAK,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,GAAG,EAC3F,EAAQ,CACV,CAAC,CACH,CAAC,CACH,CAUA,SAAgB,EAAgB,EAAsC,CACpE,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC3C,EAAU,EAAQ,UAAa,GAAqB,EAAU,EAAU,CAAM,GAC9E,EAAY,EAAQ,YAAe,IAAA,EAAqBC,EAAAA,cAAAA,CAAc,CAAE,UAAS,CAAC,GACpF,EACA,EACA,EAAS,GAEP,EAAW,KAAO,IAAyD,CAC/E,EAAO,aAAA,EAAYC,EAAAA,kBAAAA,CAAkB,CAAE,MAAO,GAAO,QAAS,EAAM,OAAiB,CAAC,GAAG,EACzF,MAAM,EAAQ,EAAQ,QAAQ,EAC9B,EAAO,eAAe,CACxB,EAEM,EAAe,SAA2B,CAC9C,GAAI,EAAQ,OAGZ,GAAI,EAAU,OAAO,EACrB,IAAM,EAAQ,EAAU,EAAQ,QAAQ,EACpC,MAAM,MAIV,MAHA,GAAW,EAAS,CAAK,CAAC,CAAC,YAAc,CACvC,EAAW,IAAA,EACb,CAAC,EACM,CACT,EAEA,MAAO,CACL,eACA,MAAM,EAAU,CACV,IACJ,EAAQ,gBAAkB,CACpB,GAAU,GACA,EAAU,EAAQ,QACxB,CAAC,CAAC,OACV,EAAkB,CAAC,CAAC,SAAW,CACxB,GAAQ,EAAS,CACxB,CAAC,CACH,EAAG,EAAQ,YAAc,GAAiB,EAE1C,EAAM,QAAQ,EAChB,EACA,OAAQ,CACN,EAAS,GACL,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,EACV,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ import{createRequire as e}from"node:module";import t from"node:path";import{spawn as n}from"node:child_process";import{describeSyncDrift as r,readSyncDrift as i}from"@agimon-ai/doompi/services";const a=[`dist`,`bin`,`cli.mjs`],o=[`sync`];function s(r,i){let s=t.join(t.dirname(e(import.meta.url).resolve(`@agimon-ai/doompi/package.json`)),...a);return new Promise(e=>{let t=n(process.execPath,[s,...o],{cwd:r,stdio:[`ignore`,`pipe`,`pipe`]}),a=[],c=e=>{a.push(e.toString()),a.length>20&&a.shift()};t.stdout?.on(`data`,c),t.stderr?.on(`data`,c),t.once(`error`,t=>{i(`sync could not start: ${t.message}`),e()}),t.once(`exit`,t=>{t!==0&&i(`sync exited ${String(t)}: ${a.join(``).trim().slice(-400)}`),e()})})}function c(e){let t=e.onNotice??(()=>{}),n=e.runSync??(e=>s(e,t)),a=e.readDrift??(e=>i({repoRoot:e})),o,c,l=!1,u=async i=>{t(`syncing: ${r({fresh:!1,reasons:i.reasons})}`),await n(e.repoRoot),t(`sync complete`)},d=async()=>{if(l)return;if(o)return o;let t=a(e.repoRoot);if(!t.fresh)return o=u(t).finally(()=>{o=void 0}),o};return{ensureSynced:d,watch(t){c||(c=setInterval(()=>{l||o||a(e.repoRoot).fresh||d().then(()=>{l||t()})},e.intervalMs??2e3),c.unref?.())},close(){l=!0,c&&clearInterval(c),c=void 0}}}export{c as createSyncGuard};
2
+ //# sourceMappingURL=syncGuard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncGuard.mjs","names":[],"sources":["../../../src/adapters/syncGuard.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { describeSyncDrift, readSyncDrift } from '@agimon-ai/doompi/services';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\nconst SYNC_ARGS = ['sync'];\n/** How often the watcher re-reads the drift inputs. */\nconst WATCH_INTERVAL_MS = 2_000;\n\nexport interface SyncGuardOptions {\n repoRoot: string;\n onNotice?: (message: string) => void;\n /** Test seam for running the sync itself. */\n runSync?: (repoRoot: string) => Promise<void>;\n /** Test seam for the drift read. */\n readDrift?: (repoRoot: string) => { fresh: boolean; reasons: readonly string[] };\n intervalMs?: number;\n}\n\nexport interface SyncGuard {\n /**\n * Syncs the repository when anything a session reads has drifted.\n *\n * Serialized: several sessions starting at once wait on one sync rather\n * than racing to rebuild the same artifacts over each other.\n */\n ensureSynced(): Promise<void>;\n /** Re-syncs on drift and reports when artifacts changed under a running cockpit. */\n watch(onSynced: () => void): void;\n close(): void;\n}\n\n/** Runs the launcher's own sync, in its own process so the hub keeps serving. */\nfunction spawnSync(repoRoot: string, onNotice: (message: string) => void): Promise<void> {\n const cli = path.join(\n path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`)),\n ...CLI_SEGMENTS,\n );\n return new Promise((resolve) => {\n const child = spawn(process.execPath, [cli, ...SYNC_ARGS], { cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });\n const tail: string[] = [];\n const keep = (chunk: Buffer): void => {\n tail.push(chunk.toString());\n if (tail.length > 20) tail.shift();\n };\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n child.once('error', (error) => {\n onNotice(`sync could not start: ${error.message}`);\n resolve();\n });\n child.once('exit', (code) => {\n // A failed sync is reported, not thrown: the session the caller wanted\n // still starts, and it starts against whatever the last good sync left.\n if (code !== 0) onNotice(`sync exited ${String(code)}: ${tail.join('').trim().slice(-400)}`);\n resolve();\n });\n });\n}\n\n/**\n * Keeps the repository synced under a running cockpit.\n *\n * A session launched from the browser reads what sync produced: the resolved\n * composition, the plugin bundle, and the package API routes. Nothing forces\n * a person clicking \"new session\" to have run sync first, and a stale one\n * fails quietly rather than loudly, so the hub checks and syncs itself.\n */\nexport function createSyncGuard(options: SyncGuardOptions): SyncGuard {\n const notice = options.onNotice ?? ((): void => {});\n const runSync = options.runSync ?? ((repoRoot: string) => spawnSync(repoRoot, notice));\n const readDrift = options.readDrift ?? ((repoRoot: string) => readSyncDrift({ repoRoot }));\n let inFlight: Promise<void> | undefined;\n let timer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n\n const syncOnce = async (drift: { reasons: readonly string[] }): Promise<void> => {\n notice(`syncing: ${describeSyncDrift({ fresh: false, reasons: drift.reasons as never })}`);\n await runSync(options.repoRoot);\n notice('sync complete');\n };\n\n const ensureSynced = async (): Promise<void> => {\n if (closed) return;\n // Joining the run already in flight is what makes concurrent launches\n // wait for one sync instead of starting several.\n if (inFlight) return inFlight;\n const drift = readDrift(options.repoRoot);\n if (drift.fresh) return;\n inFlight = syncOnce(drift).finally(() => {\n inFlight = undefined;\n });\n return inFlight;\n };\n\n return {\n ensureSynced,\n watch(onSynced) {\n if (timer) return;\n timer = setInterval(() => {\n if (closed || inFlight) return;\n const drift = readDrift(options.repoRoot);\n if (drift.fresh) return;\n void ensureSynced().then(() => {\n if (!closed) onSynced();\n });\n }, options.intervalMs ?? WATCH_INTERVAL_MS);\n // The hub should not be held open by its own watcher.\n timer.unref?.();\n },\n close() {\n closed = true;\n if (timer) clearInterval(timer);\n timer = undefined;\n },\n };\n}\n"],"mappings":"kMAKA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EACxC,EAAY,CAAC,MAAM,EA4BzB,SAAS,EAAU,EAAkB,EAAoD,CACvF,IAAM,EAAM,EAAK,KACf,EAAK,QAAQ,EAAc,YAAY,GAAG,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACrF,GAAG,CACL,EACA,OAAO,IAAI,QAAS,GAAY,CAC9B,IAAM,EAAQ,EAAM,QAAQ,SAAU,CAAC,EAAK,GAAG,CAAS,EAAG,CAAE,IAAK,EAAU,MAAO,CAAC,SAAU,OAAQ,MAAM,CAAE,CAAC,EACzG,EAAiB,CAAC,EAClB,EAAQ,GAAwB,CACpC,EAAK,KAAK,EAAM,SAAS,CAAC,EACtB,EAAK,OAAS,IAAI,EAAK,MAAM,CACnC,EACA,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAC7B,EAAM,QAAQ,GAAG,OAAQ,CAAI,EAC7B,EAAM,KAAK,QAAU,GAAU,CAC7B,EAAS,yBAAyB,EAAM,SAAS,EACjD,EAAQ,CACV,CAAC,EACD,EAAM,KAAK,OAAS,GAAS,CAGvB,IAAS,GAAG,EAAS,eAAe,OAAO,CAAI,EAAE,IAAI,EAAK,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,GAAG,EAC3F,EAAQ,CACV,CAAC,CACH,CAAC,CACH,CAUA,SAAgB,EAAgB,EAAsC,CACpE,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC3C,EAAU,EAAQ,UAAa,GAAqB,EAAU,EAAU,CAAM,GAC9E,EAAY,EAAQ,YAAe,GAAqB,EAAc,CAAE,UAAS,CAAC,GACpF,EACA,EACA,EAAS,GAEP,EAAW,KAAO,IAAyD,CAC/E,EAAO,YAAY,EAAkB,CAAE,MAAO,GAAO,QAAS,EAAM,OAAiB,CAAC,GAAG,EACzF,MAAM,EAAQ,EAAQ,QAAQ,EAC9B,EAAO,eAAe,CACxB,EAEM,EAAe,SAA2B,CAC9C,GAAI,EAAQ,OAGZ,GAAI,EAAU,OAAO,EACrB,IAAM,EAAQ,EAAU,EAAQ,QAAQ,EACpC,MAAM,MAIV,MAHA,GAAW,EAAS,CAAK,CAAC,CAAC,YAAc,CACvC,EAAW,IAAA,EACb,CAAC,EACM,CACT,EAEA,MAAO,CACL,eACA,MAAM,EAAU,CACV,IACJ,EAAQ,gBAAkB,CACpB,GAAU,GACA,EAAU,EAAQ,QACxB,CAAC,CAAC,OACV,EAAkB,CAAC,CAAC,SAAW,CACxB,GAAQ,EAAS,CACxB,CAAC,CACH,EAAG,EAAQ,YAAc,GAAiB,EAE1C,EAAM,QAAQ,EAChB,EACA,OAAQ,CACN,EAAS,GACL,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,EACV,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/journalTail.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);function i(e,t){return`${e}\n${t}`}function a(e,t,r){let i=Buffer.alloc(r-t),a;try{a=n.default.openSync(e,`r`);let r=n.default.readSync(a,i,0,i.length,t);return i.subarray(0,r)}catch{return}finally{a!==void 0&&n.default.closeSync(a)}}function o(e){let o=e.pollMs??1e3,s=e.retainLimit??300,c=e.maxInitialBytes??8388608,l=e.onNotice??(()=>{}),u=new Map,d=new Set,f,p=t=>{let n=e.resolve(t.sessionId,t.threadId);if(n!==void 0){if(r.default.isAbsolute(n)&&n.endsWith(`.jsonl`))return n;t.warned||(t.warned=!0,l(`thread ${t.threadId} of session ${t.sessionId} names '${n}', which is not a journal`))}},m=e=>{if(e.path??=p(e),e.path===void 0)return[];let r;try{r=n.default.statSync(e.path).size}catch{return[]}r<e.offset&&(e.offset=0,e.retained=[]);let i=e.offset,o=!1;if(i===0&&r>c&&(i=r-c,o=!0),r<=i)return[];let l=a(e.path,i,r);if(l===void 0)return[];let u=l.lastIndexOf(10);if(u===-1)return[];let d=o?l.indexOf(10)+1:0,f=d>u?[]:t.journalFrames(l.toString(`utf8`,d,u+1));return e.offset=i+u+1,f.length>0&&(e.retained=t.retainNewest([...e.retained,...f],s)),f},h=()=>{for(let e of u.values()){let t=m(e);for(let n of t)for(let t of d)t({sessionId:e.sessionId,threadId:e.threadId,frame:n})}},g=()=>{u.size>0&&f===void 0&&(f=setInterval(h,o)),u.size===0&&f!==void 0&&(clearInterval(f),f=void 0)};return{subscribe(e,t){let n=i(e,t),r=u.get(n);return r===void 0&&(r={sessionId:e,threadId:t,refs:0,path:void 0,offset:0,retained:[],warned:!1},u.set(n,r),m(r),g()),r.refs+=1,[...r.retained]},unsubscribe(e,t){let n=i(e,t),r=u.get(n);r!==void 0&&(--r.refs,r.refs<=0&&u.delete(n),g())},onFrame(e){return d.add(e),()=>{d.delete(e)}},close(){f!==void 0&&clearInterval(f),f=void 0,u.clear(),d.clear()}}}exports.createThreadJournals=o;
2
+ //# sourceMappingURL=threadJournals.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threadJournals.cjs","names":["fs","path","journalFrames","retainNewest"],"sources":["../../../src/adapters/threadJournals.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { journalFrames, retainNewest } from '../services/journalTail.ts';\nimport type { SessionFrame } from '../types/session.ts';\n\nconst DEFAULT_POLL_MS = 1000;\n/** Journalled messages kept per thread, the same allowance an attach restores. */\nconst DEFAULT_RETAIN_LIMIT = 300;\n/** How far back a first read goes into a long journal; older lines stay on disk. */\nconst DEFAULT_MAX_INITIAL_BYTES = 8 * 1024 * 1024;\nconst JOURNAL_EXTENSION = '.jsonl';\nconst NEWLINE = 0x0a;\n\nexport interface ThreadFrameEvent {\n sessionId: string;\n threadId: string;\n frame: SessionFrame;\n}\n\nexport interface ThreadJournalsOptions {\n /** Names the journal of one thread, or undefined while no source knows it yet. */\n resolve(sessionId: string, threadId: string): string | undefined;\n pollMs?: number;\n retainLimit?: number;\n maxInitialBytes?: number;\n onNotice?: (message: string) => void;\n}\n\nexport interface ThreadJournals {\n /** Follows the thread for one more subscriber and returns its retained history, oldest first. */\n subscribe(sessionId: string, threadId: string): SessionFrame[];\n unsubscribe(sessionId: string, threadId: string): void;\n /** Live frames of every followed thread; the return value stops listening. */\n onFrame(listener: (event: ThreadFrameEvent) => void): () => void;\n close(): void;\n}\n\ninterface Tail {\n sessionId: string;\n threadId: string;\n refs: number;\n /** Unknown until a source names the journal; asked again on every tick. */\n path: string | undefined;\n /** The byte after the last complete line read, so a torn line is re-read whole. */\n offset: number;\n retained: SessionFrame[];\n warned: boolean;\n}\n\nfunction keyOf(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\n/** Bytes [start, end) of a file, or undefined when it cannot be read right now. */\nfunction readRange(file: string, start: number, end: number): Buffer | undefined {\n const buffer = Buffer.alloc(end - start);\n let fd: number | undefined;\n try {\n fd = fs.openSync(file, 'r');\n const read = fs.readSync(fd, buffer, 0, buffer.length, start);\n return buffer.subarray(0, read);\n } catch {\n // The writer may be rotating or the file may have gone; the next tick looks again.\n return undefined;\n } finally {\n if (fd !== undefined) fs.closeSync(fd);\n }\n}\n\n/**\n * Tails Pi session journals for the pages following them.\n *\n * A thread is any journal a data channel can name for a session, such as a\n * subagent run's own session file. The hub reads only what the file gained\n * since the last look and stops at the last newline, so a line the writer is\n * still appending is never folded half-way; a file that shrank is read from\n * the start again, which is harmless because the page dedupes entries by id.\n */\nexport function createThreadJournals(options: ThreadJournalsOptions): ThreadJournals {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n const retainLimit = options.retainLimit ?? DEFAULT_RETAIN_LIMIT;\n const maxInitialBytes = options.maxInitialBytes ?? DEFAULT_MAX_INITIAL_BYTES;\n const notice = options.onNotice ?? ((): void => {});\n const tails = new Map<string, Tail>();\n const listeners = new Set<(event: ThreadFrameEvent) => void>();\n let timer: NodeJS.Timeout | undefined;\n\n const journalPathOf = (tail: Tail): string | undefined => {\n const candidate = options.resolve(tail.sessionId, tail.threadId);\n if (candidate === undefined) return undefined;\n if (path.isAbsolute(candidate) && candidate.endsWith(JOURNAL_EXTENSION)) return candidate;\n if (!tail.warned) {\n tail.warned = true;\n notice(`thread ${tail.threadId} of session ${tail.sessionId} names '${candidate}', which is not a journal`);\n }\n return undefined;\n };\n\n /** Folds what the journal gained since the last look and returns the new frames. */\n const advance = (tail: Tail): SessionFrame[] => {\n tail.path ??= journalPathOf(tail);\n if (tail.path === undefined) return [];\n let size: number;\n try {\n size = fs.statSync(tail.path).size;\n } catch {\n // Not written yet, or gone: the thread stays empty until the next tick finds it.\n return [];\n }\n if (size < tail.offset) {\n tail.offset = 0;\n tail.retained = [];\n }\n let start = tail.offset;\n let cut = false;\n if (start === 0 && size > maxInitialBytes) {\n start = size - maxInitialBytes;\n cut = true;\n }\n if (size <= start) return [];\n const chunk = readRange(tail.path, start, size);\n if (chunk === undefined) return [];\n const lastNewline = chunk.lastIndexOf(NEWLINE);\n if (lastNewline === -1) return [];\n // A cut lands mid-line; the line it tore is dropped rather than misread.\n const firstLine = cut ? chunk.indexOf(NEWLINE) + 1 : 0;\n const frames = firstLine > lastNewline ? [] : journalFrames(chunk.toString('utf8', firstLine, lastNewline + 1));\n tail.offset = start + lastNewline + 1;\n if (frames.length > 0) tail.retained = retainNewest([...tail.retained, ...frames], retainLimit);\n return frames;\n };\n\n const tick = (): void => {\n for (const tail of tails.values()) {\n const frames = advance(tail);\n for (const frame of frames) {\n for (const listener of listeners) listener({ sessionId: tail.sessionId, threadId: tail.threadId, frame });\n }\n }\n };\n\n const syncTimer = (): void => {\n if (tails.size > 0 && timer === undefined) timer = setInterval(tick, pollMs);\n if (tails.size === 0 && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n\n return {\n subscribe(sessionId, threadId) {\n const key = keyOf(sessionId, threadId);\n let tail = tails.get(key);\n if (tail === undefined) {\n tail = { sessionId, threadId, refs: 0, path: undefined, offset: 0, retained: [], warned: false };\n tails.set(key, tail);\n advance(tail);\n syncTimer();\n }\n tail.refs += 1;\n return [...tail.retained];\n },\n unsubscribe(sessionId, threadId) {\n const key = keyOf(sessionId, threadId);\n const tail = tails.get(key);\n if (tail === undefined) return;\n tail.refs -= 1;\n if (tail.refs <= 0) tails.delete(key);\n syncTimer();\n },\n onFrame(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n close() {\n if (timer !== undefined) clearInterval(timer);\n timer = undefined;\n tails.clear();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{journalFrames as e,retainNewest as t}from"../services/journalTail.mjs";import n from"node:fs";import r from"node:path";function i(e,t){return`${e}\n${t}`}function a(e,t,r){let i=Buffer.alloc(r-t),a;try{a=n.openSync(e,`r`);let r=n.readSync(a,i,0,i.length,t);return i.subarray(0,r)}catch{return}finally{a!==void 0&&n.closeSync(a)}}function o(o){let s=o.pollMs??1e3,c=o.retainLimit??300,l=o.maxInitialBytes??8388608,u=o.onNotice??(()=>{}),d=new Map,f=new Set,p,m=e=>{let t=o.resolve(e.sessionId,e.threadId);if(t!==void 0){if(r.isAbsolute(t)&&t.endsWith(`.jsonl`))return t;e.warned||(e.warned=!0,u(`thread ${e.threadId} of session ${e.sessionId} names '${t}', which is not a journal`))}},h=r=>{if(r.path??=m(r),r.path===void 0)return[];let i;try{i=n.statSync(r.path).size}catch{return[]}i<r.offset&&(r.offset=0,r.retained=[]);let o=r.offset,s=!1;if(o===0&&i>l&&(o=i-l,s=!0),i<=o)return[];let u=a(r.path,o,i);if(u===void 0)return[];let d=u.lastIndexOf(10);if(d===-1)return[];let f=s?u.indexOf(10)+1:0,p=f>d?[]:e(u.toString(`utf8`,f,d+1));return r.offset=o+d+1,p.length>0&&(r.retained=t([...r.retained,...p],c)),p},g=()=>{for(let e of d.values()){let t=h(e);for(let n of t)for(let t of f)t({sessionId:e.sessionId,threadId:e.threadId,frame:n})}},_=()=>{d.size>0&&p===void 0&&(p=setInterval(g,s)),d.size===0&&p!==void 0&&(clearInterval(p),p=void 0)};return{subscribe(e,t){let n=i(e,t),r=d.get(n);return r===void 0&&(r={sessionId:e,threadId:t,refs:0,path:void 0,offset:0,retained:[],warned:!1},d.set(n,r),h(r),_()),r.refs+=1,[...r.retained]},unsubscribe(e,t){let n=i(e,t),r=d.get(n);r!==void 0&&(--r.refs,r.refs<=0&&d.delete(n),_())},onFrame(e){return f.add(e),()=>{f.delete(e)}},close(){p!==void 0&&clearInterval(p),p=void 0,d.clear(),f.clear()}}}export{o as createThreadJournals};
2
+ //# sourceMappingURL=threadJournals.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threadJournals.mjs","names":[],"sources":["../../../src/adapters/threadJournals.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { journalFrames, retainNewest } from '../services/journalTail.ts';\nimport type { SessionFrame } from '../types/session.ts';\n\nconst DEFAULT_POLL_MS = 1000;\n/** Journalled messages kept per thread, the same allowance an attach restores. */\nconst DEFAULT_RETAIN_LIMIT = 300;\n/** How far back a first read goes into a long journal; older lines stay on disk. */\nconst DEFAULT_MAX_INITIAL_BYTES = 8 * 1024 * 1024;\nconst JOURNAL_EXTENSION = '.jsonl';\nconst NEWLINE = 0x0a;\n\nexport interface ThreadFrameEvent {\n sessionId: string;\n threadId: string;\n frame: SessionFrame;\n}\n\nexport interface ThreadJournalsOptions {\n /** Names the journal of one thread, or undefined while no source knows it yet. */\n resolve(sessionId: string, threadId: string): string | undefined;\n pollMs?: number;\n retainLimit?: number;\n maxInitialBytes?: number;\n onNotice?: (message: string) => void;\n}\n\nexport interface ThreadJournals {\n /** Follows the thread for one more subscriber and returns its retained history, oldest first. */\n subscribe(sessionId: string, threadId: string): SessionFrame[];\n unsubscribe(sessionId: string, threadId: string): void;\n /** Live frames of every followed thread; the return value stops listening. */\n onFrame(listener: (event: ThreadFrameEvent) => void): () => void;\n close(): void;\n}\n\ninterface Tail {\n sessionId: string;\n threadId: string;\n refs: number;\n /** Unknown until a source names the journal; asked again on every tick. */\n path: string | undefined;\n /** The byte after the last complete line read, so a torn line is re-read whole. */\n offset: number;\n retained: SessionFrame[];\n warned: boolean;\n}\n\nfunction keyOf(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\n/** Bytes [start, end) of a file, or undefined when it cannot be read right now. */\nfunction readRange(file: string, start: number, end: number): Buffer | undefined {\n const buffer = Buffer.alloc(end - start);\n let fd: number | undefined;\n try {\n fd = fs.openSync(file, 'r');\n const read = fs.readSync(fd, buffer, 0, buffer.length, start);\n return buffer.subarray(0, read);\n } catch {\n // The writer may be rotating or the file may have gone; the next tick looks again.\n return undefined;\n } finally {\n if (fd !== undefined) fs.closeSync(fd);\n }\n}\n\n/**\n * Tails Pi session journals for the pages following them.\n *\n * A thread is any journal a data channel can name for a session, such as a\n * subagent run's own session file. The hub reads only what the file gained\n * since the last look and stops at the last newline, so a line the writer is\n * still appending is never folded half-way; a file that shrank is read from\n * the start again, which is harmless because the page dedupes entries by id.\n */\nexport function createThreadJournals(options: ThreadJournalsOptions): ThreadJournals {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n const retainLimit = options.retainLimit ?? DEFAULT_RETAIN_LIMIT;\n const maxInitialBytes = options.maxInitialBytes ?? DEFAULT_MAX_INITIAL_BYTES;\n const notice = options.onNotice ?? ((): void => {});\n const tails = new Map<string, Tail>();\n const listeners = new Set<(event: ThreadFrameEvent) => void>();\n let timer: NodeJS.Timeout | undefined;\n\n const journalPathOf = (tail: Tail): string | undefined => {\n const candidate = options.resolve(tail.sessionId, tail.threadId);\n if (candidate === undefined) return undefined;\n if (path.isAbsolute(candidate) && candidate.endsWith(JOURNAL_EXTENSION)) return candidate;\n if (!tail.warned) {\n tail.warned = true;\n notice(`thread ${tail.threadId} of session ${tail.sessionId} names '${candidate}', which is not a journal`);\n }\n return undefined;\n };\n\n /** Folds what the journal gained since the last look and returns the new frames. */\n const advance = (tail: Tail): SessionFrame[] => {\n tail.path ??= journalPathOf(tail);\n if (tail.path === undefined) return [];\n let size: number;\n try {\n size = fs.statSync(tail.path).size;\n } catch {\n // Not written yet, or gone: the thread stays empty until the next tick finds it.\n return [];\n }\n if (size < tail.offset) {\n tail.offset = 0;\n tail.retained = [];\n }\n let start = tail.offset;\n let cut = false;\n if (start === 0 && size > maxInitialBytes) {\n start = size - maxInitialBytes;\n cut = true;\n }\n if (size <= start) return [];\n const chunk = readRange(tail.path, start, size);\n if (chunk === undefined) return [];\n const lastNewline = chunk.lastIndexOf(NEWLINE);\n if (lastNewline === -1) return [];\n // A cut lands mid-line; the line it tore is dropped rather than misread.\n const firstLine = cut ? chunk.indexOf(NEWLINE) + 1 : 0;\n const frames = firstLine > lastNewline ? [] : journalFrames(chunk.toString('utf8', firstLine, lastNewline + 1));\n tail.offset = start + lastNewline + 1;\n if (frames.length > 0) tail.retained = retainNewest([...tail.retained, ...frames], retainLimit);\n return frames;\n };\n\n const tick = (): void => {\n for (const tail of tails.values()) {\n const frames = advance(tail);\n for (const frame of frames) {\n for (const listener of listeners) listener({ sessionId: tail.sessionId, threadId: tail.threadId, frame });\n }\n }\n };\n\n const syncTimer = (): void => {\n if (tails.size > 0 && timer === undefined) timer = setInterval(tick, pollMs);\n if (tails.size === 0 && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n\n return {\n subscribe(sessionId, threadId) {\n const key = keyOf(sessionId, threadId);\n let tail = tails.get(key);\n if (tail === undefined) {\n tail = { sessionId, threadId, refs: 0, path: undefined, offset: 0, retained: [], warned: false };\n tails.set(key, tail);\n advance(tail);\n syncTimer();\n }\n tail.refs += 1;\n return [...tail.retained];\n },\n unsubscribe(sessionId, threadId) {\n const key = keyOf(sessionId, threadId);\n const tail = tails.get(key);\n if (tail === undefined) return;\n tail.refs -= 1;\n if (tail.refs <= 0) tails.delete(key);\n syncTimer();\n },\n onFrame(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n close() {\n if (timer !== undefined) clearInterval(timer);\n timer = undefined;\n tails.clear();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/webDevRoots.cjs"),n=require("./webPluginScan.cjs"),r=require("./webPluginGenerate.cjs"),i=require("./webPluginVite.cjs");let a=require("node:fs");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);const s=[`react`,`react-dom`,`@tanstack/store`,`@tanstack/react-store`,`@agimon-ai/doompi-web-components`];async function c(e){let c=e.onNotice??(()=>{}),l=r.webHostPackageRoot(),u=n.scanWebPlugins(l,e.pluginRoots,c);c(`bundling the cockpit with ${String(u.length)} web plugin(s): ${u.map(e=>e.pluginId).join(`, `)}`);let d=o.default.join(e.outDir,`generated`),f=o.default.join(e.outDir,`web`),p=r.writeSyncWebPluginModules(u,d),m=[...new Set(e.pluginRoots.map(e=>o.default.resolve(e)))];a.default.writeFileSync(o.default.join(e.outDir,t.PLUGIN_ROOTS_FILE),`${JSON.stringify(m,null,2)}\n`);let{build:h}=await import(`vite`),{default:g}=await import(`@vitejs/plugin-react`),{default:_}=await import(`@tailwindcss/vite`),v=o.default.join(l,`src`,`web`);return await h({configFile:!1,envDir:!1,logLevel:`warn`,root:v,plugins:[i.webPluginOverridePlugin(v,p),g(),_()],resolve:{dedupe:[...s],alias:[i.webPluginCssAlias(p)]},build:{outDir:f,emptyOutDir:!0,sourcemap:!1}}),a.default.writeFileSync(o.default.join(f,r.SERVER_REGISTRY_FILE),p.serverRegistry),{assetsDir:f,pluginIds:u.map(e=>e.pluginId)}}exports.bundleCockpitWeb=c;
2
+ //# sourceMappingURL=webBundler.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webBundler.cjs","names":["webHostPackageRoot","scanWebPlugins","path","writeSyncWebPluginModules","PLUGIN_ROOTS_FILE","webPluginOverridePlugin","webPluginCssAlias","SERVER_REGISTRY_FILE"],"sources":["../../../src/adapters/webBundler.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { PLUGIN_ROOTS_FILE } from '../services/webDevRoots.ts';\nimport { SERVER_REGISTRY_FILE, webHostPackageRoot, writeSyncWebPluginModules } from './webPluginGenerate.ts';\nimport { scanWebPlugins } from './webPluginScan.ts';\nimport { webPluginCssAlias, webPluginOverridePlugin } from './webPluginVite.ts';\n\n/** Shared runtime packages every plugin's client code must resolve to the host copies. */\nconst DEDUPED_RUNTIMES = [\n 'react',\n 'react-dom',\n '@tanstack/store',\n '@tanstack/react-store',\n '@agimon-ai/doompi-web-components',\n];\n\nexport interface BundleCockpitWebOptions {\n /** Package roots to scan for doompiWeb manifests, usually the installed composition. */\n pluginRoots: readonly string[];\n /** Output directory; receives web/ (the bundle), generated/ (the entry modules), and pluginRoots.json. */\n outDir: string;\n onNotice?: (message: string) => void;\n}\n\nexport interface BundleCockpitWebResult {\n /** The assets directory to serve (contains index.html and the server registry). */\n assetsDir: string;\n pluginIds: string[];\n}\n\n/**\n * Builds the cockpit SPA against the installed plugin set.\n *\n * This is what `doompi sync` calls: manifests come from the given package\n * roots (no hardcoded plugin dependencies anywhere), the generated entry\n * modules are aliased over the committed builtin registry, and Vite compiles\n * everything (host shell plus each plugin's shipped web source) into one\n * bundle. The server registry written next to the assets tells the hub which\n * built hub entries to load, and the roots file beside the bundle lets the\n * dev server serve the same composition with hot reload.\n */\nexport async function bundleCockpitWeb(options: BundleCockpitWebOptions): Promise<BundleCockpitWebResult> {\n const notice = options.onNotice ?? ((): void => {});\n const hostRoot = webHostPackageRoot();\n const plugins = scanWebPlugins(hostRoot, options.pluginRoots, notice);\n notice(\n `bundling the cockpit with ${String(plugins.length)} web plugin(s): ${plugins.map((p) => p.pluginId).join(', ')}`,\n );\n\n const generatedDir = path.join(options.outDir, 'generated');\n const assetsDir = path.join(options.outDir, 'web');\n const generated = writeSyncWebPluginModules(plugins, generatedDir);\n const roots = [...new Set(options.pluginRoots.map((root) => path.resolve(root)))];\n fs.writeFileSync(path.join(options.outDir, PLUGIN_ROOTS_FILE), `${JSON.stringify(roots, null, 2)}\\n`);\n\n // Vite and its plugins are regular dependencies so this works from an\n // installed package, not only the workspace.\n const { build } = await import('vite');\n const { default: react } = await import('@vitejs/plugin-react');\n const { default: tailwindcss } = await import('@tailwindcss/vite');\n\n const clientRoot = path.join(hostRoot, 'src', 'web');\n\n await build({\n configFile: false,\n envDir: false,\n logLevel: 'warn',\n root: clientRoot,\n plugins: [webPluginOverridePlugin(clientRoot, generated), react(), tailwindcss()],\n resolve: {\n dedupe: [...DEDUPED_RUNTIMES],\n alias: [webPluginCssAlias(generated)],\n },\n build: {\n outDir: assetsDir,\n emptyOutDir: true,\n sourcemap: false,\n },\n });\n\n fs.writeFileSync(path.join(assetsDir, SERVER_REGISTRY_FILE), generated.serverRegistry);\n return { assetsDir, pluginIds: plugins.map((plugin) => plugin.pluginId) };\n}\n"],"mappings":"8RAQA,MAAM,EAAmB,CACvB,QACA,YACA,kBACA,wBACA,kCACF,EA2BA,eAAsB,EAAiB,EAAmE,CACxG,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC3C,EAAWA,EAAAA,mBAAmB,EAC9B,EAAUC,EAAAA,eAAe,EAAU,EAAQ,YAAa,CAAM,EACpE,EACE,6BAA6B,OAAO,EAAQ,MAAM,EAAE,kBAAkB,EAAQ,IAAK,GAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,IAAI,GAChH,EAEA,IAAM,EAAeC,EAAAA,QAAK,KAAK,EAAQ,OAAQ,WAAW,EACpD,EAAYA,EAAAA,QAAK,KAAK,EAAQ,OAAQ,KAAK,EAC3C,EAAYC,EAAAA,0BAA0B,EAAS,CAAY,EAC3D,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAQ,YAAY,IAAK,GAASD,EAAAA,QAAK,QAAQ,CAAI,CAAC,CAAC,CAAC,EAChF,EAAA,QAAG,cAAcA,EAAAA,QAAK,KAAK,EAAQ,OAAQE,EAAAA,iBAAiB,EAAG,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,GAAG,EAIpG,GAAM,CAAE,SAAU,MAAM,OAAO,QACzB,CAAE,QAAS,GAAU,MAAM,OAAO,wBAClC,CAAE,QAAS,GAAgB,MAAM,OAAO,qBAExC,EAAaF,EAAAA,QAAK,KAAK,EAAU,MAAO,KAAK,EAoBnD,OAlBA,MAAM,EAAM,CACV,WAAY,GACZ,OAAQ,GACR,SAAU,OACV,KAAM,EACN,QAAS,CAACG,EAAAA,wBAAwB,EAAY,CAAS,EAAG,EAAM,EAAG,EAAY,CAAC,EAChF,QAAS,CACP,OAAQ,CAAC,GAAG,CAAgB,EAC5B,MAAO,CAACC,EAAAA,kBAAkB,CAAS,CAAC,CACtC,EACA,MAAO,CACL,OAAQ,EACR,YAAa,GACb,UAAW,EACb,CACF,CAAC,EAED,EAAA,QAAG,cAAcJ,EAAAA,QAAK,KAAK,EAAWK,EAAAA,oBAAoB,EAAG,EAAU,cAAc,EAC9E,CAAE,YAAW,UAAW,EAAQ,IAAK,GAAW,EAAO,QAAQ,CAAE,CAC1E"}
@@ -0,0 +1,28 @@
1
+ //#region src/adapters/webBundler.d.ts
2
+ interface BundleCockpitWebOptions {
3
+ /** Package roots to scan for doompiWeb manifests, usually the installed composition. */
4
+ pluginRoots: readonly string[];
5
+ /** Output directory; receives web/ (the bundle), generated/ (the entry modules), and pluginRoots.json. */
6
+ outDir: string;
7
+ onNotice?: (message: string) => void;
8
+ }
9
+ interface BundleCockpitWebResult {
10
+ /** The assets directory to serve (contains index.html and the server registry). */
11
+ assetsDir: string;
12
+ pluginIds: string[];
13
+ }
14
+ /**
15
+ * Builds the cockpit SPA against the installed plugin set.
16
+ *
17
+ * This is what `doompi sync` calls: manifests come from the given package
18
+ * roots (no hardcoded plugin dependencies anywhere), the generated entry
19
+ * modules are aliased over the committed builtin registry, and Vite compiles
20
+ * everything (host shell plus each plugin's shipped web source) into one
21
+ * bundle. The server registry written next to the assets tells the hub which
22
+ * built hub entries to load, and the roots file beside the bundle lets the
23
+ * dev server serve the same composition with hot reload.
24
+ */
25
+ declare function bundleCockpitWeb(options: BundleCockpitWebOptions): Promise<BundleCockpitWebResult>;
26
+ //#endregion
27
+ export { BundleCockpitWebOptions, BundleCockpitWebResult, bundleCockpitWeb };
28
+ //# sourceMappingURL=webBundler.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webBundler.d.cts","names":[],"sources":["../../../src/adapters/webBundler.ts"],"mappings":";UAgBiB;;EAEf;;EAEA;EACA,YAAY;;UAGG;;EAEf;EACA;;;;;;;;;;;;;iBAcoB,iBAAiB,SAAS,0BAA0B,QAAQ"}
@@ -0,0 +1,28 @@
1
+ //#region src/adapters/webBundler.d.ts
2
+ interface BundleCockpitWebOptions {
3
+ /** Package roots to scan for doompiWeb manifests, usually the installed composition. */
4
+ pluginRoots: readonly string[];
5
+ /** Output directory; receives web/ (the bundle), generated/ (the entry modules), and pluginRoots.json. */
6
+ outDir: string;
7
+ onNotice?: (message: string) => void;
8
+ }
9
+ interface BundleCockpitWebResult {
10
+ /** The assets directory to serve (contains index.html and the server registry). */
11
+ assetsDir: string;
12
+ pluginIds: string[];
13
+ }
14
+ /**
15
+ * Builds the cockpit SPA against the installed plugin set.
16
+ *
17
+ * This is what `doompi sync` calls: manifests come from the given package
18
+ * roots (no hardcoded plugin dependencies anywhere), the generated entry
19
+ * modules are aliased over the committed builtin registry, and Vite compiles
20
+ * everything (host shell plus each plugin's shipped web source) into one
21
+ * bundle. The server registry written next to the assets tells the hub which
22
+ * built hub entries to load, and the roots file beside the bundle lets the
23
+ * dev server serve the same composition with hot reload.
24
+ */
25
+ declare function bundleCockpitWeb(options: BundleCockpitWebOptions): Promise<BundleCockpitWebResult>;
26
+ //#endregion
27
+ export { BundleCockpitWebOptions, BundleCockpitWebResult, bundleCockpitWeb };
28
+ //# sourceMappingURL=webBundler.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webBundler.d.mts","names":[],"sources":["../../../src/adapters/webBundler.ts"],"mappings":";UAgBiB;;EAEf;;EAEA;EACA,YAAY;;UAGG;;EAEf;EACA;;;;;;;;;;;;;iBAcoB,iBAAiB,SAAS,0BAA0B,QAAQ"}
@@ -0,0 +1,2 @@
1
+ import{PLUGIN_ROOTS_FILE as e}from"../services/webDevRoots.mjs";import{scanWebPlugins as t}from"./webPluginScan.mjs";import{SERVER_REGISTRY_FILE as n,webHostPackageRoot as r,writeSyncWebPluginModules as i}from"./webPluginGenerate.mjs";import{webPluginCssAlias as a,webPluginOverridePlugin as o}from"./webPluginVite.mjs";import s from"node:fs";import c from"node:path";const l=[`react`,`react-dom`,`@tanstack/store`,`@tanstack/react-store`,`@agimon-ai/doompi-web-components`];async function u(u){let d=u.onNotice??(()=>{}),f=r(),p=t(f,u.pluginRoots,d);d(`bundling the cockpit with ${String(p.length)} web plugin(s): ${p.map(e=>e.pluginId).join(`, `)}`);let m=c.join(u.outDir,`generated`),h=c.join(u.outDir,`web`),g=i(p,m),_=[...new Set(u.pluginRoots.map(e=>c.resolve(e)))];s.writeFileSync(c.join(u.outDir,e),`${JSON.stringify(_,null,2)}\n`);let{build:v}=await import(`vite`),{default:y}=await import(`@vitejs/plugin-react`),{default:b}=await import(`@tailwindcss/vite`),x=c.join(f,`src`,`web`);return await v({configFile:!1,envDir:!1,logLevel:`warn`,root:x,plugins:[o(x,g),y(),b()],resolve:{dedupe:[...l],alias:[a(g)]},build:{outDir:h,emptyOutDir:!0,sourcemap:!1}}),s.writeFileSync(c.join(h,n),g.serverRegistry),{assetsDir:h,pluginIds:p.map(e=>e.pluginId)}}export{u as bundleCockpitWeb};
2
+ //# sourceMappingURL=webBundler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webBundler.mjs","names":[],"sources":["../../../src/adapters/webBundler.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { PLUGIN_ROOTS_FILE } from '../services/webDevRoots.ts';\nimport { SERVER_REGISTRY_FILE, webHostPackageRoot, writeSyncWebPluginModules } from './webPluginGenerate.ts';\nimport { scanWebPlugins } from './webPluginScan.ts';\nimport { webPluginCssAlias, webPluginOverridePlugin } from './webPluginVite.ts';\n\n/** Shared runtime packages every plugin's client code must resolve to the host copies. */\nconst DEDUPED_RUNTIMES = [\n 'react',\n 'react-dom',\n '@tanstack/store',\n '@tanstack/react-store',\n '@agimon-ai/doompi-web-components',\n];\n\nexport interface BundleCockpitWebOptions {\n /** Package roots to scan for doompiWeb manifests, usually the installed composition. */\n pluginRoots: readonly string[];\n /** Output directory; receives web/ (the bundle), generated/ (the entry modules), and pluginRoots.json. */\n outDir: string;\n onNotice?: (message: string) => void;\n}\n\nexport interface BundleCockpitWebResult {\n /** The assets directory to serve (contains index.html and the server registry). */\n assetsDir: string;\n pluginIds: string[];\n}\n\n/**\n * Builds the cockpit SPA against the installed plugin set.\n *\n * This is what `doompi sync` calls: manifests come from the given package\n * roots (no hardcoded plugin dependencies anywhere), the generated entry\n * modules are aliased over the committed builtin registry, and Vite compiles\n * everything (host shell plus each plugin's shipped web source) into one\n * bundle. The server registry written next to the assets tells the hub which\n * built hub entries to load, and the roots file beside the bundle lets the\n * dev server serve the same composition with hot reload.\n */\nexport async function bundleCockpitWeb(options: BundleCockpitWebOptions): Promise<BundleCockpitWebResult> {\n const notice = options.onNotice ?? ((): void => {});\n const hostRoot = webHostPackageRoot();\n const plugins = scanWebPlugins(hostRoot, options.pluginRoots, notice);\n notice(\n `bundling the cockpit with ${String(plugins.length)} web plugin(s): ${plugins.map((p) => p.pluginId).join(', ')}`,\n );\n\n const generatedDir = path.join(options.outDir, 'generated');\n const assetsDir = path.join(options.outDir, 'web');\n const generated = writeSyncWebPluginModules(plugins, generatedDir);\n const roots = [...new Set(options.pluginRoots.map((root) => path.resolve(root)))];\n fs.writeFileSync(path.join(options.outDir, PLUGIN_ROOTS_FILE), `${JSON.stringify(roots, null, 2)}\\n`);\n\n // Vite and its plugins are regular dependencies so this works from an\n // installed package, not only the workspace.\n const { build } = await import('vite');\n const { default: react } = await import('@vitejs/plugin-react');\n const { default: tailwindcss } = await import('@tailwindcss/vite');\n\n const clientRoot = path.join(hostRoot, 'src', 'web');\n\n await build({\n configFile: false,\n envDir: false,\n logLevel: 'warn',\n root: clientRoot,\n plugins: [webPluginOverridePlugin(clientRoot, generated), react(), tailwindcss()],\n resolve: {\n dedupe: [...DEDUPED_RUNTIMES],\n alias: [webPluginCssAlias(generated)],\n },\n build: {\n outDir: assetsDir,\n emptyOutDir: true,\n sourcemap: false,\n },\n });\n\n fs.writeFileSync(path.join(assetsDir, SERVER_REGISTRY_FILE), generated.serverRegistry);\n return { assetsDir, pluginIds: plugins.map((plugin) => plugin.pluginId) };\n}\n"],"mappings":"gXAQA,MAAM,EAAmB,CACvB,QACA,YACA,kBACA,wBACA,kCACF,EA2BA,eAAsB,EAAiB,EAAmE,CACxG,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC3C,EAAW,EAAmB,EAC9B,EAAU,EAAe,EAAU,EAAQ,YAAa,CAAM,EACpE,EACE,6BAA6B,OAAO,EAAQ,MAAM,EAAE,kBAAkB,EAAQ,IAAK,GAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,IAAI,GAChH,EAEA,IAAM,EAAe,EAAK,KAAK,EAAQ,OAAQ,WAAW,EACpD,EAAY,EAAK,KAAK,EAAQ,OAAQ,KAAK,EAC3C,EAAY,EAA0B,EAAS,CAAY,EAC3D,EAAQ,CAAC,GAAG,IAAI,IAAI,EAAQ,YAAY,IAAK,GAAS,EAAK,QAAQ,CAAI,CAAC,CAAC,CAAC,EAChF,EAAG,cAAc,EAAK,KAAK,EAAQ,OAAQ,CAAiB,EAAG,GAAG,KAAK,UAAU,EAAO,KAAM,CAAC,EAAE,GAAG,EAIpG,GAAM,CAAE,SAAU,MAAM,OAAO,QACzB,CAAE,QAAS,GAAU,MAAM,OAAO,wBAClC,CAAE,QAAS,GAAgB,MAAM,OAAO,qBAExC,EAAa,EAAK,KAAK,EAAU,MAAO,KAAK,EAoBnD,OAlBA,MAAM,EAAM,CACV,WAAY,GACZ,OAAQ,GACR,SAAU,OACV,KAAM,EACN,QAAS,CAAC,EAAwB,EAAY,CAAS,EAAG,EAAM,EAAG,EAAY,CAAC,EAChF,QAAS,CACP,OAAQ,CAAC,GAAG,CAAgB,EAC5B,MAAO,CAAC,EAAkB,CAAS,CAAC,CACtC,EACA,MAAO,CACL,OAAQ,EACR,YAAa,GACb,UAAW,EACb,CACF,CAAC,EAED,EAAG,cAAc,EAAK,KAAK,EAAW,CAAoB,EAAG,EAAU,cAAc,EAC9E,CAAE,YAAW,UAAW,EAAQ,IAAK,GAAW,EAAO,QAAQ,CAAE,CAC1E"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./webPluginGenerate.cjs"),n=require("./webHubPlugins.generated.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:url");function o(e,n){let a=i.default.join(e,t.SERVER_REGISTRY_FILE);if(!r.default.existsSync(a))return[];try{let e=JSON.parse(r.default.readFileSync(a,`utf8`));return Array.isArray(e)?e:[]}catch(e){return n(`web plugin registry ${a} is unreadable (${String(e)}); plugin channels skipped`),[]}}async function s(e,t){let r=[...n.BUILTIN_HUB_CHANNELS];for(let n of o(e,t)){let e=typeof n.pluginId==`string`?n.pluginId:`unknown`;if(typeof n.hubEntry==`string`)try{let e=await import((0,a.pathToFileURL)(n.hubEntry).href);if(!Array.isArray(e.webHubChannels))throw Error(`'${n.hubEntry}' exports no webHubChannels array`);r.push(...e.webHubChannels)}catch(n){t(`web plugin '${e}' hub channels unavailable (${n instanceof Error?n.message:String(n)}); its panels stay empty`)}}let i=new Set,s=[];for(let e of r){if(i.has(e.frameType)){t(`duplicate web channel '${e.frameType}' dropped; frame types are global`);continue}i.add(e.frameType),s.push(e)}return s}exports.loadHubChannels=s;
2
+ //# sourceMappingURL=webHubPluginLoader.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webHubPluginLoader.cjs","names":["path","SERVER_REGISTRY_FILE","fs","BUILTIN_HUB_CHANNELS","pathToFileURL"],"sources":["../../../src/adapters/webHubPluginLoader.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { SERVER_REGISTRY_FILE } from './webPluginGenerate.ts';\nimport { BUILTIN_HUB_CHANNELS } from './webHubPlugins.generated.ts';\n\ninterface RegistryEntry {\n pluginId?: unknown;\n hubEntry?: unknown;\n}\n\nconst UNKNOWN_PLUGIN_ID = 'unknown';\n\n/** The synced bundle's server registry, or an empty list when there is none to read. */\nfunction registryEntries(assetsDir: string, onNotice: (message: string) => void): RegistryEntry[] {\n const registryPath = path.join(assetsDir, SERVER_REGISTRY_FILE);\n if (!fs.existsSync(registryPath)) return [];\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(registryPath, 'utf8'));\n return Array.isArray(parsed) ? (parsed as RegistryEntry[]) : [];\n } catch (error) {\n onNotice(`web plugin registry ${registryPath} is unreadable (${String(error)}); plugin channels skipped`);\n return [];\n }\n}\n\n/**\n * Assembles the hub's data channels for the assets being served.\n *\n * A synced bundle carries webPlugins.server.json naming each plugin's built\n * hub entry by absolute path; those are imported lazily so an absent or\n * broken plugin package is a notice and an empty tab, never a crash. Without\n * a registry (the package's own prebuilt bundle, or a custom assets dir) the\n * built-in channels are the whole set.\n */\nexport async function loadHubChannels(\n assetsDir: string,\n onNotice: (message: string) => void,\n): Promise<WebHubChannel[]> {\n const channels: WebHubChannel[] = [...BUILTIN_HUB_CHANNELS];\n for (const entry of registryEntries(assetsDir, onNotice)) {\n const pluginId = typeof entry.pluginId === 'string' ? entry.pluginId : UNKNOWN_PLUGIN_ID;\n if (typeof entry.hubEntry !== 'string') continue;\n try {\n const module = (await import(pathToFileURL(entry.hubEntry).href)) as { webHubChannels?: unknown };\n if (!Array.isArray(module.webHubChannels)) {\n throw new Error(`'${entry.hubEntry}' exports no webHubChannels array`);\n }\n channels.push(...(module.webHubChannels as WebHubChannel[]));\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n onNotice(`web plugin '${pluginId}' hub channels unavailable (${cause}); its panels stay empty`);\n }\n }\n const seen = new Set<string>();\n const unique: WebHubChannel[] = [];\n for (const channel of channels) {\n if (seen.has(channel.frameType)) {\n onNotice(`duplicate web channel '${channel.frameType}' dropped; frame types are global`);\n continue;\n }\n seen.add(channel.frameType);\n unique.push(channel);\n }\n return unique;\n}\n"],"mappings":"wPAeA,SAAS,EAAgB,EAAmB,EAAsD,CAChG,IAAM,EAAeA,EAAAA,QAAK,KAAK,EAAWC,EAAAA,oBAAoB,EAC9D,GAAI,CAACC,EAAAA,QAAG,WAAW,CAAY,EAAG,MAAO,CAAC,EAC1C,GAAI,CACF,IAAM,EAAkB,KAAK,MAAMA,EAAAA,QAAG,aAAa,EAAc,MAAM,CAAC,EACxE,OAAO,MAAM,QAAQ,CAAM,EAAK,EAA6B,CAAC,CAChE,OAAS,EAAO,CAEd,OADA,EAAS,uBAAuB,EAAa,kBAAkB,OAAO,CAAK,EAAE,2BAA2B,EACjG,CAAC,CACV,CACF,CAWA,eAAsB,EACpB,EACA,EAC0B,CAC1B,IAAM,EAA4B,CAAC,GAAGC,EAAAA,oBAAoB,EAC1D,IAAK,IAAM,KAAS,EAAgB,EAAW,CAAQ,EAAG,CACxD,IAAM,EAAW,OAAO,EAAM,UAAa,SAAW,EAAM,SAAW,UACnE,UAAO,EAAM,UAAa,SAC9B,GAAI,CACF,IAAM,EAAU,MAAM,QAAA,EAAOC,EAAAA,cAAAA,CAAc,EAAM,QAAQ,CAAC,CAAC,MAC3D,GAAI,CAAC,MAAM,QAAQ,EAAO,cAAc,EACtC,MAAU,MAAM,IAAI,EAAM,SAAS,kCAAkC,EAEvE,EAAS,KAAK,GAAI,EAAO,cAAkC,CAC7D,OAAS,EAAO,CAEd,EAAS,eAAe,EAAS,8BADnB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACE,yBAAyB,CAChG,CACF,CACA,IAAM,EAAO,IAAI,IACX,EAA0B,CAAC,EACjC,IAAK,IAAM,KAAW,EAAU,CAC9B,GAAI,EAAK,IAAI,EAAQ,SAAS,EAAG,CAC/B,EAAS,0BAA0B,EAAQ,UAAU,kCAAkC,EACvF,QACF,CACA,EAAK,IAAI,EAAQ,SAAS,EAC1B,EAAO,KAAK,CAAO,CACrB,CACA,OAAO,CACT"}
@@ -0,0 +1,2 @@
1
+ import{SERVER_REGISTRY_FILE as e}from"./webPluginGenerate.mjs";import{BUILTIN_HUB_CHANNELS as t}from"./webHubPlugins.generated.mjs";import n from"node:fs";import r from"node:path";import{pathToFileURL as i}from"node:url";function a(t,i){let a=r.join(t,e);if(!n.existsSync(a))return[];try{let e=JSON.parse(n.readFileSync(a,`utf8`));return Array.isArray(e)?e:[]}catch(e){return i(`web plugin registry ${a} is unreadable (${String(e)}); plugin channels skipped`),[]}}async function o(e,n){let r=[...t];for(let t of a(e,n)){let e=typeof t.pluginId==`string`?t.pluginId:`unknown`;if(typeof t.hubEntry==`string`)try{let e=await import(i(t.hubEntry).href);if(!Array.isArray(e.webHubChannels))throw Error(`'${t.hubEntry}' exports no webHubChannels array`);r.push(...e.webHubChannels)}catch(t){n(`web plugin '${e}' hub channels unavailable (${t instanceof Error?t.message:String(t)}); its panels stay empty`)}}let o=new Set,s=[];for(let e of r){if(o.has(e.frameType)){n(`duplicate web channel '${e.frameType}' dropped; frame types are global`);continue}o.add(e.frameType),s.push(e)}return s}export{o as loadHubChannels};
2
+ //# sourceMappingURL=webHubPluginLoader.mjs.map