@agimon-ai/doompi-web 0.0.1-alpha.1 → 0.0.1-alpha.11

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 (329) hide show
  1. package/README.md +84 -49
  2. package/dist/bin/serve.cjs +1 -1
  3. package/dist/bin/serve.cjs.map +1 -1
  4. package/dist/bin/serve.mjs +1 -1
  5. package/dist/bin/serve.mjs.map +1 -1
  6. package/dist/index.d.cts +2 -2
  7. package/dist/index.d.mts +2 -2
  8. package/dist/pwa/icon-192.png +0 -0
  9. package/dist/pwa/icon-512.png +0 -0
  10. package/dist/pwa/index.html +104 -0
  11. package/dist/pwa/manifest.webmanifest +23 -0
  12. package/dist/pwa/pwa.js +21 -0
  13. package/dist/pwa/sw.js +2 -0
  14. package/dist/src/adapters/bundlePublication.cjs +3 -0
  15. package/dist/src/adapters/bundlePublication.cjs.map +1 -0
  16. package/dist/src/adapters/bundlePublication.mjs +3 -0
  17. package/dist/src/adapters/bundlePublication.mjs.map +1 -0
  18. package/dist/src/adapters/bundledServer.cjs +1 -1
  19. package/dist/src/adapters/bundledServer.cjs.map +1 -1
  20. package/dist/src/adapters/bundledServer.mjs +1 -1
  21. package/dist/src/adapters/bundledServer.mjs.map +1 -1
  22. package/dist/src/adapters/doomInitialization.cjs +2 -0
  23. package/dist/src/adapters/doomInitialization.cjs.map +1 -0
  24. package/dist/src/adapters/doomInitialization.mjs +2 -0
  25. package/dist/src/adapters/doomInitialization.mjs.map +1 -0
  26. package/dist/src/adapters/httpServer.cjs +2 -1
  27. package/dist/src/adapters/httpServer.cjs.map +1 -1
  28. package/dist/src/adapters/httpServer.d.cts +2 -0
  29. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  30. package/dist/src/adapters/httpServer.d.mts +2 -0
  31. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  32. package/dist/src/adapters/httpServer.mjs +2 -1
  33. package/dist/src/adapters/httpServer.mjs.map +1 -1
  34. package/dist/src/adapters/hubProbe.cjs +1 -1
  35. package/dist/src/adapters/hubProbe.cjs.map +1 -1
  36. package/dist/src/adapters/hubProbe.mjs +1 -1
  37. package/dist/src/adapters/hubProbe.mjs.map +1 -1
  38. package/dist/src/adapters/packageApiProxy.cjs +1 -1
  39. package/dist/src/adapters/packageApiProxy.cjs.map +1 -1
  40. package/dist/src/adapters/packageApiProxy.mjs +1 -1
  41. package/dist/src/adapters/packageApiProxy.mjs.map +1 -1
  42. package/dist/src/adapters/promptImages.cjs +3 -0
  43. package/dist/src/adapters/promptImages.cjs.map +1 -0
  44. package/dist/src/adapters/promptImages.mjs +3 -0
  45. package/dist/src/adapters/promptImages.mjs.map +1 -0
  46. package/dist/src/adapters/registryWatcher.cjs +1 -1
  47. package/dist/src/adapters/registryWatcher.cjs.map +1 -1
  48. package/dist/src/adapters/registryWatcher.d.cts +1 -0
  49. package/dist/src/adapters/registryWatcher.d.mts +1 -0
  50. package/dist/src/adapters/registryWatcher.mjs +1 -1
  51. package/dist/src/adapters/registryWatcher.mjs.map +1 -1
  52. package/dist/src/adapters/remoteAccess.cjs +1 -1
  53. package/dist/src/adapters/remoteAccess.cjs.map +1 -1
  54. package/dist/src/adapters/remoteAccess.mjs +1 -1
  55. package/dist/src/adapters/remoteAccess.mjs.map +1 -1
  56. package/dist/src/adapters/remoteRoutes.cjs +1 -1
  57. package/dist/src/adapters/remoteRoutes.cjs.map +1 -1
  58. package/dist/src/adapters/remoteRoutes.mjs +1 -1
  59. package/dist/src/adapters/remoteRoutes.mjs.map +1 -1
  60. package/dist/src/adapters/serverSpawner.cjs +1 -1
  61. package/dist/src/adapters/serverSpawner.cjs.map +1 -1
  62. package/dist/src/adapters/serverSpawner.d.cts +1 -0
  63. package/dist/src/adapters/serverSpawner.d.mts +1 -0
  64. package/dist/src/adapters/serverSpawner.mjs +1 -1
  65. package/dist/src/adapters/serverSpawner.mjs.map +1 -1
  66. package/dist/src/adapters/sessionFiles.cjs +1 -1
  67. package/dist/src/adapters/sessionFiles.cjs.map +1 -1
  68. package/dist/src/adapters/sessionFiles.mjs +1 -1
  69. package/dist/src/adapters/sessionFiles.mjs.map +1 -1
  70. package/dist/src/adapters/sessionHub.cjs +1 -1
  71. package/dist/src/adapters/sessionHub.cjs.map +1 -1
  72. package/dist/src/adapters/sessionHub.d.cts +6 -0
  73. package/dist/src/adapters/sessionHub.d.mts +6 -0
  74. package/dist/src/adapters/sessionHub.mjs +1 -1
  75. package/dist/src/adapters/sessionHub.mjs.map +1 -1
  76. package/dist/src/adapters/settingsRoutes.cjs +1 -1
  77. package/dist/src/adapters/settingsRoutes.cjs.map +1 -1
  78. package/dist/src/adapters/settingsRoutes.mjs +1 -1
  79. package/dist/src/adapters/settingsRoutes.mjs.map +1 -1
  80. package/dist/src/adapters/syncGuard.cjs +1 -1
  81. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  82. package/dist/src/adapters/syncGuard.mjs +1 -1
  83. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  84. package/dist/src/adapters/threadJournals.cjs +1 -1
  85. package/dist/src/adapters/threadJournals.cjs.map +1 -1
  86. package/dist/src/adapters/threadJournals.mjs +1 -1
  87. package/dist/src/adapters/threadJournals.mjs.map +1 -1
  88. package/dist/src/adapters/tunnelProcess.cjs +1 -1
  89. package/dist/src/adapters/tunnelProcess.cjs.map +1 -1
  90. package/dist/src/adapters/tunnelProcess.mjs +1 -1
  91. package/dist/src/adapters/tunnelProcess.mjs.map +1 -1
  92. package/dist/src/adapters/webBundler.cjs +1 -1
  93. package/dist/src/adapters/webBundler.cjs.map +1 -1
  94. package/dist/src/adapters/webBundler.d.cts +1 -1
  95. package/dist/src/adapters/webBundler.d.cts.map +1 -1
  96. package/dist/src/adapters/webBundler.d.mts +1 -1
  97. package/dist/src/adapters/webBundler.d.mts.map +1 -1
  98. package/dist/src/adapters/webBundler.mjs +1 -1
  99. package/dist/src/adapters/webBundler.mjs.map +1 -1
  100. package/dist/src/adapters/webComposition.cjs +3 -0
  101. package/dist/src/adapters/webComposition.cjs.map +1 -0
  102. package/dist/src/adapters/webComposition.mjs +3 -0
  103. package/dist/src/adapters/webComposition.mjs.map +1 -0
  104. package/dist/src/adapters/webHubPluginLoader.cjs +1 -1
  105. package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -1
  106. package/dist/src/adapters/webHubPluginLoader.mjs +1 -1
  107. package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -1
  108. package/dist/src/adapters/webPluginGenerate.cjs +3 -3
  109. package/dist/src/adapters/webPluginGenerate.cjs.map +1 -1
  110. package/dist/src/adapters/webPluginGenerate.mjs +3 -3
  111. package/dist/src/adapters/webPluginGenerate.mjs.map +1 -1
  112. package/dist/src/adapters/webPluginScan.cjs +1 -1
  113. package/dist/src/adapters/webPluginScan.cjs.map +1 -1
  114. package/dist/src/adapters/webPluginScan.mjs +1 -1
  115. package/dist/src/adapters/webPluginScan.mjs.map +1 -1
  116. package/dist/src/adapters/webPluginVite.cjs +1 -1
  117. package/dist/src/adapters/webPluginVite.cjs.map +1 -1
  118. package/dist/src/adapters/webPluginVite.mjs +1 -1
  119. package/dist/src/adapters/webPluginVite.mjs.map +1 -1
  120. package/dist/src/adapters/webPush.cjs +2 -0
  121. package/dist/src/adapters/webPush.cjs.map +1 -0
  122. package/dist/src/adapters/webPush.mjs +2 -0
  123. package/dist/src/adapters/webPush.mjs.map +1 -0
  124. package/dist/src/adapters/webTelemetry.cjs +1 -1
  125. package/dist/src/adapters/webTelemetry.cjs.map +1 -1
  126. package/dist/src/adapters/webTelemetry.mjs +1 -1
  127. package/dist/src/adapters/webTelemetry.mjs.map +1 -1
  128. package/dist/src/services/fileMatch.cjs +1 -1
  129. package/dist/src/services/fileMatch.cjs.map +1 -1
  130. package/dist/src/services/fileMatch.mjs +1 -1
  131. package/dist/src/services/fileMatch.mjs.map +1 -1
  132. package/dist/src/services/fileMedia.cjs +1 -1
  133. package/dist/src/services/fileMedia.cjs.map +1 -1
  134. package/dist/src/services/fileMedia.mjs +1 -1
  135. package/dist/src/services/fileMedia.mjs.map +1 -1
  136. package/dist/src/services/pairingFlow.cjs +1 -1
  137. package/dist/src/services/pairingFlow.cjs.map +1 -1
  138. package/dist/src/services/pairingFlow.mjs +1 -1
  139. package/dist/src/services/pairingFlow.mjs.map +1 -1
  140. package/dist/src/services/pairingPage.cjs +212 -74
  141. package/dist/src/services/pairingPage.cjs.map +1 -1
  142. package/dist/src/services/pairingPage.mjs +212 -74
  143. package/dist/src/services/pairingPage.mjs.map +1 -1
  144. package/dist/src/services/remoteGuardPolicy.cjs +1 -1
  145. package/dist/src/services/remoteGuardPolicy.cjs.map +1 -1
  146. package/dist/src/services/remoteGuardPolicy.mjs +1 -1
  147. package/dist/src/services/remoteGuardPolicy.mjs.map +1 -1
  148. package/dist/src/services/serveOptions.cjs +14 -1
  149. package/dist/src/services/serveOptions.cjs.map +1 -1
  150. package/dist/src/services/serveOptions.d.cts +4 -0
  151. package/dist/src/services/serveOptions.d.cts.map +1 -1
  152. package/dist/src/services/serveOptions.d.mts +4 -0
  153. package/dist/src/services/serveOptions.d.mts.map +1 -1
  154. package/dist/src/services/serveOptions.mjs +14 -1
  155. package/dist/src/services/serveOptions.mjs.map +1 -1
  156. package/dist/src/services/webauthnPolicy.cjs +1 -1
  157. package/dist/src/services/webauthnPolicy.cjs.map +1 -1
  158. package/dist/src/services/webauthnPolicy.mjs +1 -1
  159. package/dist/src/services/webauthnPolicy.mjs.map +1 -1
  160. package/dist/src/types/bridge.d.cts +2 -0
  161. package/dist/src/types/bridge.d.cts.map +1 -1
  162. package/dist/src/types/bridge.d.mts +2 -0
  163. package/dist/src/types/bridge.d.mts.map +1 -1
  164. package/dist/src/types/hub.cjs +1 -1
  165. package/dist/src/types/hub.cjs.map +1 -1
  166. package/dist/src/types/hub.d.cts +1 -1
  167. package/dist/src/types/hub.d.cts.map +1 -1
  168. package/dist/src/types/hub.d.mts +1 -1
  169. package/dist/src/types/hub.d.mts.map +1 -1
  170. package/dist/src/types/hub.mjs +1 -1
  171. package/dist/src/types/hub.mjs.map +1 -1
  172. package/dist/src/types/remoteAccess.cjs +1 -1
  173. package/dist/src/types/remoteAccess.cjs.map +1 -1
  174. package/dist/src/types/remoteAccess.d.cts.map +1 -1
  175. package/dist/src/types/remoteAccess.d.mts.map +1 -1
  176. package/dist/src/types/remoteAccess.mjs +1 -1
  177. package/dist/src/types/remoteAccess.mjs.map +1 -1
  178. package/dist/src/types/session.cjs +1 -1
  179. package/dist/src/types/session.mjs +1 -1
  180. package/dist/src/types/session.mjs.map +1 -1
  181. package/dist/src/types/settings.cjs +1 -1
  182. package/dist/src/types/settings.cjs.map +1 -1
  183. package/dist/src/types/settings.d.cts +1 -0
  184. package/dist/src/types/settings.d.mts +1 -0
  185. package/dist/src/types/settings.mjs +1 -1
  186. package/dist/src/types/settings.mjs.map +1 -1
  187. package/dist/web/assets/CodeEditorView-Cv2_Pqe1.js +15 -0
  188. package/dist/web/assets/CodeEditorView-Cv2_Pqe1.js.map +1 -0
  189. package/dist/web/assets/{dist-DPmeQCk2.js → dist-1N2cDSZc.js} +2 -2
  190. package/dist/web/assets/{dist-DPmeQCk2.js.map → dist-1N2cDSZc.js.map} +1 -1
  191. package/dist/web/assets/{dist-CuZIwX62.js → dist-BvJltgg9.js} +2 -2
  192. package/dist/web/assets/{dist-CuZIwX62.js.map → dist-BvJltgg9.js.map} +1 -1
  193. package/dist/web/assets/{dist-DASJVcWF.js → dist-CMcoiIPx.js} +2 -2
  194. package/dist/web/assets/{dist-DASJVcWF.js.map → dist-CMcoiIPx.js.map} +1 -1
  195. package/dist/web/assets/{dist-BIM3Wiup.js → dist-CSIKLWGg.js} +2 -2
  196. package/dist/web/assets/{dist-BIM3Wiup.js.map → dist-CSIKLWGg.js.map} +1 -1
  197. package/dist/web/assets/{dist-BNh-qcPn.js → dist-CVA2128k.js} +1 -1
  198. package/dist/web/assets/{dist-nYGDKnBj.js → dist-CfQNkp46.js} +2 -2
  199. package/dist/web/assets/{dist-nYGDKnBj.js.map → dist-CfQNkp46.js.map} +1 -1
  200. package/dist/web/assets/{dist-Q1Vv-_7E.js → dist-Dc0vEh5H.js} +2 -2
  201. package/dist/web/assets/{dist-Q1Vv-_7E.js.map → dist-Dc0vEh5H.js.map} +1 -1
  202. package/dist/web/assets/{dist-BpSem-7r.js → dist-DlhyAlgc.js} +2 -2
  203. package/dist/web/assets/{dist-BpSem-7r.js.map → dist-DlhyAlgc.js.map} +1 -1
  204. package/dist/web/assets/{dist-DHhgwtts.js → dist-Dusr83oZ.js} +2 -2
  205. package/dist/web/assets/{dist-DHhgwtts.js.map → dist-Dusr83oZ.js.map} +1 -1
  206. package/dist/web/assets/{dist-CvW-klGY.js → dist-x765ui50.js} +2 -2
  207. package/dist/web/assets/{dist-CvW-klGY.js.map → dist-x765ui50.js.map} +1 -1
  208. package/dist/web/assets/index-CdO0WgVA.js +101 -0
  209. package/dist/web/assets/index-CdO0WgVA.js.map +1 -0
  210. package/dist/web/assets/index-_BcdH66j.css +2 -0
  211. package/dist/web/index.html +3 -2
  212. package/package.json +23 -20
  213. package/src/adapters/bundlePublication.ts +156 -0
  214. package/src/adapters/bundledServer.ts +13 -6
  215. package/src/adapters/doomInitialization.ts +49 -0
  216. package/src/adapters/httpServer.ts +458 -55
  217. package/src/adapters/hubProbe.ts +34 -5
  218. package/src/adapters/packageApiProxy.ts +2 -0
  219. package/src/adapters/promptImages.ts +80 -0
  220. package/src/adapters/registryWatcher.ts +33 -30
  221. package/src/adapters/remoteAccess.ts +46 -12
  222. package/src/adapters/remoteRoutes.ts +13 -3
  223. package/src/adapters/serverSpawner.ts +38 -2
  224. package/src/adapters/sessionFiles.ts +26 -5
  225. package/src/adapters/sessionHub.ts +189 -14
  226. package/src/adapters/settingsRoutes.ts +238 -0
  227. package/src/adapters/syncGuard.ts +129 -38
  228. package/src/adapters/threadJournals.ts +69 -9
  229. package/src/adapters/tunnelProcess.ts +192 -17
  230. package/src/adapters/webBundler.ts +5 -2
  231. package/src/adapters/webComposition.ts +240 -0
  232. package/src/adapters/webHubPluginLoader.ts +6 -2
  233. package/src/adapters/webPluginGenerate.ts +19 -1
  234. package/src/adapters/webPluginScan.ts +6 -2
  235. package/src/adapters/webPluginVite.ts +24 -9
  236. package/src/adapters/webPush.ts +127 -0
  237. package/src/adapters/webTelemetry.ts +109 -24
  238. package/src/bin/serve.ts +64 -8
  239. package/src/pwa/bundleCache.ts +79 -0
  240. package/src/pwa/index.html +104 -0
  241. package/src/pwa/main.ts +163 -0
  242. package/src/pwa/public/icon-192.png +0 -0
  243. package/src/pwa/public/icon-512.png +0 -0
  244. package/src/pwa/public/manifest.webmanifest +23 -0
  245. package/src/pwa/qrScanner.ts +65 -0
  246. package/src/pwa/serviceWorker.ts +314 -0
  247. package/src/pwa/workerClient.ts +60 -0
  248. package/src/services/fileMatch.ts +5 -1
  249. package/src/services/fileMedia.ts +1 -0
  250. package/src/services/pairingFlow.ts +13 -10
  251. package/src/services/pairingPage.ts +228 -81
  252. package/src/services/remoteGuardPolicy.ts +9 -7
  253. package/src/services/serveOptions.ts +45 -2
  254. package/src/services/webauthnPolicy.ts +17 -7
  255. package/src/types/bridge.ts +2 -0
  256. package/src/types/bundle.ts +28 -0
  257. package/src/types/hub.ts +68 -3
  258. package/src/types/remoteAccess.ts +12 -0
  259. package/src/types/settings.ts +71 -8
  260. package/src/web/app/Providers.tsx +7 -3
  261. package/src/web/app/protocolRuntime.ts +3 -2
  262. package/src/web/app/sessionRuntime.ts +72 -4
  263. package/src/web/features/activity/ActivityDock.tsx +126 -85
  264. package/src/web/features/activity/ContextItemDialog.tsx +204 -0
  265. package/src/web/features/activity/ContextPanel.tsx +173 -0
  266. package/src/web/features/activity/DockTabs.tsx +39 -0
  267. package/src/web/features/dialogs/DialogOverlay.tsx +9 -3
  268. package/src/web/features/leader/CommandPalette.tsx +18 -6
  269. package/src/web/features/remote/RemoteAccessOptions.tsx +12 -6
  270. package/src/web/features/remote/RemoteAccessPairing.tsx +25 -3
  271. package/src/web/features/remote/RemoteBanner.tsx +2 -3
  272. package/src/web/features/session/Composer.tsx +151 -91
  273. package/src/web/features/session/MentionPreviews.tsx +74 -17
  274. package/src/web/features/session/MessageMarkdown.tsx +31 -0
  275. package/src/web/features/session/QueueSheet.tsx +130 -0
  276. package/src/web/features/session/ThreadView.tsx +15 -1
  277. package/src/web/features/session/Timeline.tsx +213 -56
  278. package/src/web/features/session/ToolCard.tsx +157 -8
  279. package/src/web/features/sessions/NewSessionDialog.tsx +15 -10
  280. package/src/web/features/sessions/ResumeSessionDialog.tsx +150 -0
  281. package/src/web/features/sessions/SessionRail.tsx +52 -26
  282. package/src/web/features/settings/AppearanceSettings.tsx +6 -8
  283. package/src/web/features/settings/ContributedSettings.tsx +73 -90
  284. package/src/web/features/settings/ImageSettings.tsx +112 -0
  285. package/src/web/features/settings/LoginFlowDialog.tsx +7 -4
  286. package/src/web/features/settings/NotificationSettings.tsx +39 -16
  287. package/src/web/features/settings/PluginSettings.tsx +14 -3
  288. package/src/web/features/settings/ProviderSettings.tsx +24 -14
  289. package/src/web/features/settings/RemoteControlSettings.tsx +6 -8
  290. package/src/web/features/settings/RepositorySettings.tsx +373 -0
  291. package/src/web/features/settings/RepositoryWorkspace.tsx +158 -0
  292. package/src/web/features/settings/SettingsMenu.tsx +100 -26
  293. package/src/web/features/settings/SettingsPanelHost.tsx +34 -0
  294. package/src/web/features/settings/SettingsSectionHeader.tsx +34 -0
  295. package/src/web/features/settings/TunnelSettings.tsx +29 -16
  296. package/src/web/features/status/TopBar.tsx +12 -66
  297. package/src/web/index.html +1 -0
  298. package/src/web/lib/browserTelemetry.ts +112 -7
  299. package/src/web/lib/commands.ts +39 -0
  300. package/src/web/lib/composition.ts +134 -15
  301. package/src/web/lib/contextComposition.ts +200 -0
  302. package/src/web/lib/contextDetailApi.ts +55 -0
  303. package/src/web/lib/fileMentions.ts +7 -3
  304. package/src/web/lib/hubApi.ts +49 -1
  305. package/src/web/lib/livePush.ts +116 -0
  306. package/src/web/lib/pluginRegistry.ts +53 -0
  307. package/src/web/lib/pluginSlotProps.ts +8 -2
  308. package/src/web/lib/protocolTimeline.ts +22 -4
  309. package/src/web/lib/remoteApi.ts +27 -3
  310. package/src/web/lib/sessionAsset.ts +30 -0
  311. package/src/web/lib/sessionModel.ts +201 -21
  312. package/src/web/lib/settingsApi.ts +75 -0
  313. package/src/web/lib/settingsSections.ts +108 -12
  314. package/src/web/lib/threadRenderer.ts +4 -3
  315. package/src/web/lib/timelineGroups.ts +91 -0
  316. package/src/web/lib/transport.ts +12 -0
  317. package/src/web/main.tsx +4 -0
  318. package/src/web/routes/CockpitPage.tsx +21 -6
  319. package/src/web/routes/SettingsPage.tsx +78 -17
  320. package/src/web/stores/composerStore.ts +141 -0
  321. package/src/web/stores/remoteAccessStore.ts +27 -3
  322. package/src/web/stores/sessionStore.ts +136 -14
  323. package/src/web/stores/uiStore.ts +30 -1
  324. package/src/web/stores/usePluginSlotProps.ts +20 -9
  325. package/dist/web/assets/CodeEditorView-D-TJJJaq.js +0 -15
  326. package/dist/web/assets/CodeEditorView-D-TJJJaq.js.map +0 -1
  327. package/dist/web/assets/index-D8NYSvmx.css +0 -2
  328. package/dist/web/assets/index-dUt-mAKq.js +0 -97
  329. package/dist/web/assets/index-dUt-mAKq.js.map +0 -1
