@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,181 @@
1
+ import { spawn } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { sessionRecordPath } from '../services/registryStore.ts';
6
+ import { type BundledServerLaunch, defaultServerLaunch } from './bundledServer.ts';
7
+
8
+ /** Names the server in messages; the resolved command is a node path nobody would recognise. */
9
+ const SERVER_LABEL = 'doompi-server';
10
+ const SPAWNED_SEGMENT = 'spawned';
11
+ const SOCKET_FILE = 's.sock';
12
+ const TOKEN_FILE = 'token';
13
+ const LOG_FILE = 'server.log';
14
+ const DIR_NAME_PREFIX_LENGTH = 8;
15
+ const OWNER_ONLY_FILE = 0o600;
16
+ const OWNER_ONLY_DIR = 0o700;
17
+ const TOKEN_BYTES = 32;
18
+ // macOS caps sun_path around 104 bytes; failing here beats a cryptic bind error.
19
+ const MAX_SOCKET_PATH = 103;
20
+ const RECORD_WAIT_MS = 10_000;
21
+ const RECORD_POLL_MS = 100;
22
+ const LOG_TAIL_CHARS = 400;
23
+
24
+ export type SpawnOutcome =
25
+ | { ok: true; sessionId: string }
26
+ | { ok: false; code: 'invalid_request' | 'spawn_failed'; error: string };
27
+
28
+ export interface SpawnSessionInput {
29
+ cwd: string;
30
+ name?: string;
31
+ }
32
+
33
+ /** Launches doompi-server processes for sessions created from the page. */
34
+ export interface SessionSpawner {
35
+ spawn(input: SpawnSessionInput): Promise<SpawnOutcome>;
36
+ }
37
+
38
+ export interface ServerSpawnerOptions {
39
+ registryDir: string;
40
+ /** Overridable so tests can stand in a fake server; defaults to the installation's own doompi-server. */
41
+ command?: string;
42
+ onNotice?: (message: string) => void;
43
+ }
44
+
45
+ function logTail(logPath: string): string {
46
+ try {
47
+ return fs.readFileSync(logPath, 'utf8').slice(-LOG_TAIL_CHARS).trim();
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Creates sessions the way a terminal would: by starting a doompi-server.
55
+ *
56
+ * The spawned server is detached and owns its own lifetime; it registers
57
+ * itself like any other, which is why success is defined as "its record
58
+ * appeared", not as anything about the child process. The hub stopping later
59
+ * does not take created sessions down with it.
60
+ *
61
+ * Without an explicit command the server comes from this installation rather
62
+ * than from PATH, so the cockpit needs no flag to launch the build it belongs
63
+ * to. The resolution happens per session because it reads the session's own
64
+ * working directory.
65
+ */
66
+ export function createServerSpawner(options: ServerSpawnerOptions): SessionSpawner {
67
+ return {
68
+ spawn(input) {
69
+ let stats: fs.Stats;
70
+ try {
71
+ stats = fs.statSync(input.cwd);
72
+ } catch {
73
+ return Promise.resolve({ ok: false, code: 'invalid_request', error: `No such directory: ${input.cwd}` });
74
+ }
75
+ if (!path.isAbsolute(input.cwd) || !stats.isDirectory()) {
76
+ return Promise.resolve({
77
+ ok: false,
78
+ code: 'invalid_request',
79
+ error: `The working directory must be an absolute path to a directory, received "${input.cwd}".`,
80
+ });
81
+ }
82
+
83
+ // Resolved before anything is written, so a broken installation fails
84
+ // without leaving a session directory nobody will ever use.
85
+ let launch: BundledServerLaunch;
86
+ try {
87
+ launch = options.command
88
+ ? { command: options.command, args: [], environment: process.env }
89
+ : defaultServerLaunch(input.cwd, process.env);
90
+ } catch (error) {
91
+ return Promise.resolve({
92
+ ok: false,
93
+ code: 'spawn_failed',
94
+ error: `Could not locate ${SERVER_LABEL}: ${error instanceof Error ? error.message : String(error)}`,
95
+ });
96
+ }
97
+ const command = options.command ?? SERVER_LABEL;
98
+
99
+ const sessionId = crypto.randomUUID();
100
+ // The directory borrows only a prefix of the id: sun_path is capped
101
+ // around 104 bytes and every byte of the session dir counts against it.
102
+ const spawnedRoot = path.join(options.registryDir, SPAWNED_SEGMENT);
103
+ let dirName = sessionId.slice(0, DIR_NAME_PREFIX_LENGTH);
104
+ while (fs.existsSync(path.join(spawnedRoot, dirName)) && dirName.length < sessionId.length) {
105
+ dirName = sessionId.slice(0, dirName.length + DIR_NAME_PREFIX_LENGTH);
106
+ }
107
+ const sessionDir = path.join(spawnedRoot, dirName);
108
+ const socketPath = path.join(sessionDir, SOCKET_FILE);
109
+ if (socketPath.length > MAX_SOCKET_PATH) {
110
+ return Promise.resolve({
111
+ ok: false,
112
+ code: 'spawn_failed',
113
+ error: `The socket path would exceed the unix limit (${socketPath.length} > ${MAX_SOCKET_PATH}); use a shorter --registry-dir.`,
114
+ });
115
+ }
116
+ const tokenFile = path.join(sessionDir, TOKEN_FILE);
117
+ const logPath = path.join(sessionDir, LOG_FILE);
118
+ fs.mkdirSync(sessionDir, { recursive: true, mode: OWNER_ONLY_DIR });
119
+ fs.writeFileSync(tokenFile, crypto.randomBytes(TOKEN_BYTES).toString('hex'), { mode: OWNER_ONLY_FILE });
120
+
121
+ const log = fs.openSync(logPath, 'a', OWNER_ONLY_FILE);
122
+ const args = [
123
+ '--listen',
124
+ socketPath,
125
+ '--auth-token-file',
126
+ tokenFile,
127
+ '--session-id',
128
+ sessionId,
129
+ '--name',
130
+ input.name ?? 'untitled',
131
+ '--registry-dir',
132
+ options.registryDir,
133
+ ];
134
+ const child = spawn(launch.command, [...launch.args, ...args], {
135
+ cwd: input.cwd,
136
+ detached: true,
137
+ stdio: ['ignore', log, log],
138
+ env: launch.environment,
139
+ });
140
+ fs.closeSync(log);
141
+
142
+ return new Promise((resolve) => {
143
+ let settled = false;
144
+ const finish = (outcome: SpawnOutcome): void => {
145
+ if (settled) return;
146
+ settled = true;
147
+ clearInterval(pollTimer);
148
+ clearTimeout(deadline);
149
+ if (outcome.ok) options.onNotice?.(`created session ${sessionId} in ${input.cwd}`);
150
+ resolve(outcome);
151
+ };
152
+
153
+ child.once('error', (error) =>
154
+ finish({ ok: false, code: 'spawn_failed', error: `Could not start ${command}: ${error.message}` }),
155
+ );
156
+ child.once('exit', (code) => {
157
+ const tail = logTail(logPath);
158
+ finish({
159
+ ok: false,
160
+ code: 'spawn_failed',
161
+ error: `${command} exited with code ${code ?? 'unknown'} before registering.${tail ? ` Log: ${tail}` : ''}`,
162
+ });
163
+ });
164
+ child.unref();
165
+
166
+ const recordFile = sessionRecordPath(options.registryDir, sessionId);
167
+ const pollTimer = setInterval(() => {
168
+ if (fs.existsSync(recordFile)) finish({ ok: true, sessionId });
169
+ }, RECORD_POLL_MS);
170
+ const deadline = setTimeout(() => {
171
+ const tail = logTail(logPath);
172
+ finish({
173
+ ok: false,
174
+ code: 'spawn_failed',
175
+ error: `${command} did not register within ${RECORD_WAIT_MS / 1000}s.${tail ? ` Log: ${tail}` : ''}`,
176
+ });
177
+ }, RECORD_WAIT_MS);
178
+ });
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,99 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { rankFileMatches } from '../services/fileMatch.ts';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ const GIT_TIMEOUT_MS = 2000;
10
+ const GIT_MAX_BUFFER = 8 * 1024 * 1024;
11
+ /** The walk fallback stops collecting past this many entries. */
12
+ const WALK_LIMIT = 4000;
13
+ const WALK_DEPTH = 6;
14
+ const SKIPPED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.pnpm']);
15
+
16
+ async function gitFiles(cwd: string): Promise<string[] | undefined> {
17
+ try {
18
+ // Tracked plus untracked-but-not-ignored: what a person means by "the files".
19
+ const { stdout } = await execFileAsync('git', ['ls-files', '-co', '--exclude-standard'], {
20
+ cwd,
21
+ timeout: GIT_TIMEOUT_MS,
22
+ maxBuffer: GIT_MAX_BUFFER,
23
+ });
24
+ const files = stdout.split('\n').filter((line) => line !== '');
25
+ return files.length > 0 ? files : undefined;
26
+ } catch {
27
+ return undefined; // Not a repository, or git is unavailable; the walk covers it.
28
+ }
29
+ }
30
+
31
+ function walkFiles(cwd: string): string[] {
32
+ const files: string[] = [];
33
+ const visit = (dir: string, depth: number): void => {
34
+ if (files.length >= WALK_LIMIT || depth > WALK_DEPTH) return;
35
+ let entries: fs.Dirent[];
36
+ try {
37
+ entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ } catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ if (files.length >= WALK_LIMIT) return;
43
+ if (entry.name.startsWith('.') && entry.name !== '.doom') continue;
44
+ const full = path.join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) visit(full, depth + 1);
47
+ } else if (entry.isFile()) {
48
+ files.push(path.relative(cwd, full));
49
+ }
50
+ }
51
+ };
52
+ visit(cwd, 0);
53
+ return files;
54
+ }
55
+
56
+ /**
57
+ * Repository-relative files under a session's working directory matching a
58
+ * query, for the composer's @ completion. Git's index is the source when
59
+ * available (it already understands ignores); a bounded walk covers plain
60
+ * directories.
61
+ */
62
+ export async function listSessionFiles(cwd: string, query: string, limit: number): Promise<string[]> {
63
+ const files = (await gitFiles(cwd)) ?? walkFiles(cwd);
64
+ return rankFileMatches(files, query, limit);
65
+ }
66
+
67
+ export type SessionFileResult =
68
+ | { status: 'ok'; body: Buffer }
69
+ | { status: 'not-found' }
70
+ | { status: 'forbidden' }
71
+ | { status: 'too-large' };
72
+
73
+ function isInside(root: string, candidate: string): boolean {
74
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
75
+ }
76
+
77
+ /**
78
+ * One file under a session's working directory, for the timeline's mention
79
+ * previews. Containment is checked on the real path as well as the lexical
80
+ * one, so a symlink inside the tree cannot hand out something beyond it.
81
+ */
82
+ export async function readSessionFile(cwd: string, relativePath: string, maxBytes: number): Promise<SessionFileResult> {
83
+ if (!relativePath || relativePath.includes('\0') || path.isAbsolute(relativePath)) return { status: 'forbidden' };
84
+ const root = path.resolve(cwd);
85
+ const candidate = path.resolve(root, relativePath);
86
+ if (!isInside(root, candidate)) return { status: 'forbidden' };
87
+ let real: string;
88
+ let stats: fs.Stats;
89
+ try {
90
+ real = await fs.promises.realpath(candidate);
91
+ stats = await fs.promises.stat(real);
92
+ } catch {
93
+ return { status: 'not-found' };
94
+ }
95
+ if (!isInside(await fs.promises.realpath(root), real)) return { status: 'forbidden' };
96
+ if (!stats.isFile()) return { status: 'not-found' };
97
+ if (stats.size > maxBytes) return { status: 'too-large' };
98
+ return { status: 'ok', body: await fs.promises.readFile(real) };
99
+ }