package/README.md CHANGED
@@ -26,27 +26,31 @@ npm install -g @agimon-ai/doompi-web
26
26
  doompi-web
27
27
  ```
28
28
 
29
- Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and can start new sessions from the page. Sessions started by the cockpit use the server and agent from the same installation.
29
+ Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and resolves the union of their synchronized cockpit compositions from `~/.pi/.doom/sync/registrations`. The directory that launches `doompi-web` does not select the bundle. Sessions started by the cockpit use the server and agent from the same installation.
30
30
 
31
- If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
31
+ If `~/.pi/.doom` does not exist, the command runs its bundled `doompi init` before starting. If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
32
32
 
33
- To run this repository's build:
33
+ To run this repository's build with its composition pinned and watched:
34
34
 
35
35
  ```bash
36
- pnpm cockpit
36
+ pnpm cockpit:build
37
+ pnpm doompi-web --dir=<PWD>
37
38
  ```
38
39
 
39
- The command builds the required packages and starts the hub on port 7433. Sessions created from the page use this checkout's builds.
40
-
41
- | Flag | Default | Meaning |
42
- | ----------------- | --------------- | ------------------------------------------------ |
43
- | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
44
- | `--spawn-command` | this install | Command launching sessions created from the page |
45
- | `--port` | `7433` | HTTP port |
46
- | `--host` | `127.0.0.1` | Bind address |
47
- | `--assets` | bundled | Override the built SPA directory |
48
- | `--state-dir` | `~/.doompi/web` | Remote-access settings and the tunnel pid file |
49
- | `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
40
+ `--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
41
+
42
+ | Flag | Default | Meaning |
43
+ | ----------------- | ------------------------ | ------------------------------------------------ |
44
+ | `--dir` | live-session union | Pin, sync, and watch one repository composition |
45
+ | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
46
+ | `--spawn-command` | this install | Command launching sessions created from the page |
47
+ | `--port` | `7433` | HTTP port |
48
+ | `--host` | `127.0.0.1` | Bind address |
49
+ | `--assets` | synchronized or packaged | Override the built SPA directory |
50
+ | `--state-dir` | `~/.doompi/web` | Remote-access settings and cockpit cache |
51
+ | `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
52
+ | `-h`, `--help` | | Print command help |
53
+ | `-v`, `--version` | | Print the installed package version |
50
54
 
51
55
  Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) when you also want to run a session server directly. The hub does not require `doompi-server` on `PATH` to create sessions.
52
56
 
@@ -120,8 +124,27 @@ perform the handshake itself: `attach` frames sent from the browser are refused.
120
124
  them.
121
125
  - Subagent, workflow, and runner surfaces report summaries only; per-run detail has no RPC source
122
126
  to render from yet.
123
- - Browser notifications require a live, connected page. There is no service worker, Web Push, replay,
124
- or durable queue, so notifications emitted while every page is closed are not delivered later.
127
+ - Open pages show full live browser notifications. An installed paired PWA can also receive a generic
128
+ zero-TTL Web Push alert while closed. There is no durable subscription database, outbox, replay, or
129
+ historical delivery.
130
+
131
+ ## iPhone Home Screen app
132
+
133
+ Use a stable named HTTPS tunnel such as `doompi.agimon.win`. Quick-tunnel hostnames rotate, so they cannot
134
+ provide durable PWA identity, passkeys, or reliable Push subscriptions. Open `/pair` in Safari, add it to the
135
+ Home Screen, then launch the installed app and scan the QR shown by the host.
136
+
137
+ The install shell, scanner, manifest, and verifier worker are package-owned. The QR pins the host signing key
138
+ and minimum bundle revision. The worker verifies every host and plugin asset before committing it to the
139
+ active cache, and the host never serves its SPA directly. A failed update keeps the last-known-good bundle.
140
+ Signer loss, signer rotation, or a host/container transition requires explicit fingerprint confirmation and
141
+ re-pairing. Session file previews remain sealed `no-store` HTTP Blob URLs and never enter the application
142
+ cache, IndexedDB, a signed manifest, or a Push payload.
143
+
144
+ Closed-app alerts are opt-in under settings. The server keeps subscriptions only in memory and sends fixed
145
+ generic copy with `TTL: 0` only when the device has no connected cockpit socket. Opening the app after a host
146
+ restart re-registers the browser subscription. Disabling alerts, revoking or expiring the device, switching
147
+ off remote access, or rotating process credentials removes it.
125
148
 
126
149
  ## Remote access
127
150
 
@@ -137,12 +160,13 @@ so the socket a connection arrived on is the only thing about it that cannot be
137
160
 
138
161
  Pairing takes **two** things, not one:
139
162
 
140
- 1. The host shows a QR encoding `https://<tunnel-host>/pair#c=<code>`. The code rides in the URL
141
- fragment, which no browser sends to any server, so it never reaches Cloudflare's logs, this
142
- process's log, or a `Referer`.
143
- 2. Scanning it raises an approve or deny prompt **on the host**. Nothing is paired until someone at
144
- the machine answers. A screen is visible over a shoulder and in a screenshare, so the code alone
145
- is one factor for a credential that ends in shell access.
163
+ 1. The host shows a QR encoding `https://<tunnel-host>/pair#c=<code>&k=<channel-key>&s=<signer>&r=<revision>`.
164
+ Every value rides in the URL fragment, which no browser sends to a server, so the relay cannot replace the
165
+ channel or bundle key without changing what was shown on the host. Manual pairing requires comparing the
166
+ signing-key fingerprint shown on both devices.
167
+ 2. Scanning raises an approve or deny prompt **on the host**. Nothing is paired until someone at the machine
168
+ answers. A screen is visible over a shoulder and in a screenshare, so the code alone is one factor for a
169
+ credential that ends in shell access.
146
170
 
147
171
  A paired device then has the same powers as the person at the keyboard, with one exception: it
148
172
  cannot mint pairing codes, approve devices, or change these settings. A device able to approve
@@ -160,18 +184,24 @@ setting in force at the moment of the check rather than stamped onto a session w
160
184
  Turning expiry on therefore drops sessions that are already idle, and turning it off brings them
161
185
  back.
162
186
 
163
- With both off, the only expiry is you: a tunnel stays up until it is closed or the hub restarts, and
164
- a paired device keeps its session until it is revoked or remote access is switched off. Switching
165
- remote access off always revokes every paired session, and that is not a toggle.
187
+ With both off, a paired device keeps its session until it is revoked, remote access is switched off, or
188
+ the hub restarts. Switching remote access off always revokes every paired session, and that is not a
189
+ toggle.
166
190
 
167
191
  Before reporting success, the tunnel is **self-tested through its own public URL**: the pairing page
168
192
  must answer 200 and `/api/health` must answer 401. Anything else means the agent is on the public
169
193
  internet unguarded, so the tunnel is killed immediately rather than warned about.
170
194
 
171
- Requires `cloudflared` on `PATH` (`brew install cloudflared`). A quick tunnel needs no account but
172
- gets a new hostname on every start, so every restart means scanning again, and Cloudflare does not
173
- support SSE on quick tunnels, which breaks the workflow and runner-log plugin surfaces. A named
174
- tunnel on your own domain has neither problem and is the mode to use if you do this daily.
195
+ Requires `cloudflared` on `PATH` (`brew install cloudflared`). A quick tunnel needs no account but gets a new
196
+ hostname on every start. It does not provide a durable PWA, passkey, or Push identity, and Cloudflare does not
197
+ support SSE on quick tunnels, which breaks workflow and runner-log plugin surfaces. Use a stable named tunnel
198
+ for the Home Screen app.
199
+
200
+ If a named tunnel's connector exits unexpectedly, the hub makes three backoff restart attempts and
201
+ self-tests every replacement. The existing listener and paired sessions stay valid while recovery is
202
+ in progress. A quick tunnel cannot recover transparently because its origin changes, so it still fails
203
+ closed immediately. Exhausting named-tunnel recovery also switches remote access off and revokes the
204
+ paired sessions.
175
205
 
176
206
  ### Passkeys and step-up
177
207
 
@@ -187,18 +217,20 @@ so rather than letting the ceremony fail in the browser.
187
217
 
188
218
  ### Tunnel-provider trust and payload privacy
189
219
 
190
- Cloudflare is a trusted delivery boundary. It terminates TLS and delivers both the pairing page and the
191
- cockpit SPA. A browser cannot independently verify its first executable page against the same edge that
192
- served it: a malicious replacement could omit any in-page verifier, read the QR fragment, and control a
193
- paired agent. DoomPi therefore does not claim that browser-side bundle signing protects against a
194
- compromised Cloudflare edge.
220
+ Cloudflare terminates TLS and delivers the package-owned pairing and verifier bootstrap. A browser cannot
221
+ independently authenticate that first executable shell against the same edge that served it: a malicious
222
+ replacement could omit the worker and steal a QR fragment. DoomPi does not claim protection from a malicious
223
+ code-delivery edge.
224
+
225
+ After that bootstrap, **bundle signing is mandatory**. The QR pins the host ECDSA key and revision floor. The
226
+ worker verifies the signed manifest plus every raw asset before any host JavaScript executes, serves only the
227
+ atomically committed verified cache, and retains the last-known-good revision on failure. Cloudflare may
228
+ alter the raw bytes, but altered bytes do not execute.
195
229
 
196
- Within that trust model, **the payload is sealed**. A QR carries an ephemeral P-256 public key alongside the
197
- pairing code, in the URL fragment, which no browser sends to any server. A returning device that proves a
198
- passkey receives the current public key in the successful authentication response instead. Both paths then
199
- complete ECDH and derive AES-256-GCM keys, separate per direction. Socket frames and API bodies travel as
200
- ciphertext. This limits routine relay visibility, but it still depends on Cloudflare delivering the intended
201
- pairing page and SPA.
230
+ **The payload is also sealed.** A QR carries an ephemeral P-256 public key in the fragment. A returning
231
+ passkey device restores its channel only after the pinned bundle worker accepts the same signer. Both paths
232
+ complete ECDH and derive separate AES-256-GCM keys per direction. Socket frames and API bodies travel as
233
+ ciphertext.
202
234
 
203
235
  The channel implementation lives in `@agimon-ai/doompi-web-security`, which ships the envelope contract,
204
236
  a `node:crypto` half, and a WebCrypto half. **A web plugin that calls `fetch` directly sends plaintext to
@@ -340,14 +372,17 @@ package declares a `doompiWeb` block in its package.json naming a client entry (
340
372
  definition: tab, panel, badge, session data channels, tool renderers for the timeline cards of
341
373
  the tools the package registers, slots it opens for other plugins, fills into slots others open,
342
374
  and the host's own slot contributions) and an optional hub entry (`webHubChannels` plus its built
343
- dist). This package's own bundle carries no plugins; the full set is assembled on your machine:
344
- `doompi sync` discovers the installed composition's manifests, generates the import entries, and
345
- rebuilds the SPA through the `./bundler` subpath into `~/.doompi/web/current`, which the server
346
- prefers over the packaged bundle (`--assets` and `DOOMPI_WEB_DIST` override it). The synced bundle
347
- carries `webPlugins.server.json` naming each plugin's built hub entry, imported lazily: a missing
348
- plugin package logs a notice and its tab shows an empty state. The subagents tab in
349
- `@agimon-ai/doompi-team` is the reference plugin; the workflows tab ships with
350
- `@agimon-ai/doompi-workflow`. Contracts come from `@agimon-ai/doompi-web-contracts`.
375
+ dist). This package's own bundle carries no plugins; the full set is assembled on your machine.
376
+ `doompi sync` discovers the installed composition's manifests, generates the import entries, and builds
377
+ the SPA through the `./bundler` subpath inside the repository/worktree's immutable sync generation.
378
+ The shared server uses the validated registration for its startup repository. `--assets` and
379
+ `DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
380
+ `webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served
381
+ bundle. It names each plugin's built hub entry, imported lazily. A missing plugin package logs a notice
382
+ and its tab is omitted. Session package API routes use the session `cwd` registration, while
383
+ `DOOMPI_API_DIR` remains the explicit API override. `@agimon-ai/doompi-team` is the reference plugin;
384
+ the workflows tab ships with `@agimon-ai/doompi-workflow`. Contracts come from
385
+ `@agimon-ai/doompi-web-contracts`.
351
386
 
352
387
  A tool call's timeline item belongs to the plugin that registers the tool: its `toolRenderers`
353
388
  entry names the tool (or claims it with `matches` when the name is only known at runtime) and
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/hubProbe.cjs"),c=require("../src/adapters/sessionRelaunch.cjs");let l=require("node:os");l=e.__toESM(l,1);function u(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function d(){let e=i.parseServeOptions(process.argv.slice(2)),o=`http://${e.host}:${String(e.port)}`,c=e.stateDir??n.defaultRemoteStateDir(),d=a.createCockpitContainer({stateDir:c,onNotice:u});if(await d.reapStale(),i.isLoopbackHost(e.host)||(u(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),u(`WARNING: turn on remote access in settings instead, which requires a paired device.`)),await s.hubAnswers(e.host,e.port)){u(`cockpit already running at ${o}`);return}let p=async()=>{},m=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:l.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:c,cloudflaredPath:e.cloudflaredPath,onNotice:u,onHandover:t=>{f({serving:n,restart:m,container:d,handover:t,port:e.port,host:e.host,adopt:e=>{p=e}})}});return p=async()=>{await n.close()},n};try{await m()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await s.hubAnswers(e.host,e.port)){u(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let h=()=>{p().then(()=>{process.exit(0)})};process.on(`SIGINT`,h),process.on(`SIGTERM`,h)}async function f(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:u});if(!t.ok){u(`the cockpit could not move into a container: ${t.error}`),u(`serving from the host again`),await e.restart(),await c.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:u});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:u}),await c.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:u}),u(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}d().catch(e=>{u(e instanceof Error?e.message:String(e)),process.exitCode=1});
2
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/doomInitialization.cjs"),c=require("../src/adapters/hubProbe.cjs"),l=require("../src/adapters/sessionRelaunch.cjs");let u=require("node:os");u=e.__toESM(u,1);function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await c.hubAnswers(t,n))return!0;return!1}async function p(){let e=i.parseServeOptions(process.argv.slice(2));if(e.help){process.stdout.write(i.serveHelp());return}if(e.version){process.stdout.write(`${r.packagedVersion()}\n`);return}await s.ensureDoomInitialized({homeDirectory:u.default.homedir(),onNotice:d});let o=`http://${e.host}:${String(e.port)}`,l=e.stateDir??n.defaultRemoteStateDir(),p=a.createCockpitContainer({stateDir:l,onNotice:d});await p.reapStale(),i.isLoopbackHost(e.host)||(d(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let h=await c.probeHub(e.host,e.port);if(h!==void 0){let t=r.packagedVersion(),n=h.version??`an older build`;if(h.version===t){d(`cockpit already running at ${o}`);return}if(h.sessions>0){d(`WARNING: ${n} is already running at ${o} with ${String(h.sessions)} live session(s).`),d(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(h.pid===void 0||!i.isLoopbackHost(e.host)){d(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),d(`WARNING: stop it and run again to serve ${t}.`);return}if(d(`replacing ${n} at ${o} with ${t}`),!await f(h.pid,e.host,e.port)){d(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let g=async()=>{},_=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,compositionDir:e.directory,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:u.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:l,cloudflaredPath:e.cloudflaredPath,onNotice:d,onHandover:t=>{m({serving:n,restart:_,container:p,handover:t,port:e.port,host:e.host,adopt:e=>{g=e}})}});return g=async()=>{await n.close()},n};try{await _()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await c.hubAnswers(e.host,e.port)){d(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let v=()=>{g().then(()=>{process.exit(0)})};process.on(`SIGINT`,v),process.on(`SIGTERM`,v)}async function m(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:d}),await l.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}p().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});
3
3
  //# sourceMappingURL=serve.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.cjs","names":["parseServeOptions","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","hubAnswers","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","os","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{serveWeb as r}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as i,parseServeOptions as a}from"../src/services/serveOptions.mjs";import{createCockpitContainer as o}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as s}from"../src/adapters/cockpitHandoff.mjs";import{hubAnswers as c}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as l}from"../src/adapters/sessionRelaunch.mjs";import u from"node:os";function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(){let s=a(process.argv.slice(2)),l=`http://${s.host}:${String(s.port)}`,f=s.stateDir??n(),m=o({stateDir:f,onNotice:d});if(await m.reapStale(),i(s.host)||(d(`WARNING: --host ${s.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`)),await c(s.host,s.port)){d(`cockpit already running at ${l}`);return}let h=async()=>{},g=async()=>{let n=await r({port:s.port,host:s.host,assetsDir:s.assetsDir,registryDir:t({flagValue:s.registryDir,envValue:process.env[e],homeDir:u.homedir()}),spawnCommand:s.spawnCommand,remoteStateDir:f,cloudflaredPath:s.cloudflaredPath,onNotice:d,onHandover:e=>{p({serving:n,restart:g,container:m,handover:e,port:s.port,host:s.host,adopt:e=>{h=e}})}});return h=async()=>{await n.close()},n};try{await g()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await c(s.host,s.port)){d(`cockpit already running at ${l}`);return}throw Error(`Port ${String(s.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let _=()=>{h().then(()=>{process.exit(0)})};process.on(`SIGINT`,_),process.on(`SIGTERM`,_)}async function p(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await s({port:e.port,settings:e.handover.settings,onNotice:d}),await l({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}f().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
2
+ import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o,serveHelp as s}from"../src/services/serveOptions.mjs";import{createCockpitContainer as c}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as l}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as u}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as d,probeHub as f}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as p}from"../src/adapters/sessionRelaunch.mjs";import m from"node:os";function h(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function g(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await d(t,n))return!0;return!1}async function _(){let l=o(process.argv.slice(2));if(l.help){process.stdout.write(s());return}if(l.version){process.stdout.write(`${r()}\n`);return}await u({homeDirectory:m.homedir(),onNotice:h});let p=`http://${l.host}:${String(l.port)}`,_=l.stateDir??n(),y=c({stateDir:_,onNotice:h});await y.reapStale(),a(l.host)||(h(`WARNING: --host ${l.host} is not loopback, so the cockpit is reachable without pairing.`),h(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let b=await f(l.host,l.port);if(b!==void 0){let e=r(),t=b.version??`an older build`;if(b.version===e){h(`cockpit already running at ${p}`);return}if(b.sessions>0){h(`WARNING: ${t} is already running at ${p} with ${String(b.sessions)} live session(s).`),h(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(b.pid===void 0||!a(l.host)){h(`WARNING: ${t} is already running at ${p} and cannot be replaced automatically.`),h(`WARNING: stop it and run again to serve ${e}.`);return}if(h(`replacing ${t} at ${p} with ${e}`),!await g(b.pid,l.host,l.port)){h(`WARNING: the hub at ${p} did not stop; serve ${e} on another port with --port.`);return}}let x=async()=>{},S=async()=>{let n=await i({port:l.port,host:l.host,assetsDir:l.assetsDir,compositionDir:l.directory,registryDir:t({flagValue:l.registryDir,envValue:process.env[e],homeDir:m.homedir()}),spawnCommand:l.spawnCommand,remoteStateDir:_,cloudflaredPath:l.cloudflaredPath,onNotice:h,onHandover:e=>{v({serving:n,restart:S,container:y,handover:e,port:l.port,host:l.host,adopt:e=>{x=e}})}});return x=async()=>{await n.close()},n};try{await S()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await d(l.host,l.port)){h(`cockpit already running at ${p}`);return}throw Error(`Port ${String(l.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let C=()=>{x().then(()=>{process.exit(0)})};process.on(`SIGINT`,C),process.on(`SIGTERM`,C)}async function v(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:h});if(!t.ok){h(`the cockpit could not move into a container: ${t.error}`),h(`serving from the host again`),await e.restart(),await p({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:h});return}e.adopt(async()=>{await t.container.stop()}),await l({port:e.port,settings:e.handover.settings,onNotice:h}),await p({port:e.port,sessions:e.handover.sessions,onNotice:h}),h(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}_().catch(e=>{h(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
3
3
  //# sourceMappingURL=serve.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";6mBAgBA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAU,EAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IACnD,EAAW,EAAQ,UAAY,EAAsB,EACrD,EAAY,EAAuB,CAAE,WAAU,SAAU,CAAO,CAAC,EAgBvE,GAZA,MAAM,EAAU,UAAU,EAKrB,EAAe,EAAQ,IAAI,IAC9B,EAAO,mBAAmB,EAAQ,KAAK,+DAA+D,EACtG,EAAO,qFAAqF,GAK1F,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CASA,IAAI,EAAgC,SAAY,CAAC,EAE3C,EAAQ,SAAgC,CAC5C,IAAM,EAAS,MAAM,EAAS,CAC5B,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAa,EAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAI,GACtB,QAAS,EAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,eAAgB,EAChB,gBAAiB,EAAQ,gBACzB,SAAU,EAGV,WAAa,GAAa,CACxB,EAAc,CACZ,QAAS,EACT,QAAS,EACT,YACA,WACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,MAAQ,GAAS,CACf,EAAW,CACb,CACF,CAAC,CACH,CACF,CAAC,EAID,MAHA,GAAW,SAAY,CACrB,MAAM,EAAO,MAAM,CACrB,EACO,CACT,EAEA,GAAI,CACF,MAAM,EAAM,CACd,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAc,CAAC,CAAC,SAAW,CACzB,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAWA,eAAe,EAAS,EAQN,CAChB,MAAM,EAAM,QAAQ,MAAM,EAC1B,IAAM,EAAU,MAAM,EAAM,UAAU,MAAM,CAC1C,WAAY,EAAM,SAAS,SAAS,QAAQ,WAAW,IAAK,IAAe,CAAE,KAAM,CAAU,EAAE,EAC/F,KAAM,EAAM,KACZ,WAAY,CACd,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,EAAO,gDAAgD,EAAQ,OAAO,EACtE,EAAO,6BAA6B,EACpC,MAAM,EAAM,QAAQ,EACpB,MAAM,EAAiB,CACrB,KAAM,EAAM,KACZ,KAAM,EAAM,KACZ,SAAU,EAAM,SAAS,SACzB,SAAU,CACZ,CAAC,EACD,MACF,CACA,EAAM,MAAM,SAAY,CACtB,MAAM,EAAQ,UAAU,KAAK,CAC/B,CAAC,EAGD,MAAM,EAAoB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EACnG,MAAM,EAAiB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EAGhG,EAAO,oDAAoD,EAC3D,MAAM,EAAQ,UAAU,UAAU,CACpC,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.cjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.cjs";
3
- import { serveWeb } from "./src/adapters/httpServer.cjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.cjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.cjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.cjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.mjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.mjs";
3
- import { serveWeb } from "./src/adapters/httpServer.mjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.mjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.mjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.mjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
Binary file
Binary file
@@ -0,0 +1,104 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="theme-color" content="#14161a" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
+ <meta name="robots" content="noindex, nofollow" />
10
+ <link rel="manifest" href="/pwa/manifest.webmanifest" />
11
+ <link rel="apple-touch-icon" href="/pwa/icon-192.png" />
12
+ <title>Pair with DoomPi</title>
13
+ <style>
14
+ :root {
15
+ color-scheme: dark;
16
+ }
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+ body {
21
+ margin: 0;
22
+ min-height: 100vh;
23
+ min-height: 100dvh;
24
+ display: grid;
25
+ place-items: center;
26
+ padding: max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right))
27
+ max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));
28
+ background: #14161a;
29
+ color: #e6e8ec;
30
+ font:
31
+ 16px/1.5 ui-sans-serif,
32
+ system-ui,
33
+ -apple-system,
34
+ sans-serif;
35
+ }
36
+ main {
37
+ width: min(32rem, 100%);
38
+ text-align: center;
39
+ }
40
+ h1 {
41
+ margin: 0 0 0.5rem;
42
+ font-size: 1.35rem;
43
+ }
44
+ p {
45
+ margin: 0 0 1rem;
46
+ color: #a2a8b4;
47
+ }
48
+ #pwa-status[data-tone='good'] {
49
+ color: #6bd68a;
50
+ }
51
+ #pwa-status[data-tone='bad'] {
52
+ color: #ff7b7b;
53
+ }
54
+ video {
55
+ width: 100%;
56
+ max-height: 24rem;
57
+ margin: 0.75rem 0;
58
+ border: 1px solid #2c313a;
59
+ border-radius: 0.75rem;
60
+ background: #090a0c;
61
+ object-fit: cover;
62
+ }
63
+ .actions {
64
+ display: flex;
65
+ gap: 0.75rem;
66
+ margin-top: 1.25rem;
67
+ }
68
+ button {
69
+ flex: 1;
70
+ min-height: 3rem;
71
+ border: 1px solid #2f6feb;
72
+ border-radius: 0.6rem;
73
+ background: #2f6feb;
74
+ color: #fff;
75
+ font: inherit;
76
+ font-weight: 650;
77
+ }
78
+ button.secondary {
79
+ border-color: #2c313a;
80
+ background: #1b1e24;
81
+ }
82
+ button:disabled {
83
+ opacity: 0.55;
84
+ }
85
+ [hidden] {
86
+ display: none !important;
87
+ }
88
+ </style>
89
+ <script type="module" crossorigin src="/pwa/pwa.js"></script>
90
+ </head>
91
+ <body data-marker="doompi-pwa-shell">
92
+ <main>
93
+ <h1>Pair this device</h1>
94
+ <p>This cockpit can run shell commands on the host. Pair only with your own DoomPi.</p>
95
+ <p id="pwa-status" role="status">Checking browser support.</p>
96
+ <video id="camera" playsinline muted hidden></video>
97
+ <div class="actions">
98
+ <button id="scan" type="button">Scan pairing QR</button>
99
+ <button id="cancel" class="secondary" type="button" hidden>Cancel</button>
100
+ </div>
101
+ <noscript><p>Pairing needs JavaScript.</p></noscript>
102
+ </main>
103
+ </body>
104
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "/pair",
3
+ "name": "DoomPi Cockpit",
4
+ "short_name": "DoomPi",
5
+ "description": "Securely pair with and open your DoomPi cockpit.",
6
+ "start_url": "/",
7
+ "scope": "/",
8
+ "display": "standalone",
9
+ "background_color": "#14161a",
10
+ "theme_color": "#14161a",
11
+ "icons": [
12
+ {
13
+ "src": "/pwa/icon-192.png",
14
+ "sizes": "192x192",
15
+ "type": "image/png"
16
+ },
17
+ {
18
+ "src": "/pwa/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png"
21
+ }
22
+ ]
23
+ }