@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.
- package/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.cjs","names":["path","fileURLToPath","fs","os","createSessionHub","watchRegistry","createServerSpawner","readGitStatus","DOOM_API_ROUTE_PREFIX","API_SESSION_QUERY_PARAM","proxyToSocket","loadHubChannels","createThreadJournals","createSyncGuard","Hono","createNodeWebSocket","createPiWebSocketListener","PiServer","createPiHubService","createProviderAuth","loadPackageApis","SESSIONS_API_ROUTE","listSessionFiles","DIRECTORIES_API_ROUTE","suggestDirectories","SESSION_FILE_ROUTE","readSessionFile","MAX_SESSION_FILE_BYTES","sessionFileHeaders","hubHello","SESSIONS_SNAPSHOT_TYPE","SESSION_UPSERT_TYPE","SESSION_REMOVED_TYPE","sessionFrameEnvelope","threadFrameEnvelope","threadBacklog","resolveAssetPath","contentTypeFor","HUB_RESYNCED_TYPE","serve"],"sources":["../../../src/adapters/httpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { PiServer } from '@earendil-works/pi-server';\nimport { createPiHubService } from './piHubService.ts';\nimport { createSyncGuard } from './syncGuard.ts';\nimport { createPiWebSocketListener } from './piWebSocketListener.ts';\nimport { Hono } from 'hono';\nimport type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport {\n DOOM_API_ROUTE_PREFIX,\n type DoomApi,\n type DoomApiHandler,\n} from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { loadPackageApis } from '@agimon-ai/doompi-extension-contracts/package-api-loader';\nimport { sessionFileHeaders } from '../services/fileMedia.ts';\nimport { contentTypeFor, resolveAssetPath } from '../services/staticAssets.ts';\nimport { MAX_SESSION_FILE_BYTES, SESSION_FILE_ROUTE } from '../types/media.ts';\nimport type { WebServer, WebServerOptions } from '../types/bridge.ts';\nimport {\n API_SESSION_QUERY_PARAM,\n DIRECTORIES_API_ROUTE,\n HISTORY_REQUEST_TYPE,\n HUB_PROTOCOL_VERSION,\n HUB_RESYNCED_TYPE,\n HUB_ROLE,\n SESSIONS_API_ROUTE,\n SESSIONS_SNAPSHOT_TYPE,\n SESSION_COMMAND_TYPE,\n SESSION_REMOVED_TYPE,\n SESSION_UPSERT_TYPE,\n SUBSCRIBE_THREAD_TYPE,\n SUBSCRIBE_TYPE,\n UNSUBSCRIBE_THREAD_TYPE,\n UNSUBSCRIBE_TYPE,\n hubHello,\n sessionFrameEnvelope,\n threadBacklog,\n threadFrameEnvelope,\n} from '../types/hub.ts';\nimport { ATTACH_TYPE, type SessionFrame } from '../types/session.ts';\nimport { readGitStatus } from './gitStatus.ts';\nimport { watchRegistry } from './registryWatcher.ts';\nimport { createServerSpawner } from './serverSpawner.ts';\nimport { createSessionHub, type SessionHub } from './sessionHub.ts';\nimport { registerAuthRoutes } from './authRoutes.ts';\nimport { createProviderAuth } from './providerAuth.ts';\nimport { suggestDirectories } from './directoryListing.ts';\nimport { listSessionFiles, readSessionFile } from './sessionFiles.ts';\nimport { proxyToSocket } from './packageApiProxy.ts';\nimport { createThreadJournals } from './threadJournals.ts';\nimport { loadHubChannels } from './webHubPluginLoader.ts';\n\nconst SESSION_ROUTE = '/api/session';\n// Pi's protocol rides its own socket so the DoomPi channel keeps the\n// vocabulary the protocol has no shape for: dialogs, minor modes, selection.\nconst PROTOCOL_ROUTE = '/api/pi';\n/** Directory suggestions per picker query; more than this means \"type further\". */\nconst DIRECTORY_SUGGESTION_LIMIT = 12;\nconst INDEX_FILE = 'index.html';\n/** Env override for the assets directory, set by launchers that know a synced bundle. */\nconst WEB_DIST_ENV = 'DOOMPI_WEB_DIST';\n/** Where `doompi sync` publishes the machine's cockpit bundle. */\nconst SYNCED_WEB_DIRECTORY = ['.doompi', 'web', 'current', 'web'];\n\n/**\n * Locates the built SPA next to the package that owns this module.\n *\n * Walking to the manifest rather than counting `..` segments keeps this correct\n * whether the code runs from `src` in development or from the unbundled `dist`\n * tree, whose depth is a build detail.\n */\nfunction packagedAssetsDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return path.join(dir, 'dist', 'web');\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('doompi-web could not locate its own package root.');\n dir = parent;\n }\n}\n\n/**\n * The assets to serve, in preference order: an explicit option, the env\n * override, the bundle `doompi sync` published for this machine (which\n * carries the installed plugin set), then the package's own prebuilt bundle\n * (built-in plugins only).\n */\nfunction resolveAssetsDir(explicit: string | undefined, notice: (message: string) => void): string {\n if (explicit !== undefined) return explicit;\n const fromEnv = process.env[WEB_DIST_ENV];\n if (fromEnv !== undefined && fromEnv !== '') return fromEnv;\n const synced = path.join(os.homedir(), ...SYNCED_WEB_DIRECTORY);\n if (fs.existsSync(path.join(synced, INDEX_FILE))) {\n notice(`serving the synced cockpit bundle from ${synced}`);\n return synced;\n }\n return packagedAssetsDir();\n}\n\nfunction readAsset(filePath: string): Buffer | undefined {\n try {\n const stat = fs.statSync(filePath);\n if (!stat.isFile()) return undefined;\n return fs.readFileSync(filePath);\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** One page's hold on one thread; a session id never contains a newline, so the pair cannot collide. */\nfunction threadKey(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\nfunction buildHub(\n options: WebServerOptions,\n notice: (message: string) => void,\n channels: readonly WebHubChannel[],\n): SessionHub {\n return createSessionHub({\n source: watchRegistry(options.registryDir, notice),\n spawner: createServerSpawner({\n registryDir: options.registryDir,\n command: options.spawnCommand,\n onNotice: notice,\n }),\n readGit: readGitStatus,\n channels,\n onNotice: notice,\n });\n}\n\n/**\n * Mounts the hub-scoped package APIs under /api/plugin/<basePath>/.\n *\n * The prefix is stripped before the API sees the request, so a package\n * declares its routes relative to its own mount and never repeats where a host\n * put it. An API that throws answers 500 for its own routes alone; one bad\n * package never takes the cockpit down with it.\n */\nfunction mountHubApis(app: Hono, apis: readonly DoomApi[], notice: (message: string) => void): DoomApiHandler[] {\n const handlers: DoomApiHandler[] = [];\n for (const api of apis) {\n const mount = `${DOOM_API_ROUTE_PREFIX}/${api.basePath}`;\n let handler: DoomApiHandler;\n try {\n handler = api.start({ scope: 'hub', onNotice: notice });\n } catch (error) {\n notice(`hub API '${api.basePath}' did not start (${describeError(error)}); its routes stay unmounted`);\n continue;\n }\n handlers.push(handler);\n app.all(`${mount}/*`, async (context) => {\n if (context.req.query(API_SESSION_QUERY_PARAM) !== undefined) return context.notFound();\n const url = new URL(context.req.url);\n url.pathname = url.pathname.slice(mount.length) || '/';\n try {\n return await handler.fetch(new Request(url, context.req.raw));\n } catch (error) {\n notice(`hub API '${api.basePath}' failed on ${url.pathname} (${describeError(error)})`);\n return context.json({ error: `The '${api.basePath}' API failed.` }, 500);\n }\n });\n }\n return handlers;\n}\n\n/**\n * Forwards a package API request carrying ?session= to that session's own\n * server, over the API socket its registry record names.\n *\n * The session servers are where session-scoped data lives, but the browser must\n * not talk to them: their attach tokens stay in this process and the page only\n * ever reaches loopback. So the hub is the one door, and this is the hop behind\n * it. A session that never mounted an API answers 404 with the reason rather\n * than leaving the page waiting.\n */\nfunction mountSessionApiProxy(app: Hono, hub: SessionHub, notice: (message: string) => void): void {\n app.all(`${DOOM_API_ROUTE_PREFIX}/*`, async (context) => {\n const sessionId = context.req.query(API_SESSION_QUERY_PARAM);\n if (sessionId === undefined) return context.notFound();\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (summary === undefined) return context.json({ error: `No session ${sessionId}.` }, 404);\n if (summary.apiSocketPath === undefined) {\n return context.json({ error: `Session ${sessionId} serves no package API.` }, 404);\n }\n const url = new URL(context.req.url);\n url.searchParams.delete(API_SESSION_QUERY_PARAM);\n try {\n return await proxyToSocket({\n socketPath: summary.apiSocketPath,\n path: `${url.pathname}${url.search}`,\n method: context.req.method,\n headers: context.req.raw.headers,\n body: context.req.raw.body,\n });\n } catch (error) {\n notice(`session ${sessionId} API is unreachable (${describeError(error)})`);\n return context.json({ error: `Session ${sessionId} is not answering.` }, 502);\n }\n });\n}\n\n/**\n * Serves the cockpit and multiplexes every session behind it.\n *\n * Attach tokens stay in this process. The browser authenticates by reaching a\n * loopback port, so a page never holds a credential that would let it talk to\n * a session directly. One page WebSocket carries all sessions: hub frames\n * describe the set, and session traffic travels enveloped by session id.\n */\nexport async function serveWeb(options: WebServerOptions): Promise<WebServer> {\n const host = options.host ?? '127.0.0.1';\n const notice = options.onNotice ?? ((): void => {});\n const assetsDir = resolveAssetsDir(options.assetsDir, notice);\n const hub = buildHub(options, notice, await loadHubChannels(assetsDir, notice));\n // Threads are journals the data channels can name (a subagent run's own\n // session file); the hub tails one only while a page follows it.\n const threads = createThreadJournals({\n resolve: (sessionId, threadId) => hub.threadJournal(sessionId, threadId),\n onNotice: notice,\n });\n // A session reads what sync produced, and nothing makes the person who\n // opened the cockpit run sync first. The hub keeps the repository current.\n const syncGuard = createSyncGuard({ repoRoot: process.cwd(), onNotice: notice });\n await syncGuard.ensureSynced();\n /** Every attached page, so a rebuild can tell them all to pick it up. */\n const pages = new Set<(frame: object) => void>();\n\n const app = new Hono();\n const nodeWs = createNodeWebSocket({ app });\n // Every running session already serves Pi's protocol; the hub composes them\n // into one server so a browser sees a single endpoint with many sessions.\n const protocolListener = createPiWebSocketListener({ onError: (error) => notice(`protocol: ${error.message}`) });\n const protocolServer = new PiServer(\n createPiHubService({\n records: () => hub.records(),\n spawn: (input) => hub.create(input),\n onNotice: notice,\n }),\n { listeners: [protocolListener], onError: (error) => notice(`protocol: ${error.message}`) },\n );\n await protocolServer.start();\n // Provider credentials belong to the machine, not to a session: the hub\n // keeps one Pi runtime over the shared auth.json and signs in for all.\n const providerAuth = createProviderAuth({ runtime: options.authRuntime, onNotice: notice });\n registerAuthRoutes(app, providerAuth);\n // Package APIs, mounted before the SPA fallback so their routes are reachable\n // and everything else still falls through to the bundle. Hub-scoped ones run\n // here; session-scoped ones live in each session's own server and are proxied.\n const pluginApis = mountHubApis(app, await loadPackageApis('hub', { onNotice: notice }), notice);\n mountSessionApiProxy(app, hub, notice);\n\n app.get('/api/health', (context) =>\n context.json({\n ok: true,\n role: HUB_ROLE,\n protocol: HUB_PROTOCOL_VERSION,\n sessions: hub.snapshot().length,\n pid: process.pid,\n }),\n );\n\n app.post(SESSIONS_API_ROUTE, async (context) => {\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The request body must be JSON.' }, 400);\n }\n if (!isRecord(body) || typeof body.cwd !== 'string' || body.cwd === '') {\n return context.json({ error: 'A cwd string is required.' }, 400);\n }\n const name = typeof body.name === 'string' && body.name !== '' ? body.name : undefined;\n await syncGuard.ensureSynced();\n const outcome = await hub.create({ cwd: body.cwd, name });\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 201);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 400 : 502);\n });\n\n app.delete(`${SESSIONS_API_ROUTE}/:sessionId`, (context) => {\n const sessionId = context.req.param('sessionId');\n const outcome = hub.stop(sessionId);\n if (outcome.ok) return context.json({ sessionId }, 202);\n return context.json(\n { error: outcome.error },\n outcome.code === 'unknown' ? 404 : outcome.code === 'self' ? 409 : 502,\n );\n });\n\n // File completion for the composer's @ references: bounded, cwd-scoped,\n // and only for sessions the hub actually manages.\n app.get('/api/sessions/:sessionId/files', async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const query = context.req.query('q') ?? '';\n const files = await listSessionFiles(summary.cwd, query, 20);\n return context.json({ files });\n });\n\n // Directory suggestions for the new-session picker: the children of the\n // typed parent while a path is being drilled into, and a ranked search of\n // the home directory for anything else, so a name or a path remembered from\n // another machine still finds the folder.\n app.get(DIRECTORIES_API_ROUTE, async (context) => {\n const directories = await suggestDirectories(context.req.query('q') ?? '', {\n limit: DIRECTORY_SUGGESTION_LIMIT,\n });\n return context.json({ directories });\n });\n\n // The timeline's @file previews: one cwd-contained file, capped in size.\n app.get(SESSION_FILE_ROUTE, async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const relativePath = context.req.query('path') ?? '';\n const file = await readSessionFile(summary.cwd, relativePath, MAX_SESSION_FILE_BYTES);\n if (file.status === 'forbidden') return context.json({ error: 'The path leaves the session directory.' }, 403);\n if (file.status === 'not-found') return context.json({ error: 'No such file.' }, 404);\n if (file.status === 'too-large') return context.json({ error: 'The file is too large to preview.' }, 413);\n return context.body(new Uint8Array(file.body), 200, sessionFileHeaders(relativePath));\n });\n\n app.get(\n SESSION_ROUTE,\n nodeWs.upgradeWebSocket(() => {\n const subscriptions = new Set<string>();\n /** The threads this page follows; one socket may follow several of one session. */\n const threadSubscriptions = new Map<string, { sessionId: string; threadId: string }>();\n let disconnect: (() => void) | undefined;\n let disconnectThreads: (() => void) | undefined;\n /** Held on the socket, not inside onOpen, so close can withdraw it. */\n let registered: ((frame: object) => void) | undefined;\n /** Lets go of every followed thread, or only a departed session's. */\n const releaseThreads = (sessionId?: string): void => {\n for (const [key, held] of threadSubscriptions) {\n if (sessionId !== undefined && held.sessionId !== sessionId) continue;\n threadSubscriptions.delete(key);\n threads.unsubscribe(held.sessionId, held.threadId);\n }\n };\n return {\n onOpen(_event, ws) {\n const post = (frame: SessionFrame | object): void => {\n try {\n ws.send(JSON.stringify(frame));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n };\n registered = post;\n pages.add(post);\n post(hubHello(hub.channelTypes()));\n post({ type: SESSIONS_SNAPSHOT_TYPE, sessions: hub.snapshot() });\n disconnect = hub.onEvent((event) => {\n if (event.kind === 'upsert') post({ type: SESSION_UPSERT_TYPE, session: event.session });\n else if (event.kind === 'removed') {\n subscriptions.delete(event.sessionId);\n releaseThreads(event.sessionId);\n post({ type: SESSION_REMOVED_TYPE, sessionId: event.sessionId });\n } else if (event.kind === 'channel') {\n if (subscriptions.has(event.sessionId)) {\n post({ type: event.frameType, sessionId: event.sessionId, payload: event.payload });\n }\n } else if (subscriptions.has(event.sessionId)) {\n post(sessionFrameEnvelope(event.sessionId, event.frame));\n }\n });\n disconnectThreads = threads.onFrame((event) => {\n if (threadSubscriptions.has(threadKey(event.sessionId, event.threadId))) {\n post(threadFrameEnvelope(event.sessionId, event.threadId, event.frame));\n }\n });\n notice('browser attached');\n },\n onMessage(event, ws) {\n if (typeof event.data !== 'string') return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(event.data);\n } catch {\n return;\n }\n if (!isRecord(parsed) || typeof parsed.sessionId !== 'string') return;\n const sessionId = parsed.sessionId;\n if (parsed.type === SUBSCRIBE_TYPE) {\n const backlog = hub.backlog(sessionId);\n if (!backlog) return; // Unknown session; the snapshot said otherwise.\n subscriptions.add(sessionId);\n try {\n ws.send(JSON.stringify(backlog));\n for (const frame of hub.channelFrames(sessionId)) ws.send(JSON.stringify(frame));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === UNSUBSCRIBE_TYPE) {\n subscriptions.delete(sessionId);\n return;\n }\n if (parsed.type === HISTORY_REQUEST_TYPE) {\n // Older transcript, on demand. The hub kept what the attach path\n // was too small to publish, so scrolling back reads from memory\n // rather than asking the session to re-read its journal.\n const page = hub.history(sessionId, {\n ...(typeof parsed.before === 'string' ? { before: parsed.before } : {}),\n ...(typeof parsed.limit === 'number' ? { limit: parsed.limit } : {}),\n });\n if (!page) return;\n try {\n ws.send(JSON.stringify(page));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === SUBSCRIBE_THREAD_TYPE || parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (typeof parsed.threadId !== 'string') return;\n const threadId = parsed.threadId;\n const key = threadKey(sessionId, threadId);\n if (parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (threadSubscriptions.delete(key)) threads.unsubscribe(sessionId, threadId);\n return;\n }\n if (threadSubscriptions.has(key) || hub.backlog(sessionId) === undefined) return;\n threadSubscriptions.set(key, { sessionId, threadId });\n try {\n ws.send(JSON.stringify(threadBacklog(sessionId, threadId, threads.subscribe(sessionId, threadId))));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === SESSION_COMMAND_TYPE && isRecord(parsed.frame)) {\n // The hub owns the handshake; a page must not be able to replay it.\n if (parsed.frame.type === ATTACH_TYPE) return;\n hub.command(sessionId, parsed.frame);\n }\n },\n onClose() {\n if (registered) pages.delete(registered);\n registered = undefined;\n disconnect?.();\n disconnect = undefined;\n disconnectThreads?.();\n disconnectThreads = undefined;\n subscriptions.clear();\n releaseThreads();\n notice('browser detached');\n },\n };\n }),\n );\n\n app.get(\n PROTOCOL_ROUTE,\n nodeWs.upgradeWebSocket(() => {\n let handler: ReturnType<typeof protocolListener.accept>;\n return {\n onOpen(_event, ws) {\n handler = protocolListener.accept({\n send: (data) => ws.send(data as ArrayBuffer),\n close: () => ws.close(),\n get readyState() {\n return ws.readyState;\n },\n });\n },\n onMessage(event) {\n const data = event.data;\n if (typeof data === 'string') return;\n if (data instanceof ArrayBuffer) handler?.onData(new Uint8Array(data));\n else if (ArrayBuffer.isView(data)) {\n handler?.onData(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));\n }\n },\n onClose() {\n handler?.onClose();\n handler = undefined;\n },\n onError(event) {\n handler?.onError(event instanceof Error ? event : new Error('The protocol socket failed'));\n handler = undefined;\n },\n };\n }),\n );\n\n app.get('*', (context) => {\n const requested = new URL(context.req.url).pathname;\n const direct = requested === '/' ? undefined : resolveAssetPath(assetsDir, requested);\n const body = direct ? readAsset(direct) : undefined;\n if (direct && body) {\n return context.body(new Uint8Array(body), 200, { 'Content-Type': contentTypeFor(direct) });\n }\n const index = readAsset(path.join(assetsDir, INDEX_FILE));\n if (!index) return context.text('The cockpit bundle is missing. Run the package build.', 500);\n return context.body(new Uint8Array(index), 200, { 'Content-Type': 'text/html; charset=utf-8' });\n });\n\n // A rebuilt bundle only changes on disk, so the page it replaced has to be\n // told; nothing about a loaded bundle notices that its source moved.\n syncGuard.watch(() => {\n for (const post of pages) post({ type: HUB_RESYNCED_TYPE });\n });\n\n return new Promise<WebServer>((resolve, reject) => {\n const server = serve({ fetch: app.fetch, port: options.port, hostname: host }, (info) => {\n nodeWs.injectWebSocket(server);\n const url = `http://${host}:${info.port}`;\n notice(`cockpit on ${url}`);\n resolve({\n url,\n port: info.port,\n close: () =>\n new Promise<void>((done) => {\n threads.close();\n syncGuard.close();\n void protocolServer.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n // An upgraded socket leaves the HTTP server's connection tracking,\n // so only the WebSocket server can let go of it. Without this the\n // close callback waits on a browser that has no reason to leave.\n for (const client of nodeWs.wss.clients) client.terminate();\n nodeWs.wss.close();\n (server as { closeAllConnections?: () => void }).closeAllConnections?.();\n server.close(() => done());\n }),\n });\n });\n server.once('error', (error) => {\n threads.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n reject(error);\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebServer, WebServerOptions } from "../types/bridge.cjs";
|
|
2
|
+
//#region src/adapters/httpServer.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Serves the cockpit and multiplexes every session behind it.
|
|
5
|
+
*
|
|
6
|
+
* Attach tokens stay in this process. The browser authenticates by reaching a
|
|
7
|
+
* loopback port, so a page never holds a credential that would let it talk to
|
|
8
|
+
* a session directly. One page WebSocket carries all sessions: hub frames
|
|
9
|
+
* describe the set, and session traffic travels enveloped by session id.
|
|
10
|
+
*/
|
|
11
|
+
declare function serveWeb(options: WebServerOptions): Promise<WebServer>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { serveWeb };
|
|
14
|
+
//# sourceMappingURL=httpServer.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.d.cts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;iBA+NsB,SAAS,SAAS,mBAAmB,QAAQ"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebServer, WebServerOptions } from "../types/bridge.mjs";
|
|
2
|
+
//#region src/adapters/httpServer.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Serves the cockpit and multiplexes every session behind it.
|
|
5
|
+
*
|
|
6
|
+
* Attach tokens stay in this process. The browser authenticates by reaching a
|
|
7
|
+
* loopback port, so a page never holds a credential that would let it talk to
|
|
8
|
+
* a session directly. One page WebSocket carries all sessions: hub frames
|
|
9
|
+
* describe the set, and session traffic travels enveloped by session id.
|
|
10
|
+
*/
|
|
11
|
+
declare function serveWeb(options: WebServerOptions): Promise<WebServer>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { serveWeb };
|
|
14
|
+
//# sourceMappingURL=httpServer.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.d.mts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;iBA+NsB,SAAS,SAAS,mBAAmB,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createPiHubService as e}from"./piHubService.mjs";import{createSyncGuard as t}from"./syncGuard.mjs";import{createPiWebSocketListener as n}from"./piWebSocketListener.mjs";import{MAX_SESSION_FILE_BYTES as r,SESSION_FILE_ROUTE as i}from"../types/media.mjs";import{sessionFileHeaders as a}from"../services/fileMedia.mjs";import{contentTypeFor as o,resolveAssetPath as s}from"../services/staticAssets.mjs";import{API_SESSION_QUERY_PARAM as c,DIRECTORIES_API_ROUTE as l,HUB_RESYNCED_TYPE as u,SESSIONS_API_ROUTE as d,SESSIONS_SNAPSHOT_TYPE as f,SESSION_REMOVED_TYPE as p,SESSION_UPSERT_TYPE as m,hubHello as h,sessionFrameEnvelope as g,threadBacklog as _,threadFrameEnvelope as v}from"../types/hub.mjs";import"../types/session.mjs";import{readGitStatus as y}from"./gitStatus.mjs";import{watchRegistry as b}from"./registryWatcher.mjs";import{createServerSpawner as x}from"./serverSpawner.mjs";import{createSessionHub as S}from"./sessionHub.mjs";import{registerAuthRoutes as C}from"./authRoutes.mjs";import{createProviderAuth as w}from"./providerAuth.mjs";import{suggestDirectories as T}from"./directoryListing.mjs";import{listSessionFiles as E,readSessionFile as D}from"./sessionFiles.mjs";import{proxyToSocket as O}from"./packageApiProxy.mjs";import{createThreadJournals as k}from"./threadJournals.mjs";import{loadHubChannels as A}from"./webHubPluginLoader.mjs";import j from"node:fs";import M from"node:path";import{fileURLToPath as N}from"node:url";import P from"node:os";import{serve as F}from"@hono/node-server";import{createNodeWebSocket as I}from"@hono/node-ws";import{PiServer as L}from"@earendil-works/pi-server";import{Hono as R}from"hono";import{DOOM_API_ROUTE_PREFIX as z}from"@agimon-ai/doompi-extension-contracts/package-api";import{loadPackageApis as B}from"@agimon-ai/doompi-extension-contracts/package-api-loader";const V=`index.html`,H=[`.doompi`,`web`,`current`,`web`];function U(){let e=M.dirname(N(import.meta.url));for(;;){if(j.existsSync(M.join(e,`package.json`)))return M.join(e,`dist`,`web`);let t=M.dirname(e);if(t===e)throw Error(`doompi-web could not locate its own package root.`);e=t}}function W(e,t){if(e!==void 0)return e;let n=process.env.DOOMPI_WEB_DIST;if(n!==void 0&&n!==``)return n;let r=M.join(P.homedir(),...H);return j.existsSync(M.join(r,V))?(t(`serving the synced cockpit bundle from ${r}`),r):U()}function G(e){try{return j.statSync(e).isFile()?j.readFileSync(e):void 0}catch{return}}function K(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function q(e){return e instanceof Error?e.message:String(e)}function J(e,t){return`${e}\n${t}`}function Y(e,t,n){return S({source:b(e.registryDir,t),spawner:x({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:y,channels:n,onNotice:t})}function X(e,t,n){let r=[];for(let i of t){let t=`${z}/${i.basePath}`,a;try{a=i.start({scope:`hub`,onNotice:n})}catch(e){n(`hub API '${i.basePath}' did not start (${q(e)}); its routes stay unmounted`);continue}r.push(a),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await a.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${i.basePath}' failed on ${r.pathname} (${q(t)})`),e.json({error:`The '${i.basePath}' API failed.`},500)}})}return r}function Z(e,t,n){e.all(`${z}/*`,async e=>{let r=e.req.query(c);if(r===void 0)return e.notFound();let i=t.snapshot().find(e=>e.id===r);if(i===void 0)return e.json({error:`No session ${r}.`},404);if(i.apiSocketPath===void 0)return e.json({error:`Session ${r} serves no package API.`},404);let a=new URL(e.req.url);a.searchParams.delete(c);try{return await O({socketPath:i.apiSocketPath,path:`${a.pathname}${a.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body})}catch(t){return n(`session ${r} API is unreachable (${q(t)})`),e.json({error:`Session ${r} is not answering.`},502)}})}async function Q(c){let y=c.host??`127.0.0.1`,b=c.onNotice??(()=>{}),x=W(c.assetsDir,b),S=Y(c,b,await A(x,b)),O=k({resolve:(e,t)=>S.threadJournal(e,t),onNotice:b}),j=t({repoRoot:process.cwd(),onNotice:b});await j.ensureSynced();let N=new Set,P=new R,z=I({app:P}),H=n({onError:e=>b(`protocol: ${e.message}`)}),U=new L(e({records:()=>S.records(),spawn:e=>S.create(e),onNotice:b}),{listeners:[H],onError:e=>b(`protocol: ${e.message}`)});await U.start();let q=w({runtime:c.authRuntime,onNotice:b});C(P,q);let Q=X(P,await B(`hub`,{onNotice:b}),b);return Z(P,S,b),P.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:S.snapshot().length,pid:process.pid})),P.post(d,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!K(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await j.ensureSynced();let r=await S.create({cwd:t.cwd,name:n});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),P.delete(`${d}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=S.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),P.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=S.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await E(n.cwd,r,20);return e.json({files:i})}),P.get(l,async e=>{let t=await T(e.req.query(`q`)??``,{limit:12});return e.json({directories:t})}),P.get(i,async e=>{let t=e.req.param(`sessionId`),n=S.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let i=e.req.query(`path`)??``,o=await D(n.cwd,i,r);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a(i))}),P.get(`/api/session`,z.upgradeWebSocket(()=>{let e=new Set,t=new Map,n,r,i,a=e=>{for(let[n,r]of t)(e===void 0||r.sessionId===e)&&(t.delete(n),O.unsubscribe(r.sessionId,r.threadId))};return{onOpen(o,s){let c=e=>{try{s.send(JSON.stringify(e))}catch{}};i=c,N.add(c),c(h(S.channelTypes())),c({type:f,sessions:S.snapshot()}),n=S.onEvent(t=>{t.kind===`upsert`?c({type:m,session:t.session}):t.kind===`removed`?(e.delete(t.sessionId),a(t.sessionId),c({type:p,sessionId:t.sessionId})):t.kind===`channel`?e.has(t.sessionId)&&c({type:t.frameType,sessionId:t.sessionId,payload:t.payload}):e.has(t.sessionId)&&c(g(t.sessionId,t.frame))}),r=O.onFrame(e=>{t.has(J(e.sessionId,e.threadId))&&c(v(e.sessionId,e.threadId,e.frame))}),b(`browser attached`)},onMessage(n,r){if(typeof n.data!=`string`)return;let i;try{i=JSON.parse(n.data)}catch{return}if(!K(i)||typeof i.sessionId!=`string`)return;let a=i.sessionId;if(i.type===`subscribe`){let t=S.backlog(a);if(!t)return;e.add(a);try{r.send(JSON.stringify(t));for(let e of S.channelFrames(a))r.send(JSON.stringify(e))}catch{}return}if(i.type===`unsubscribe`){e.delete(a);return}if(i.type===`history_request`){let e=S.history(a,{...typeof i.before==`string`?{before:i.before}:{},...typeof i.limit==`number`?{limit:i.limit}:{}});if(!e)return;try{r.send(JSON.stringify(e))}catch{}return}if(i.type===`subscribe_thread`||i.type===`unsubscribe_thread`){if(typeof i.threadId!=`string`)return;let e=i.threadId,n=J(a,e);if(i.type===`unsubscribe_thread`){t.delete(n)&&O.unsubscribe(a,e);return}if(t.has(n)||S.backlog(a)===void 0)return;t.set(n,{sessionId:a,threadId:e});try{r.send(JSON.stringify(_(a,e,O.subscribe(a,e))))}catch{}return}if(i.type===`session_command`&&K(i.frame)){if(i.frame.type===`attach`)return;S.command(a,i.frame)}},onClose(){i&&N.delete(i),i=void 0,n?.(),n=void 0,r?.(),r=void 0,e.clear(),a(),b(`browser detached`)}}})),P.get(`/api/pi`,z.upgradeWebSocket(()=>{let e;return{onOpen(t,n){e=H.accept({send:e=>n.send(e),close:()=>n.close(),get readyState(){return n.readyState}})},onMessage(t){let n=t.data;typeof n!=`string`&&(n instanceof ArrayBuffer?e?.onData(new Uint8Array(n)):ArrayBuffer.isView(n)&&e?.onData(new Uint8Array(n.buffer,n.byteOffset,n.byteLength)))},onClose(){e?.onClose(),e=void 0},onError(t){e?.onError(t instanceof Error?t:Error(`The protocol socket failed`)),e=void 0}}})),P.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=t===`/`?void 0:s(x,t),r=n?G(n):void 0;if(n&&r)return e.body(new Uint8Array(r),200,{"Content-Type":o(n)});let i=G(M.join(x,V));return i?e.body(new Uint8Array(i),200,{"Content-Type":`text/html; charset=utf-8`}):e.text(`The cockpit bundle is missing. Run the package build.`,500)}),j.watch(()=>{for(let e of N)e({type:u})}),new Promise((e,t)=>{let n=F({fetch:P.fetch,port:c.port,hostname:y},t=>{z.injectWebSocket(n);let r=`http://${y}:${t.port}`;b(`cockpit on ${r}`),e({url:r,port:t.port,close:()=>new Promise(e=>{O.close(),j.close(),U.close(),S.close(),q.close();for(let e of Q)e.close();for(let e of z.wss.clients)e.terminate();z.wss.close(),n.closeAllConnections?.(),n.close(()=>e())})})});n.once(`error`,e=>{O.close(),S.close(),q.close();for(let e of Q)e.close();t(e)})})}export{Q as serveWeb};
|
|
2
|
+
//# sourceMappingURL=httpServer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"httpServer.mjs","names":[],"sources":["../../../src/adapters/httpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { PiServer } from '@earendil-works/pi-server';\nimport { createPiHubService } from './piHubService.ts';\nimport { createSyncGuard } from './syncGuard.ts';\nimport { createPiWebSocketListener } from './piWebSocketListener.ts';\nimport { Hono } from 'hono';\nimport type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport {\n DOOM_API_ROUTE_PREFIX,\n type DoomApi,\n type DoomApiHandler,\n} from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { loadPackageApis } from '@agimon-ai/doompi-extension-contracts/package-api-loader';\nimport { sessionFileHeaders } from '../services/fileMedia.ts';\nimport { contentTypeFor, resolveAssetPath } from '../services/staticAssets.ts';\nimport { MAX_SESSION_FILE_BYTES, SESSION_FILE_ROUTE } from '../types/media.ts';\nimport type { WebServer, WebServerOptions } from '../types/bridge.ts';\nimport {\n API_SESSION_QUERY_PARAM,\n DIRECTORIES_API_ROUTE,\n HISTORY_REQUEST_TYPE,\n HUB_PROTOCOL_VERSION,\n HUB_RESYNCED_TYPE,\n HUB_ROLE,\n SESSIONS_API_ROUTE,\n SESSIONS_SNAPSHOT_TYPE,\n SESSION_COMMAND_TYPE,\n SESSION_REMOVED_TYPE,\n SESSION_UPSERT_TYPE,\n SUBSCRIBE_THREAD_TYPE,\n SUBSCRIBE_TYPE,\n UNSUBSCRIBE_THREAD_TYPE,\n UNSUBSCRIBE_TYPE,\n hubHello,\n sessionFrameEnvelope,\n threadBacklog,\n threadFrameEnvelope,\n} from '../types/hub.ts';\nimport { ATTACH_TYPE, type SessionFrame } from '../types/session.ts';\nimport { readGitStatus } from './gitStatus.ts';\nimport { watchRegistry } from './registryWatcher.ts';\nimport { createServerSpawner } from './serverSpawner.ts';\nimport { createSessionHub, type SessionHub } from './sessionHub.ts';\nimport { registerAuthRoutes } from './authRoutes.ts';\nimport { createProviderAuth } from './providerAuth.ts';\nimport { suggestDirectories } from './directoryListing.ts';\nimport { listSessionFiles, readSessionFile } from './sessionFiles.ts';\nimport { proxyToSocket } from './packageApiProxy.ts';\nimport { createThreadJournals } from './threadJournals.ts';\nimport { loadHubChannels } from './webHubPluginLoader.ts';\n\nconst SESSION_ROUTE = '/api/session';\n// Pi's protocol rides its own socket so the DoomPi channel keeps the\n// vocabulary the protocol has no shape for: dialogs, minor modes, selection.\nconst PROTOCOL_ROUTE = '/api/pi';\n/** Directory suggestions per picker query; more than this means \"type further\". */\nconst DIRECTORY_SUGGESTION_LIMIT = 12;\nconst INDEX_FILE = 'index.html';\n/** Env override for the assets directory, set by launchers that know a synced bundle. */\nconst WEB_DIST_ENV = 'DOOMPI_WEB_DIST';\n/** Where `doompi sync` publishes the machine's cockpit bundle. */\nconst SYNCED_WEB_DIRECTORY = ['.doompi', 'web', 'current', 'web'];\n\n/**\n * Locates the built SPA next to the package that owns this module.\n *\n * Walking to the manifest rather than counting `..` segments keeps this correct\n * whether the code runs from `src` in development or from the unbundled `dist`\n * tree, whose depth is a build detail.\n */\nfunction packagedAssetsDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return path.join(dir, 'dist', 'web');\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('doompi-web could not locate its own package root.');\n dir = parent;\n }\n}\n\n/**\n * The assets to serve, in preference order: an explicit option, the env\n * override, the bundle `doompi sync` published for this machine (which\n * carries the installed plugin set), then the package's own prebuilt bundle\n * (built-in plugins only).\n */\nfunction resolveAssetsDir(explicit: string | undefined, notice: (message: string) => void): string {\n if (explicit !== undefined) return explicit;\n const fromEnv = process.env[WEB_DIST_ENV];\n if (fromEnv !== undefined && fromEnv !== '') return fromEnv;\n const synced = path.join(os.homedir(), ...SYNCED_WEB_DIRECTORY);\n if (fs.existsSync(path.join(synced, INDEX_FILE))) {\n notice(`serving the synced cockpit bundle from ${synced}`);\n return synced;\n }\n return packagedAssetsDir();\n}\n\nfunction readAsset(filePath: string): Buffer | undefined {\n try {\n const stat = fs.statSync(filePath);\n if (!stat.isFile()) return undefined;\n return fs.readFileSync(filePath);\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** One page's hold on one thread; a session id never contains a newline, so the pair cannot collide. */\nfunction threadKey(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\nfunction buildHub(\n options: WebServerOptions,\n notice: (message: string) => void,\n channels: readonly WebHubChannel[],\n): SessionHub {\n return createSessionHub({\n source: watchRegistry(options.registryDir, notice),\n spawner: createServerSpawner({\n registryDir: options.registryDir,\n command: options.spawnCommand,\n onNotice: notice,\n }),\n readGit: readGitStatus,\n channels,\n onNotice: notice,\n });\n}\n\n/**\n * Mounts the hub-scoped package APIs under /api/plugin/<basePath>/.\n *\n * The prefix is stripped before the API sees the request, so a package\n * declares its routes relative to its own mount and never repeats where a host\n * put it. An API that throws answers 500 for its own routes alone; one bad\n * package never takes the cockpit down with it.\n */\nfunction mountHubApis(app: Hono, apis: readonly DoomApi[], notice: (message: string) => void): DoomApiHandler[] {\n const handlers: DoomApiHandler[] = [];\n for (const api of apis) {\n const mount = `${DOOM_API_ROUTE_PREFIX}/${api.basePath}`;\n let handler: DoomApiHandler;\n try {\n handler = api.start({ scope: 'hub', onNotice: notice });\n } catch (error) {\n notice(`hub API '${api.basePath}' did not start (${describeError(error)}); its routes stay unmounted`);\n continue;\n }\n handlers.push(handler);\n app.all(`${mount}/*`, async (context) => {\n if (context.req.query(API_SESSION_QUERY_PARAM) !== undefined) return context.notFound();\n const url = new URL(context.req.url);\n url.pathname = url.pathname.slice(mount.length) || '/';\n try {\n return await handler.fetch(new Request(url, context.req.raw));\n } catch (error) {\n notice(`hub API '${api.basePath}' failed on ${url.pathname} (${describeError(error)})`);\n return context.json({ error: `The '${api.basePath}' API failed.` }, 500);\n }\n });\n }\n return handlers;\n}\n\n/**\n * Forwards a package API request carrying ?session= to that session's own\n * server, over the API socket its registry record names.\n *\n * The session servers are where session-scoped data lives, but the browser must\n * not talk to them: their attach tokens stay in this process and the page only\n * ever reaches loopback. So the hub is the one door, and this is the hop behind\n * it. A session that never mounted an API answers 404 with the reason rather\n * than leaving the page waiting.\n */\nfunction mountSessionApiProxy(app: Hono, hub: SessionHub, notice: (message: string) => void): void {\n app.all(`${DOOM_API_ROUTE_PREFIX}/*`, async (context) => {\n const sessionId = context.req.query(API_SESSION_QUERY_PARAM);\n if (sessionId === undefined) return context.notFound();\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (summary === undefined) return context.json({ error: `No session ${sessionId}.` }, 404);\n if (summary.apiSocketPath === undefined) {\n return context.json({ error: `Session ${sessionId} serves no package API.` }, 404);\n }\n const url = new URL(context.req.url);\n url.searchParams.delete(API_SESSION_QUERY_PARAM);\n try {\n return await proxyToSocket({\n socketPath: summary.apiSocketPath,\n path: `${url.pathname}${url.search}`,\n method: context.req.method,\n headers: context.req.raw.headers,\n body: context.req.raw.body,\n });\n } catch (error) {\n notice(`session ${sessionId} API is unreachable (${describeError(error)})`);\n return context.json({ error: `Session ${sessionId} is not answering.` }, 502);\n }\n });\n}\n\n/**\n * Serves the cockpit and multiplexes every session behind it.\n *\n * Attach tokens stay in this process. The browser authenticates by reaching a\n * loopback port, so a page never holds a credential that would let it talk to\n * a session directly. One page WebSocket carries all sessions: hub frames\n * describe the set, and session traffic travels enveloped by session id.\n */\nexport async function serveWeb(options: WebServerOptions): Promise<WebServer> {\n const host = options.host ?? '127.0.0.1';\n const notice = options.onNotice ?? ((): void => {});\n const assetsDir = resolveAssetsDir(options.assetsDir, notice);\n const hub = buildHub(options, notice, await loadHubChannels(assetsDir, notice));\n // Threads are journals the data channels can name (a subagent run's own\n // session file); the hub tails one only while a page follows it.\n const threads = createThreadJournals({\n resolve: (sessionId, threadId) => hub.threadJournal(sessionId, threadId),\n onNotice: notice,\n });\n // A session reads what sync produced, and nothing makes the person who\n // opened the cockpit run sync first. The hub keeps the repository current.\n const syncGuard = createSyncGuard({ repoRoot: process.cwd(), onNotice: notice });\n await syncGuard.ensureSynced();\n /** Every attached page, so a rebuild can tell them all to pick it up. */\n const pages = new Set<(frame: object) => void>();\n\n const app = new Hono();\n const nodeWs = createNodeWebSocket({ app });\n // Every running session already serves Pi's protocol; the hub composes them\n // into one server so a browser sees a single endpoint with many sessions.\n const protocolListener = createPiWebSocketListener({ onError: (error) => notice(`protocol: ${error.message}`) });\n const protocolServer = new PiServer(\n createPiHubService({\n records: () => hub.records(),\n spawn: (input) => hub.create(input),\n onNotice: notice,\n }),\n { listeners: [protocolListener], onError: (error) => notice(`protocol: ${error.message}`) },\n );\n await protocolServer.start();\n // Provider credentials belong to the machine, not to a session: the hub\n // keeps one Pi runtime over the shared auth.json and signs in for all.\n const providerAuth = createProviderAuth({ runtime: options.authRuntime, onNotice: notice });\n registerAuthRoutes(app, providerAuth);\n // Package APIs, mounted before the SPA fallback so their routes are reachable\n // and everything else still falls through to the bundle. Hub-scoped ones run\n // here; session-scoped ones live in each session's own server and are proxied.\n const pluginApis = mountHubApis(app, await loadPackageApis('hub', { onNotice: notice }), notice);\n mountSessionApiProxy(app, hub, notice);\n\n app.get('/api/health', (context) =>\n context.json({\n ok: true,\n role: HUB_ROLE,\n protocol: HUB_PROTOCOL_VERSION,\n sessions: hub.snapshot().length,\n pid: process.pid,\n }),\n );\n\n app.post(SESSIONS_API_ROUTE, async (context) => {\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The request body must be JSON.' }, 400);\n }\n if (!isRecord(body) || typeof body.cwd !== 'string' || body.cwd === '') {\n return context.json({ error: 'A cwd string is required.' }, 400);\n }\n const name = typeof body.name === 'string' && body.name !== '' ? body.name : undefined;\n await syncGuard.ensureSynced();\n const outcome = await hub.create({ cwd: body.cwd, name });\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 201);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 400 : 502);\n });\n\n app.delete(`${SESSIONS_API_ROUTE}/:sessionId`, (context) => {\n const sessionId = context.req.param('sessionId');\n const outcome = hub.stop(sessionId);\n if (outcome.ok) return context.json({ sessionId }, 202);\n return context.json(\n { error: outcome.error },\n outcome.code === 'unknown' ? 404 : outcome.code === 'self' ? 409 : 502,\n );\n });\n\n // File completion for the composer's @ references: bounded, cwd-scoped,\n // and only for sessions the hub actually manages.\n app.get('/api/sessions/:sessionId/files', async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const query = context.req.query('q') ?? '';\n const files = await listSessionFiles(summary.cwd, query, 20);\n return context.json({ files });\n });\n\n // Directory suggestions for the new-session picker: the children of the\n // typed parent while a path is being drilled into, and a ranked search of\n // the home directory for anything else, so a name or a path remembered from\n // another machine still finds the folder.\n app.get(DIRECTORIES_API_ROUTE, async (context) => {\n const directories = await suggestDirectories(context.req.query('q') ?? '', {\n limit: DIRECTORY_SUGGESTION_LIMIT,\n });\n return context.json({ directories });\n });\n\n // The timeline's @file previews: one cwd-contained file, capped in size.\n app.get(SESSION_FILE_ROUTE, async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const relativePath = context.req.query('path') ?? '';\n const file = await readSessionFile(summary.cwd, relativePath, MAX_SESSION_FILE_BYTES);\n if (file.status === 'forbidden') return context.json({ error: 'The path leaves the session directory.' }, 403);\n if (file.status === 'not-found') return context.json({ error: 'No such file.' }, 404);\n if (file.status === 'too-large') return context.json({ error: 'The file is too large to preview.' }, 413);\n return context.body(new Uint8Array(file.body), 200, sessionFileHeaders(relativePath));\n });\n\n app.get(\n SESSION_ROUTE,\n nodeWs.upgradeWebSocket(() => {\n const subscriptions = new Set<string>();\n /** The threads this page follows; one socket may follow several of one session. */\n const threadSubscriptions = new Map<string, { sessionId: string; threadId: string }>();\n let disconnect: (() => void) | undefined;\n let disconnectThreads: (() => void) | undefined;\n /** Held on the socket, not inside onOpen, so close can withdraw it. */\n let registered: ((frame: object) => void) | undefined;\n /** Lets go of every followed thread, or only a departed session's. */\n const releaseThreads = (sessionId?: string): void => {\n for (const [key, held] of threadSubscriptions) {\n if (sessionId !== undefined && held.sessionId !== sessionId) continue;\n threadSubscriptions.delete(key);\n threads.unsubscribe(held.sessionId, held.threadId);\n }\n };\n return {\n onOpen(_event, ws) {\n const post = (frame: SessionFrame | object): void => {\n try {\n ws.send(JSON.stringify(frame));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n };\n registered = post;\n pages.add(post);\n post(hubHello(hub.channelTypes()));\n post({ type: SESSIONS_SNAPSHOT_TYPE, sessions: hub.snapshot() });\n disconnect = hub.onEvent((event) => {\n if (event.kind === 'upsert') post({ type: SESSION_UPSERT_TYPE, session: event.session });\n else if (event.kind === 'removed') {\n subscriptions.delete(event.sessionId);\n releaseThreads(event.sessionId);\n post({ type: SESSION_REMOVED_TYPE, sessionId: event.sessionId });\n } else if (event.kind === 'channel') {\n if (subscriptions.has(event.sessionId)) {\n post({ type: event.frameType, sessionId: event.sessionId, payload: event.payload });\n }\n } else if (subscriptions.has(event.sessionId)) {\n post(sessionFrameEnvelope(event.sessionId, event.frame));\n }\n });\n disconnectThreads = threads.onFrame((event) => {\n if (threadSubscriptions.has(threadKey(event.sessionId, event.threadId))) {\n post(threadFrameEnvelope(event.sessionId, event.threadId, event.frame));\n }\n });\n notice('browser attached');\n },\n onMessage(event, ws) {\n if (typeof event.data !== 'string') return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(event.data);\n } catch {\n return;\n }\n if (!isRecord(parsed) || typeof parsed.sessionId !== 'string') return;\n const sessionId = parsed.sessionId;\n if (parsed.type === SUBSCRIBE_TYPE) {\n const backlog = hub.backlog(sessionId);\n if (!backlog) return; // Unknown session; the snapshot said otherwise.\n subscriptions.add(sessionId);\n try {\n ws.send(JSON.stringify(backlog));\n for (const frame of hub.channelFrames(sessionId)) ws.send(JSON.stringify(frame));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === UNSUBSCRIBE_TYPE) {\n subscriptions.delete(sessionId);\n return;\n }\n if (parsed.type === HISTORY_REQUEST_TYPE) {\n // Older transcript, on demand. The hub kept what the attach path\n // was too small to publish, so scrolling back reads from memory\n // rather than asking the session to re-read its journal.\n const page = hub.history(sessionId, {\n ...(typeof parsed.before === 'string' ? { before: parsed.before } : {}),\n ...(typeof parsed.limit === 'number' ? { limit: parsed.limit } : {}),\n });\n if (!page) return;\n try {\n ws.send(JSON.stringify(page));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === SUBSCRIBE_THREAD_TYPE || parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (typeof parsed.threadId !== 'string') return;\n const threadId = parsed.threadId;\n const key = threadKey(sessionId, threadId);\n if (parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (threadSubscriptions.delete(key)) threads.unsubscribe(sessionId, threadId);\n return;\n }\n if (threadSubscriptions.has(key) || hub.backlog(sessionId) === undefined) return;\n threadSubscriptions.set(key, { sessionId, threadId });\n try {\n ws.send(JSON.stringify(threadBacklog(sessionId, threadId, threads.subscribe(sessionId, threadId))));\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n return;\n }\n if (parsed.type === SESSION_COMMAND_TYPE && isRecord(parsed.frame)) {\n // The hub owns the handshake; a page must not be able to replay it.\n if (parsed.frame.type === ATTACH_TYPE) return;\n hub.command(sessionId, parsed.frame);\n }\n },\n onClose() {\n if (registered) pages.delete(registered);\n registered = undefined;\n disconnect?.();\n disconnect = undefined;\n disconnectThreads?.();\n disconnectThreads = undefined;\n subscriptions.clear();\n releaseThreads();\n notice('browser detached');\n },\n };\n }),\n );\n\n app.get(\n PROTOCOL_ROUTE,\n nodeWs.upgradeWebSocket(() => {\n let handler: ReturnType<typeof protocolListener.accept>;\n return {\n onOpen(_event, ws) {\n handler = protocolListener.accept({\n send: (data) => ws.send(data as ArrayBuffer),\n close: () => ws.close(),\n get readyState() {\n return ws.readyState;\n },\n });\n },\n onMessage(event) {\n const data = event.data;\n if (typeof data === 'string') return;\n if (data instanceof ArrayBuffer) handler?.onData(new Uint8Array(data));\n else if (ArrayBuffer.isView(data)) {\n handler?.onData(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));\n }\n },\n onClose() {\n handler?.onClose();\n handler = undefined;\n },\n onError(event) {\n handler?.onError(event instanceof Error ? event : new Error('The protocol socket failed'));\n handler = undefined;\n },\n };\n }),\n );\n\n app.get('*', (context) => {\n const requested = new URL(context.req.url).pathname;\n const direct = requested === '/' ? undefined : resolveAssetPath(assetsDir, requested);\n const body = direct ? readAsset(direct) : undefined;\n if (direct && body) {\n return context.body(new Uint8Array(body), 200, { 'Content-Type': contentTypeFor(direct) });\n }\n const index = readAsset(path.join(assetsDir, INDEX_FILE));\n if (!index) return context.text('The cockpit bundle is missing. Run the package build.', 500);\n return context.body(new Uint8Array(index), 200, { 'Content-Type': 'text/html; charset=utf-8' });\n });\n\n // A rebuilt bundle only changes on disk, so the page it replaced has to be\n // told; nothing about a loaded bundle notices that its source moved.\n syncGuard.watch(() => {\n for (const post of pages) post({ type: HUB_RESYNCED_TYPE });\n });\n\n return new Promise<WebServer>((resolve, reject) => {\n const server = serve({ fetch: app.fetch, port: options.port, hostname: host }, (info) => {\n nodeWs.injectWebSocket(server);\n const url = `http://${host}:${info.port}`;\n notice(`cockpit on ${url}`);\n resolve({\n url,\n port: info.port,\n close: () =>\n new Promise<void>((done) => {\n threads.close();\n syncGuard.close();\n void protocolServer.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n // An upgraded socket leaves the HTTP server's connection tracking,\n // so only the WebSocket server can let go of it. Without this the\n // close callback waits on a browser that has no reason to leave.\n for (const client of nodeWs.wss.clients) client.terminate();\n nodeWs.wss.close();\n (server as { closeAllConnections?: () => void }).closeAllConnections?.();\n server.close(() => done());\n }),\n });\n });\n server.once('error', (error) => {\n threads.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n reject(error);\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubProbe.cjs","names":[],"sources":["../../../src/adapters/hubProbe.ts"],"sourcesContent":["const HEALTH_ROUTE = '/api/health';\nconst HUB_ROLE = 'hub';\nconst PROBE_TIMEOUT_MS = 1000;\n\n/**\n * True when a DoomPi cockpit hub already answers on the address.\n *\n * The cockpit is a multi-session hub, so a second one is never what someone\n * wants: running `doompi-web` again should hand back the running cockpit\n * rather than fail on a port clash. Anything else answering the port is not a\n * hub and must not be mistaken for one, which is why the role is checked and\n * not just the status code.\n */\nexport async function hubAnswers(host: string, port: number): Promise<boolean> {\n try {\n const response = await fetch(`http://${host}:${String(port)}${HEALTH_ROUTE}`, {\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),\n });\n if (!response.ok) return false;\n const body = (await response.json()) as { ok?: boolean; role?: string };\n return body.ok === true && body.role === HUB_ROLE;\n } catch {\n // Nothing listening, a timeout, or a body that is not the hub's: all mean\n // \"no hub here\", which is the caller's cue to start one.\n return false;\n }\n}\n"],"mappings":"AAaA,eAAsB,EAAW,EAAc,EAAgC,CAC7E,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,UAAU,EAAK,GAAG,OAAO,CAAI,eAAoB,CAC5E,OAAQ,YAAY,QAAQ,GAAgB,CAC9C,CAAC,EACD,GAAI,CAAC,EAAS,GAAI,MAAO,GACzB,IAAM,EAAQ,MAAM,EAAS,KAAK,EAClC,OAAO,EAAK,KAAO,IAAQ,EAAK,OAAS,KAC3C,MAAQ,CAGN,MAAO,EACT,CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubProbe.mjs","names":[],"sources":["../../../src/adapters/hubProbe.ts"],"sourcesContent":["const HEALTH_ROUTE = '/api/health';\nconst HUB_ROLE = 'hub';\nconst PROBE_TIMEOUT_MS = 1000;\n\n/**\n * True when a DoomPi cockpit hub already answers on the address.\n *\n * The cockpit is a multi-session hub, so a second one is never what someone\n * wants: running `doompi-web` again should hand back the running cockpit\n * rather than fail on a port clash. Anything else answering the port is not a\n * hub and must not be mistaken for one, which is why the role is checked and\n * not just the status code.\n */\nexport async function hubAnswers(host: string, port: number): Promise<boolean> {\n try {\n const response = await fetch(`http://${host}:${String(port)}${HEALTH_ROUTE}`, {\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),\n });\n if (!response.ok) return false;\n const body = (await response.json()) as { ok?: boolean; role?: string };\n return body.ok === true && body.role === HUB_ROLE;\n } catch {\n // Nothing listening, a timeout, or a body that is not the hub's: all mean\n // \"no hub here\", which is the caller's cue to start one.\n return false;\n }\n}\n"],"mappings":"AAaA,eAAsB,EAAW,EAAc,EAAgC,CAC7E,GAAI,CACF,IAAM,EAAW,MAAM,MAAM,UAAU,EAAK,GAAG,OAAO,CAAI,eAAoB,CAC5E,OAAQ,YAAY,QAAQ,GAAgB,CAC9C,CAAC,EACD,GAAI,CAAC,EAAS,GAAI,MAAO,GACzB,IAAM,EAAQ,MAAM,EAAS,KAAK,EAClC,OAAO,EAAK,KAAO,IAAQ,EAAK,OAAS,KAC3C,MAAQ,CAGN,MAAO,EACT,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:http");t=e.__toESM(t,1);let n=require("node:stream");const r=new Set([`connection`,`keep-alive`,`transfer-encoding`,`upgrade`]);function i(e){let t={};return e.forEach((e,n)=>{r.has(n.toLowerCase())||(t[n]=e)}),t.host=`session.local`,t}async function a(e){return new Promise((a,o)=>{let s=t.default.request({socketPath:e.socketPath,path:e.path,method:e.method,headers:i(e.headers)},e=>{let t=new Headers;for(let[n,i]of Object.entries(e.headers))i===void 0||r.has(n.toLowerCase())||t.set(n,Array.isArray(i)?i.join(`, `):i);let i=e.statusCode??502,o=i===204||i===304?null:n.Readable.toWeb(e);a(new Response(o,{status:i,headers:t}))});if(s.once(`error`,o),e.body===null){s.end();return}new Response(e.body).arrayBuffer().then(e=>s.end(Buffer.from(e))).catch(o)})}exports.proxyToSocket=a;
|
|
2
|
+
//# sourceMappingURL=packageApiProxy.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packageApiProxy.cjs","names":["http","Readable"],"sources":["../../../src/adapters/packageApiProxy.ts"],"sourcesContent":["import http from 'node:http';\nimport { Readable } from 'node:stream';\n\n/** Headers that describe one hop and must not be copied onto the next. */\nconst HOP_BY_HOP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'upgrade']);\n\nexport interface ProxyToSocketInput {\n socketPath: string;\n /** Path and query as the upstream should see them. */\n path: string;\n method: string;\n headers: Headers;\n body: BodyInit | null;\n}\n\nfunction outgoingHeaders(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n if (!HOP_BY_HOP.has(key.toLowerCase())) out[key] = value;\n });\n // The upstream is a unix socket with no meaningful authority; a stable value\n // keeps its own URL parsing predictable.\n out.host = 'session.local';\n return out;\n}\n\n/**\n * Forwards one request to a session server's API socket and returns its answer.\n *\n * The response body is handed back as a stream rather than buffered, so a\n * server-sent event stream reaches the browser as it is produced instead of\n * arriving all at once when the runner finally exits.\n */\nexport async function proxyToSocket(input: ProxyToSocketInput): Promise<Response> {\n return new Promise<Response>((resolve, reject) => {\n const request = http.request(\n { socketPath: input.socketPath, path: input.path, method: input.method, headers: outgoingHeaders(input.headers) },\n (incoming) => {\n const headers = new Headers();\n for (const [key, value] of Object.entries(incoming.headers)) {\n if (value === undefined || HOP_BY_HOP.has(key.toLowerCase())) continue;\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n const status = incoming.statusCode ?? 502;\n // 204 and 304 carry no body, and constructing one with a stream throws.\n const body = status === 204 || status === 304 ? null : (Readable.toWeb(incoming) as ReadableStream);\n resolve(new Response(body, { status, headers }));\n },\n );\n request.once('error', reject);\n if (input.body === null) {\n request.end();\n return;\n }\n void new Response(input.body)\n .arrayBuffer()\n .then((buffer) => request.end(Buffer.from(buffer)))\n .catch(reject);\n });\n}\n"],"mappings":"iIAIA,MAAM,EAAa,IAAI,IAAI,CAAC,aAAc,aAAc,oBAAqB,SAAS,CAAC,EAWvF,SAAS,EAAgB,EAA0C,CACjE,IAAM,EAA8B,CAAC,EAOrC,OANA,EAAQ,SAAS,EAAO,IAAQ,CACzB,EAAW,IAAI,EAAI,YAAY,CAAC,IAAG,EAAI,GAAO,EACrD,CAAC,EAGD,EAAI,KAAO,gBACJ,CACT,CASA,eAAsB,EAAc,EAA8C,CAChF,OAAO,IAAI,SAAmB,EAAS,IAAW,CAChD,IAAM,EAAUA,EAAAA,QAAK,QACnB,CAAE,WAAY,EAAM,WAAY,KAAM,EAAM,KAAM,OAAQ,EAAM,OAAQ,QAAS,EAAgB,EAAM,OAAO,CAAE,EAC/G,GAAa,CACZ,IAAM,EAAU,IAAI,QACpB,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAS,OAAO,EACpD,IAAU,IAAA,IAAa,EAAW,IAAI,EAAI,YAAY,CAAC,GAC3D,EAAQ,IAAI,EAAK,MAAM,QAAQ,CAAK,EAAI,EAAM,KAAK,IAAI,EAAI,CAAK,EAElE,IAAM,EAAS,EAAS,YAAc,IAEhC,EAAO,IAAW,KAAO,IAAW,IAAM,KAAQC,EAAAA,SAAS,MAAM,CAAQ,EAC/E,EAAQ,IAAI,SAAS,EAAM,CAAE,SAAQ,SAAQ,CAAC,CAAC,CACjD,CACF,EAEA,GADA,EAAQ,KAAK,QAAS,CAAM,EACxB,EAAM,OAAS,KAAM,CACvB,EAAQ,IAAI,EACZ,MACF,CACA,IAAS,SAAS,EAAM,IAAI,CAAC,CAC1B,YAAY,CAAC,CACb,KAAM,GAAW,EAAQ,IAAI,OAAO,KAAK,CAAM,CAAC,CAAC,CAAC,CAClD,MAAM,CAAM,CACjB,CAAC,CACH"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"node:http";import{Readable as t}from"node:stream";const n=new Set([`connection`,`keep-alive`,`transfer-encoding`,`upgrade`]);function r(e){let t={};return e.forEach((e,r)=>{n.has(r.toLowerCase())||(t[r]=e)}),t.host=`session.local`,t}async function i(i){return new Promise((a,o)=>{let s=e.request({socketPath:i.socketPath,path:i.path,method:i.method,headers:r(i.headers)},e=>{let r=new Headers;for(let[t,i]of Object.entries(e.headers))i===void 0||n.has(t.toLowerCase())||r.set(t,Array.isArray(i)?i.join(`, `):i);let i=e.statusCode??502,o=i===204||i===304?null:t.toWeb(e);a(new Response(o,{status:i,headers:r}))});if(s.once(`error`,o),i.body===null){s.end();return}new Response(i.body).arrayBuffer().then(e=>s.end(Buffer.from(e))).catch(o)})}export{i as proxyToSocket};
|
|
2
|
+
//# sourceMappingURL=packageApiProxy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packageApiProxy.mjs","names":[],"sources":["../../../src/adapters/packageApiProxy.ts"],"sourcesContent":["import http from 'node:http';\nimport { Readable } from 'node:stream';\n\n/** Headers that describe one hop and must not be copied onto the next. */\nconst HOP_BY_HOP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'upgrade']);\n\nexport interface ProxyToSocketInput {\n socketPath: string;\n /** Path and query as the upstream should see them. */\n path: string;\n method: string;\n headers: Headers;\n body: BodyInit | null;\n}\n\nfunction outgoingHeaders(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n if (!HOP_BY_HOP.has(key.toLowerCase())) out[key] = value;\n });\n // The upstream is a unix socket with no meaningful authority; a stable value\n // keeps its own URL parsing predictable.\n out.host = 'session.local';\n return out;\n}\n\n/**\n * Forwards one request to a session server's API socket and returns its answer.\n *\n * The response body is handed back as a stream rather than buffered, so a\n * server-sent event stream reaches the browser as it is produced instead of\n * arriving all at once when the runner finally exits.\n */\nexport async function proxyToSocket(input: ProxyToSocketInput): Promise<Response> {\n return new Promise<Response>((resolve, reject) => {\n const request = http.request(\n { socketPath: input.socketPath, path: input.path, method: input.method, headers: outgoingHeaders(input.headers) },\n (incoming) => {\n const headers = new Headers();\n for (const [key, value] of Object.entries(incoming.headers)) {\n if (value === undefined || HOP_BY_HOP.has(key.toLowerCase())) continue;\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n const status = incoming.statusCode ?? 502;\n // 204 and 304 carry no body, and constructing one with a stream throws.\n const body = status === 204 || status === 304 ? null : (Readable.toWeb(incoming) as ReadableStream);\n resolve(new Response(body, { status, headers }));\n },\n );\n request.once('error', reject);\n if (input.body === null) {\n request.end();\n return;\n }\n void new Response(input.body)\n .arrayBuffer()\n .then((buffer) => request.end(Buffer.from(buffer)))\n .catch(reject);\n });\n}\n"],"mappings":"gEAIA,MAAM,EAAa,IAAI,IAAI,CAAC,aAAc,aAAc,oBAAqB,SAAS,CAAC,EAWvF,SAAS,EAAgB,EAA0C,CACjE,IAAM,EAA8B,CAAC,EAOrC,OANA,EAAQ,SAAS,EAAO,IAAQ,CACzB,EAAW,IAAI,EAAI,YAAY,CAAC,IAAG,EAAI,GAAO,EACrD,CAAC,EAGD,EAAI,KAAO,gBACJ,CACT,CASA,eAAsB,EAAc,EAA8C,CAChF,OAAO,IAAI,SAAmB,EAAS,IAAW,CAChD,IAAM,EAAU,EAAK,QACnB,CAAE,WAAY,EAAM,WAAY,KAAM,EAAM,KAAM,OAAQ,EAAM,OAAQ,QAAS,EAAgB,EAAM,OAAO,CAAE,EAC/G,GAAa,CACZ,IAAM,EAAU,IAAI,QACpB,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAS,OAAO,EACpD,IAAU,IAAA,IAAa,EAAW,IAAI,EAAI,YAAY,CAAC,GAC3D,EAAQ,IAAI,EAAK,MAAM,QAAQ,CAAK,EAAI,EAAM,KAAK,IAAI,EAAI,CAAK,EAElE,IAAM,EAAS,EAAS,YAAc,IAEhC,EAAO,IAAW,KAAO,IAAW,IAAM,KAAQ,EAAS,MAAM,CAAQ,EAC/E,EAAQ,IAAI,SAAS,EAAM,CAAE,SAAQ,SAAQ,CAAC,CAAC,CACjD,CACF,EAEA,GADA,EAAQ,KAAK,QAAS,CAAM,EACxB,EAAM,OAAS,KAAM,CACvB,EAAQ,IAAI,EACZ,MACF,CACA,IAAS,SAAS,EAAM,IAAI,CAAC,CAC1B,YAAY,CAAC,CACb,KAAM,GAAW,EAAQ,IAAI,OAAO,KAAK,CAAM,CAAC,CAAC,CAAC,CAClD,MAAM,CAAM,CACjB,CAAC,CACH"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("@earendil-works/pi-server"),t=require("@earendil-works/pi-client"),n=require("@earendil-works/pi-client/unix");function r(e){let t=Date.parse(e.createdAt);return{id:e.id,createdAt:Number.isFinite(t)?t:0,sessionName:e.name,cwd:e.cwd}}function i(t,n){let r=()=>{let t=n.snapshot;if(!t)throw new e.PiServerError(`not_found`,`The session has not published a snapshot yet`);return t};return{snapshot:r,getPhase:()=>r().phase,prompt:async e=>void await n.prompt(e.text),steer:async e=>void await n.steer(e.text),abort:async()=>void await n.abort(),setModel:async e=>void await n.setModel(e),setThinking:async e=>void await n.setThinking(e),subscribe(e){let t=n.subscribe(()=>e({type:`snapshot`})),r=n.onEvent(t=>{t.type===`session_progress`&&e({type:`progress`,progress:t.progress})});return()=>{t(),r()}},async dispose(){await n.dispose().catch(()=>void 0),await t.dispose().catch(()=>void 0)}}}function a(a){let o=t=>{let n=a.records().find(e=>e.id===t);if(!n)throw new e.PiServerError(`not_found`,`No session ${t}`);return n},s=async r=>{if(!r.protocolSocketPath)throw new e.PiServerError(`not_implemented`,`Session ${r.id} predates the protocol socket`);let o=new t.PiClient({transportFactory:(0,n.createUnixTransportFactory)({path:r.protocolSocketPath}),onListenerError:e=>a.onNotice?.(`session ${r.id} listener error: ${e.message}`)});await o.connect();try{return i(o,await o.attachSession(r.id))}catch(e){throw await o.dispose().catch(()=>void 0),e}};return{listSessions:async()=>a.records().map(r),listModels:async()=>[],async createSession(t){if(!t.cwd)throw new e.PiServerError(`invalid_request`,`A new session needs a working directory`);let n=await a.spawn({cwd:t.cwd,...t.name?{name:t.name}:{}});if(!n.ok)throw new e.PiServerError(n.code===`invalid_request`?`invalid_request`:`not_found`,n.error);let r=Date.now()+1e4;for(;;){let t=a.records().find(e=>e.id===n.sessionId);if(t?.protocolSocketPath)return s(t);if(Date.now()>=r)throw new e.PiServerError(`not_found`,`Session ${n.sessionId} never published a protocol socket`);await new Promise(e=>setTimeout(e,100))}},openSession:async e=>s(o(e))}}exports.createPiHubService=a;
|
|
2
|
+
//# sourceMappingURL=piHubService.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piHubService.cjs","names":["PiServerError","PiClient","createUnixTransportFactory"],"sources":["../../../src/adapters/piHubService.ts"],"sourcesContent":["import { PiClient } from '@earendil-works/pi-client';\nimport { createUnixTransportFactory } from '@earendil-works/pi-client/unix';\nimport type { SessionLease } from '@earendil-works/pi-client';\nimport type { ModelMetadata, SessionMetadata } from '@earendil-works/pi-protocol';\nimport { PiServerError } from '@earendil-works/pi-server';\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime } from '@earendil-works/pi-server';\nimport type { SessionRecord } from '../types/registry.ts';\nimport type { SpawnSessionInput, SpawnOutcome } from './serverSpawner.ts';\n\n/** How long a created session has to publish its record before the client is told it failed. */\nconst CREATE_TIMEOUT_MS = 10_000;\nconst CREATE_POLL_MS = 100;\n\nexport interface PiHubServiceOptions {\n /** Every session the registry currently lists. */\n records(): readonly SessionRecord[];\n spawn(input: SpawnSessionInput): Promise<SpawnOutcome>;\n onNotice?: (message: string) => void;\n}\n\nfunction toMetadata(record: SessionRecord): SessionMetadata {\n const createdAt = Date.parse(record.createdAt);\n return {\n id: record.id,\n createdAt: Number.isFinite(createdAt) ? createdAt : 0,\n sessionName: record.name,\n cwd: record.cwd,\n };\n}\n\n/**\n * Presents one live lease on a session's own server as a runtime.\n *\n * The hub owns no session state of its own here. It holds a connection open\n * and forwards, so the snapshot a browser reads is the one the session server\n * published rather than a copy the hub reduced for itself.\n */\nfunction leaseRuntime(client: PiClient, lease: SessionLease): PiSessionRuntime {\n const snapshot = () => {\n const current = lease.snapshot;\n if (!current) throw new PiServerError('not_found', 'The session has not published a snapshot yet');\n return current;\n };\n return {\n snapshot,\n getPhase: () => snapshot().phase,\n prompt: async (input) => void (await lease.prompt(input.text)),\n steer: async (input) => void (await lease.steer(input.text)),\n abort: async () => void (await lease.abort()),\n setModel: async (model) => void (await lease.setModel(model)),\n setThinking: async (level) => void (await lease.setThinking(level)),\n subscribe(listener) {\n const stopSnapshots = lease.subscribe(() => listener({ type: 'snapshot' }));\n const stopEvents = lease.onEvent((event) => {\n if (event.type === 'session_progress') listener({ type: 'progress', progress: event.progress });\n });\n return () => {\n stopSnapshots();\n stopEvents();\n };\n },\n async dispose() {\n // The upstream connection belongs to this hold, so both go together and\n // the session server sees the detach it is waiting for.\n await lease.dispose().catch(() => undefined);\n await client.dispose().catch(() => undefined);\n },\n };\n}\n\n/**\n * The cockpit's own protocol server, backed by every running session.\n *\n * Each session already serves the protocol on its own socket, so the hub does\n * not reinterpret any of it: it lists what the registry knows and dials the\n * session a browser asks for. That keeps one vocabulary from the agent all the\n * way to the page.\n */\nexport function createPiHubService(options: PiHubServiceOptions): PiServerService {\n const find = (sessionId: string): SessionRecord => {\n const record = options.records().find((candidate) => candidate.id === sessionId);\n if (!record) throw new PiServerError('not_found', `No session ${sessionId}`);\n return record;\n };\n\n const open = async (record: SessionRecord): Promise<PiSessionRuntime> => {\n if (!record.protocolSocketPath) {\n throw new PiServerError('not_implemented', `Session ${record.id} predates the protocol socket`);\n }\n const client = new PiClient({\n transportFactory: createUnixTransportFactory({ path: record.protocolSocketPath }),\n onListenerError: (error) => options.onNotice?.(`session ${record.id} listener error: ${error.message}`),\n });\n await client.connect();\n try {\n return leaseRuntime(client, await client.attachSession(record.id));\n } catch (error) {\n await client.dispose().catch(() => undefined);\n throw error;\n }\n };\n\n return {\n listSessions: async () => options.records().map(toMetadata),\n listModels: async (): Promise<ModelMetadata[]> => [],\n\n async createSession(create: CreateSessionOptions) {\n if (!create.cwd) throw new PiServerError('invalid_request', 'A new session needs a working directory');\n const outcome = await options.spawn({ cwd: create.cwd, ...(create.name ? { name: create.name } : {}) });\n if (!outcome.ok) {\n throw new PiServerError(outcome.code === 'invalid_request' ? 'invalid_request' : 'not_found', outcome.error);\n }\n // The spawner returns once the record lands, but the session's protocol\n // socket is published a moment later; waiting here spares the caller a\n // retry loop it has no way to bound.\n const deadline = Date.now() + CREATE_TIMEOUT_MS;\n for (;;) {\n const record = options.records().find((candidate) => candidate.id === outcome.sessionId);\n if (record?.protocolSocketPath) return open(record);\n if (Date.now() >= deadline) {\n throw new PiServerError('not_found', `Session ${outcome.sessionId} never published a protocol socket`);\n }\n await new Promise((resolve) => setTimeout(resolve, CREATE_POLL_MS));\n }\n },\n\n openSession: async (sessionId: string) => open(find(sessionId)),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{PiServerError as e}from"@earendil-works/pi-server";import{PiClient as t}from"@earendil-works/pi-client";import{createUnixTransportFactory as n}from"@earendil-works/pi-client/unix";function r(e){let t=Date.parse(e.createdAt);return{id:e.id,createdAt:Number.isFinite(t)?t:0,sessionName:e.name,cwd:e.cwd}}function i(t,n){let r=()=>{let t=n.snapshot;if(!t)throw new e(`not_found`,`The session has not published a snapshot yet`);return t};return{snapshot:r,getPhase:()=>r().phase,prompt:async e=>void await n.prompt(e.text),steer:async e=>void await n.steer(e.text),abort:async()=>void await n.abort(),setModel:async e=>void await n.setModel(e),setThinking:async e=>void await n.setThinking(e),subscribe(e){let t=n.subscribe(()=>e({type:`snapshot`})),r=n.onEvent(t=>{t.type===`session_progress`&&e({type:`progress`,progress:t.progress})});return()=>{t(),r()}},async dispose(){await n.dispose().catch(()=>void 0),await t.dispose().catch(()=>void 0)}}}function a(a){let o=t=>{let n=a.records().find(e=>e.id===t);if(!n)throw new e(`not_found`,`No session ${t}`);return n},s=async r=>{if(!r.protocolSocketPath)throw new e(`not_implemented`,`Session ${r.id} predates the protocol socket`);let o=new t({transportFactory:n({path:r.protocolSocketPath}),onListenerError:e=>a.onNotice?.(`session ${r.id} listener error: ${e.message}`)});await o.connect();try{return i(o,await o.attachSession(r.id))}catch(e){throw await o.dispose().catch(()=>void 0),e}};return{listSessions:async()=>a.records().map(r),listModels:async()=>[],async createSession(t){if(!t.cwd)throw new e(`invalid_request`,`A new session needs a working directory`);let n=await a.spawn({cwd:t.cwd,...t.name?{name:t.name}:{}});if(!n.ok)throw new e(n.code===`invalid_request`?`invalid_request`:`not_found`,n.error);let r=Date.now()+1e4;for(;;){let t=a.records().find(e=>e.id===n.sessionId);if(t?.protocolSocketPath)return s(t);if(Date.now()>=r)throw new e(`not_found`,`Session ${n.sessionId} never published a protocol socket`);await new Promise(e=>setTimeout(e,100))}},openSession:async e=>s(o(e))}}export{a as createPiHubService};
|
|
2
|
+
//# sourceMappingURL=piHubService.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piHubService.mjs","names":[],"sources":["../../../src/adapters/piHubService.ts"],"sourcesContent":["import { PiClient } from '@earendil-works/pi-client';\nimport { createUnixTransportFactory } from '@earendil-works/pi-client/unix';\nimport type { SessionLease } from '@earendil-works/pi-client';\nimport type { ModelMetadata, SessionMetadata } from '@earendil-works/pi-protocol';\nimport { PiServerError } from '@earendil-works/pi-server';\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime } from '@earendil-works/pi-server';\nimport type { SessionRecord } from '../types/registry.ts';\nimport type { SpawnSessionInput, SpawnOutcome } from './serverSpawner.ts';\n\n/** How long a created session has to publish its record before the client is told it failed. */\nconst CREATE_TIMEOUT_MS = 10_000;\nconst CREATE_POLL_MS = 100;\n\nexport interface PiHubServiceOptions {\n /** Every session the registry currently lists. */\n records(): readonly SessionRecord[];\n spawn(input: SpawnSessionInput): Promise<SpawnOutcome>;\n onNotice?: (message: string) => void;\n}\n\nfunction toMetadata(record: SessionRecord): SessionMetadata {\n const createdAt = Date.parse(record.createdAt);\n return {\n id: record.id,\n createdAt: Number.isFinite(createdAt) ? createdAt : 0,\n sessionName: record.name,\n cwd: record.cwd,\n };\n}\n\n/**\n * Presents one live lease on a session's own server as a runtime.\n *\n * The hub owns no session state of its own here. It holds a connection open\n * and forwards, so the snapshot a browser reads is the one the session server\n * published rather than a copy the hub reduced for itself.\n */\nfunction leaseRuntime(client: PiClient, lease: SessionLease): PiSessionRuntime {\n const snapshot = () => {\n const current = lease.snapshot;\n if (!current) throw new PiServerError('not_found', 'The session has not published a snapshot yet');\n return current;\n };\n return {\n snapshot,\n getPhase: () => snapshot().phase,\n prompt: async (input) => void (await lease.prompt(input.text)),\n steer: async (input) => void (await lease.steer(input.text)),\n abort: async () => void (await lease.abort()),\n setModel: async (model) => void (await lease.setModel(model)),\n setThinking: async (level) => void (await lease.setThinking(level)),\n subscribe(listener) {\n const stopSnapshots = lease.subscribe(() => listener({ type: 'snapshot' }));\n const stopEvents = lease.onEvent((event) => {\n if (event.type === 'session_progress') listener({ type: 'progress', progress: event.progress });\n });\n return () => {\n stopSnapshots();\n stopEvents();\n };\n },\n async dispose() {\n // The upstream connection belongs to this hold, so both go together and\n // the session server sees the detach it is waiting for.\n await lease.dispose().catch(() => undefined);\n await client.dispose().catch(() => undefined);\n },\n };\n}\n\n/**\n * The cockpit's own protocol server, backed by every running session.\n *\n * Each session already serves the protocol on its own socket, so the hub does\n * not reinterpret any of it: it lists what the registry knows and dials the\n * session a browser asks for. That keeps one vocabulary from the agent all the\n * way to the page.\n */\nexport function createPiHubService(options: PiHubServiceOptions): PiServerService {\n const find = (sessionId: string): SessionRecord => {\n const record = options.records().find((candidate) => candidate.id === sessionId);\n if (!record) throw new PiServerError('not_found', `No session ${sessionId}`);\n return record;\n };\n\n const open = async (record: SessionRecord): Promise<PiSessionRuntime> => {\n if (!record.protocolSocketPath) {\n throw new PiServerError('not_implemented', `Session ${record.id} predates the protocol socket`);\n }\n const client = new PiClient({\n transportFactory: createUnixTransportFactory({ path: record.protocolSocketPath }),\n onListenerError: (error) => options.onNotice?.(`session ${record.id} listener error: ${error.message}`),\n });\n await client.connect();\n try {\n return leaseRuntime(client, await client.attachSession(record.id));\n } catch (error) {\n await client.dispose().catch(() => undefined);\n throw error;\n }\n };\n\n return {\n listSessions: async () => options.records().map(toMetadata),\n listModels: async (): Promise<ModelMetadata[]> => [],\n\n async createSession(create: CreateSessionOptions) {\n if (!create.cwd) throw new PiServerError('invalid_request', 'A new session needs a working directory');\n const outcome = await options.spawn({ cwd: create.cwd, ...(create.name ? { name: create.name } : {}) });\n if (!outcome.ok) {\n throw new PiServerError(outcome.code === 'invalid_request' ? 'invalid_request' : 'not_found', outcome.error);\n }\n // The spawner returns once the record lands, but the session's protocol\n // socket is published a moment later; waiting here spares the caller a\n // retry loop it has no way to bound.\n const deadline = Date.now() + CREATE_TIMEOUT_MS;\n for (;;) {\n const record = options.records().find((candidate) => candidate.id === outcome.sessionId);\n if (record?.protocolSocketPath) return open(record);\n if (Date.now() >= deadline) {\n throw new PiServerError('not_found', `Session ${outcome.sessionId} never published a protocol socket`);\n }\n await new Promise((resolve) => setTimeout(resolve, CREATE_POLL_MS));\n }\n },\n\n openSession: async (sessionId: string) => open(find(sessionId)),\n };\n}\n"],"mappings":"2LAoBA,SAAS,EAAW,EAAwC,CAC1D,IAAM,EAAY,KAAK,MAAM,EAAO,SAAS,EAC7C,MAAO,CACL,GAAI,EAAO,GACX,UAAW,OAAO,SAAS,CAAS,EAAI,EAAY,EACpD,YAAa,EAAO,KACpB,IAAK,EAAO,GACd,CACF,CASA,SAAS,EAAa,EAAkB,EAAuC,CAC7E,IAAM,MAAiB,CACrB,IAAM,EAAU,EAAM,SACtB,GAAI,CAAC,EAAS,MAAM,IAAI,EAAc,YAAa,8CAA8C,EACjG,OAAO,CACT,EACA,MAAO,CACL,WACA,aAAgB,EAAS,CAAC,CAAC,MAC3B,OAAQ,KAAO,IAAU,KAAM,MAAM,EAAM,OAAO,EAAM,IAAI,EAC5D,MAAO,KAAO,IAAU,KAAM,MAAM,EAAM,MAAM,EAAM,IAAI,EAC1D,MAAO,SAAY,KAAM,MAAM,EAAM,MAAM,EAC3C,SAAU,KAAO,IAAU,KAAM,MAAM,EAAM,SAAS,CAAK,EAC3D,YAAa,KAAO,IAAU,KAAM,MAAM,EAAM,YAAY,CAAK,EACjE,UAAU,EAAU,CAClB,IAAM,EAAgB,EAAM,cAAgB,EAAS,CAAE,KAAM,UAAW,CAAC,CAAC,EACpE,EAAa,EAAM,QAAS,GAAU,CACtC,EAAM,OAAS,oBAAoB,EAAS,CAAE,KAAM,WAAY,SAAU,EAAM,QAAS,CAAC,CAChG,CAAC,EACD,UAAa,CACX,EAAc,EACd,EAAW,CACb,CACF,EACA,MAAM,SAAU,CAGd,MAAM,EAAM,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,EAC3C,MAAM,EAAO,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,CAC9C,CACF,CACF,CAUA,SAAgB,EAAmB,EAA+C,CAChF,IAAM,EAAQ,GAAqC,CACjD,IAAM,EAAS,EAAQ,QAAQ,CAAC,CAAC,KAAM,GAAc,EAAU,KAAO,CAAS,EAC/E,GAAI,CAAC,EAAQ,MAAM,IAAI,EAAc,YAAa,cAAc,GAAW,EAC3E,OAAO,CACT,EAEM,EAAO,KAAO,IAAqD,CACvE,GAAI,CAAC,EAAO,mBACV,MAAM,IAAI,EAAc,kBAAmB,WAAW,EAAO,GAAG,8BAA8B,EAEhG,IAAM,EAAS,IAAI,EAAS,CAC1B,iBAAkB,EAA2B,CAAE,KAAM,EAAO,kBAAmB,CAAC,EAChF,gBAAkB,GAAU,EAAQ,WAAW,WAAW,EAAO,GAAG,mBAAmB,EAAM,SAAS,CACxG,CAAC,EACD,MAAM,EAAO,QAAQ,EACrB,GAAI,CACF,OAAO,EAAa,EAAQ,MAAM,EAAO,cAAc,EAAO,EAAE,CAAC,CACnE,OAAS,EAAO,CAEd,MADA,MAAM,EAAO,QAAQ,CAAC,CAAC,UAAY,IAAA,EAAS,EACtC,CACR,CACF,EAEA,MAAO,CACL,aAAc,SAAY,EAAQ,QAAQ,CAAC,CAAC,IAAI,CAAU,EAC1D,WAAY,SAAsC,CAAC,EAEnD,MAAM,cAAc,EAA8B,CAChD,GAAI,CAAC,EAAO,IAAK,MAAM,IAAI,EAAc,kBAAmB,yCAAyC,EACrG,IAAM,EAAU,MAAM,EAAQ,MAAM,CAAE,IAAK,EAAO,IAAK,GAAI,EAAO,KAAO,CAAE,KAAM,EAAO,IAAK,EAAI,CAAC,CAAG,CAAC,EACtG,GAAI,CAAC,EAAQ,GACX,MAAM,IAAI,EAAc,EAAQ,OAAS,kBAAoB,kBAAoB,YAAa,EAAQ,KAAK,EAK7G,IAAM,EAAW,KAAK,IAAI,EAAI,IAC9B,OAAS,CACP,IAAM,EAAS,EAAQ,QAAQ,CAAC,CAAC,KAAM,GAAc,EAAU,KAAO,EAAQ,SAAS,EACvF,GAAI,GAAQ,mBAAoB,OAAO,EAAK,CAAM,EAClD,GAAI,KAAK,IAAI,GAAK,EAChB,MAAM,IAAI,EAAc,YAAa,WAAW,EAAQ,UAAU,mCAAmC,EAEvG,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,GAAc,CAAC,CACpE,CACF,EAEA,YAAa,KAAO,IAAsB,EAAK,EAAK,CAAS,CAAC,CAChE,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e={}){let t,n=new Set;return{address:`websocket`,async start(e){t=e},async close(){t=void 0;for(let t of n)try{t.close()}catch(t){e.onError?.(t instanceof Error?t:Error(String(t)))}n.clear()},accept(e){if(!t){e.close();return}n.add(e);let r=!1,i=t({get closed(){return r||e.readyState!==void 0&&e.readyState!==1},async send(t){e.send(t)},close(t){t&&e.send(t),r=!0,n.delete(e),e.close()}});return{onData:e=>i.onData(e),onClose:()=>{r=!0,n.delete(e),i.onClose()},onError:t=>{r=!0,n.delete(e),i.onError(t)}}}}}exports.createPiWebSocketListener=e;
|
|
2
|
+
//# sourceMappingURL=piWebSocketListener.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piWebSocketListener.cjs","names":[],"sources":["../../../src/adapters/piWebSocketListener.ts"],"sourcesContent":["import type { PiServerListener } from '@earendil-works/pi-server';\n\n// pi-server names these in PiServerListener's signature but does not export\n// them from its root, so they are recovered from the type that does.\ntype ByteConnectionAcceptor = Parameters<PiServerListener['start']>[0];\ntype ByteConnection = Parameters<ByteConnectionAcceptor>[0];\ntype ByteConnectionHandler = ReturnType<ByteConnectionAcceptor>;\n\n/** The minimum of a WebSocket this listener drives, so a test needs no real socket. */\nexport interface ListenerSocket {\n send(data: ArrayBufferLike | Uint8Array): void;\n close(): void;\n readonly readyState?: number;\n}\n\nconst OPEN = 1;\n\nexport interface PiWebSocketListener extends PiServerListener {\n /**\n * Hands an already-upgraded socket to the server.\n *\n * The transport authenticates during the HTTP upgrade, so a socket only\n * reaches here once the request that opened it was allowed.\n */\n accept(socket: ListenerSocket): ByteConnectionHandler | undefined;\n}\n\n/**\n * Feeds PiServer the browser's connections.\n *\n * Pi ships only a Unix listener, where the file mode is the access control.\n * A browser cannot open one, so the cockpit terminates the protocol over a\n * WebSocket instead and keeps the same contract: bytes in order, closure\n * reported once, and no protocol knowledge in the transport itself.\n */\nexport function createPiWebSocketListener(options: { onError?: (error: Error) => void } = {}): PiWebSocketListener {\n let acceptor: ByteConnectionAcceptor | undefined;\n const open = new Set<ListenerSocket>();\n\n return {\n address: 'websocket',\n\n async start(accept) {\n acceptor = accept;\n },\n\n async close() {\n acceptor = undefined;\n for (const socket of open) {\n try {\n socket.close();\n } catch (error) {\n options.onError?.(error instanceof Error ? error : new Error(String(error)));\n }\n }\n open.clear();\n },\n\n accept(socket) {\n // A socket that arrives before start, or after close, has no server to\n // talk to. Closing it is the honest answer.\n if (!acceptor) {\n socket.close();\n return undefined;\n }\n open.add(socket);\n let closed = false;\n const connection: ByteConnection = {\n get closed() {\n return closed || (socket.readyState !== undefined && socket.readyState !== OPEN);\n },\n async send(chunk) {\n socket.send(chunk);\n },\n close(finalChunk) {\n if (finalChunk) socket.send(finalChunk);\n closed = true;\n open.delete(socket);\n socket.close();\n },\n };\n const handler = acceptor(connection);\n return {\n onData: (chunk) => handler.onData(chunk),\n onClose: () => {\n closed = true;\n open.delete(socket);\n handler.onClose();\n },\n onError: (error) => {\n closed = true;\n open.delete(socket);\n handler.onError(error);\n },\n };\n },\n };\n}\n"],"mappings":"AAmCA,SAAgB,EAA0B,EAAgD,CAAC,EAAwB,CACjH,IAAI,EACE,EAAO,IAAI,IAEjB,MAAO,CACL,QAAS,YAET,MAAM,MAAM,EAAQ,CAClB,EAAW,CACb,EAEA,MAAM,OAAQ,CACZ,EAAW,IAAA,GACX,IAAK,IAAM,KAAU,EACnB,GAAI,CACF,EAAO,MAAM,CACf,OAAS,EAAO,CACd,EAAQ,UAAU,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CAAC,CAC7E,CAEF,EAAK,MAAM,CACb,EAEA,OAAO,EAAQ,CAGb,GAAI,CAAC,EAAU,CACb,EAAO,MAAM,EACb,MACF,CACA,EAAK,IAAI,CAAM,EACf,IAAI,EAAS,GAeP,EAAU,EAAS,CAbvB,IAAI,QAAS,CACX,OAAO,GAAW,EAAO,aAAe,IAAA,IAAa,EAAO,aAAe,CAC7E,EACA,MAAM,KAAK,EAAO,CAChB,EAAO,KAAK,CAAK,CACnB,EACA,MAAM,EAAY,CACZ,GAAY,EAAO,KAAK,CAAU,EACtC,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAO,MAAM,CACf,CAEgC,CAAC,EACnC,MAAO,CACL,OAAS,GAAU,EAAQ,OAAO,CAAK,EACvC,YAAe,CACb,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAQ,QAAQ,CAClB,EACA,QAAU,GAAU,CAClB,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAQ,QAAQ,CAAK,CACvB,CACF,CACF,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e={}){let t,n=new Set;return{address:`websocket`,async start(e){t=e},async close(){t=void 0;for(let t of n)try{t.close()}catch(t){e.onError?.(t instanceof Error?t:Error(String(t)))}n.clear()},accept(e){if(!t){e.close();return}n.add(e);let r=!1,i=t({get closed(){return r||e.readyState!==void 0&&e.readyState!==1},async send(t){e.send(t)},close(t){t&&e.send(t),r=!0,n.delete(e),e.close()}});return{onData:e=>i.onData(e),onClose:()=>{r=!0,n.delete(e),i.onClose()},onError:t=>{r=!0,n.delete(e),i.onError(t)}}}}}export{e as createPiWebSocketListener};
|
|
2
|
+
//# sourceMappingURL=piWebSocketListener.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piWebSocketListener.mjs","names":[],"sources":["../../../src/adapters/piWebSocketListener.ts"],"sourcesContent":["import type { PiServerListener } from '@earendil-works/pi-server';\n\n// pi-server names these in PiServerListener's signature but does not export\n// them from its root, so they are recovered from the type that does.\ntype ByteConnectionAcceptor = Parameters<PiServerListener['start']>[0];\ntype ByteConnection = Parameters<ByteConnectionAcceptor>[0];\ntype ByteConnectionHandler = ReturnType<ByteConnectionAcceptor>;\n\n/** The minimum of a WebSocket this listener drives, so a test needs no real socket. */\nexport interface ListenerSocket {\n send(data: ArrayBufferLike | Uint8Array): void;\n close(): void;\n readonly readyState?: number;\n}\n\nconst OPEN = 1;\n\nexport interface PiWebSocketListener extends PiServerListener {\n /**\n * Hands an already-upgraded socket to the server.\n *\n * The transport authenticates during the HTTP upgrade, so a socket only\n * reaches here once the request that opened it was allowed.\n */\n accept(socket: ListenerSocket): ByteConnectionHandler | undefined;\n}\n\n/**\n * Feeds PiServer the browser's connections.\n *\n * Pi ships only a Unix listener, where the file mode is the access control.\n * A browser cannot open one, so the cockpit terminates the protocol over a\n * WebSocket instead and keeps the same contract: bytes in order, closure\n * reported once, and no protocol knowledge in the transport itself.\n */\nexport function createPiWebSocketListener(options: { onError?: (error: Error) => void } = {}): PiWebSocketListener {\n let acceptor: ByteConnectionAcceptor | undefined;\n const open = new Set<ListenerSocket>();\n\n return {\n address: 'websocket',\n\n async start(accept) {\n acceptor = accept;\n },\n\n async close() {\n acceptor = undefined;\n for (const socket of open) {\n try {\n socket.close();\n } catch (error) {\n options.onError?.(error instanceof Error ? error : new Error(String(error)));\n }\n }\n open.clear();\n },\n\n accept(socket) {\n // A socket that arrives before start, or after close, has no server to\n // talk to. Closing it is the honest answer.\n if (!acceptor) {\n socket.close();\n return undefined;\n }\n open.add(socket);\n let closed = false;\n const connection: ByteConnection = {\n get closed() {\n return closed || (socket.readyState !== undefined && socket.readyState !== OPEN);\n },\n async send(chunk) {\n socket.send(chunk);\n },\n close(finalChunk) {\n if (finalChunk) socket.send(finalChunk);\n closed = true;\n open.delete(socket);\n socket.close();\n },\n };\n const handler = acceptor(connection);\n return {\n onData: (chunk) => handler.onData(chunk),\n onClose: () => {\n closed = true;\n open.delete(socket);\n handler.onClose();\n },\n onError: (error) => {\n closed = true;\n open.delete(socket);\n handler.onError(error);\n },\n };\n },\n };\n}\n"],"mappings":"AAmCA,SAAgB,EAA0B,EAAgD,CAAC,EAAwB,CACjH,IAAI,EACE,EAAO,IAAI,IAEjB,MAAO,CACL,QAAS,YAET,MAAM,MAAM,EAAQ,CAClB,EAAW,CACb,EAEA,MAAM,OAAQ,CACZ,EAAW,IAAA,GACX,IAAK,IAAM,KAAU,EACnB,GAAI,CACF,EAAO,MAAM,CACf,OAAS,EAAO,CACd,EAAQ,UAAU,aAAiB,MAAQ,EAAY,MAAM,OAAO,CAAK,CAAC,CAAC,CAC7E,CAEF,EAAK,MAAM,CACb,EAEA,OAAO,EAAQ,CAGb,GAAI,CAAC,EAAU,CACb,EAAO,MAAM,EACb,MACF,CACA,EAAK,IAAI,CAAM,EACf,IAAI,EAAS,GAeP,EAAU,EAAS,CAbvB,IAAI,QAAS,CACX,OAAO,GAAW,EAAO,aAAe,IAAA,IAAa,EAAO,aAAe,CAC7E,EACA,MAAM,KAAK,EAAO,CAChB,EAAO,KAAK,CAAK,CACnB,EACA,MAAM,EAAY,CACZ,GAAY,EAAO,KAAK,CAAU,EACtC,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAO,MAAM,CACf,CAEgC,CAAC,EACnC,MAAO,CACL,OAAS,GAAU,EAAQ,OAAO,CAAK,EACvC,YAAe,CACb,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAQ,QAAQ,CAClB,EACA,QAAU,GAAU,CAClB,EAAS,GACT,EAAK,OAAO,CAAM,EAClB,EAAQ,QAAQ,CAAK,CACvB,CACF,CACF,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/loginFlow.cjs");let n=require("node:crypto");n=e.__toESM(n,1);function r(e){return e instanceof Error?e.message:String(e)}async function i(){let{ModelRuntime:e}=await import(`@earendil-works/pi-coding-agent`);return e.create()}function a(e){let t=[],{oauth:n,apiKey:r}=e.auth;return n&&t.push({type:`oauth`,label:n.loginLabel??n.name}),r?.login&&t.push({type:`api_key`,label:r.name}),t}function o(e,t){let n={id:t.id,name:t.name,methods:a(t)},r=e.getProviderAuthStatus(t.id);return r.configured&&(n.authenticated={type:e.isUsingOAuth(t.id)?`oauth`:`api_key`,source:r.label??r.source}),n}function s(e={}){let s=e.onNotice??(()=>{}),c=e.runtime??i,l=new Map,u,d=()=>(u??=c().catch(e=>{throw u=void 0,e}),u),f=(e,t)=>e.getProviders().find(e=>e.id===t),p=e=>[...l.values()].find(t=>t.providerId===e&&t.snapshot().status===`running`);return{async listProviders(){let e=await d();try{await e.refresh({allowNetwork:!1})}catch(e){s(`provider auth refresh failed: ${r(e)}`)}return e.getProviders().map(t=>o(e,t)).sort((e,t)=>e.name.localeCompare(t.name))},async startLogin(e,i){let o=await d(),c=f(o,e);if(!c)return{ok:!1,code:`unknown_provider`,error:`Unknown provider: ${e}`};if(!a(c).some(e=>e.type===i))return{ok:!1,code:`unsupported_method`,error:`${c.name} has no ${i} login.`};if(p(e))return{ok:!1,code:`busy`,error:`A login for ${c.name} is already in progress.`};for(let[e,t]of l)t.snapshot().status!==`running`&&l.delete(e);let u=t.createLoginFlow({id:n.default.randomUUID(),providerId:e,providerName:c.name,type:i});return l.set(u.id,u),o.login(e,i,u.interaction).then(()=>{u.settle({ok:!0}),s(`signed in to ${c.name} (${i})`)},e=>{u.settle({ok:!1,error:r(e)})}),{ok:!0,flow:u.snapshot()}},getLogin(e){return l.get(e)?.snapshot()},answerLogin(e,t,n){let r=l.get(e);return r?r.answer(t,n)?`answered`:`not_waiting`:`unknown_flow`},cancelLogin(e){let t=l.get(e);if(t)return t.cancel(),t.snapshot()},async logout(e){let t=await d(),n=f(t,e);if(!n)return{ok:!1,code:`unknown_provider`,error:`Unknown provider: ${e}`};try{await t.logout(e,{signal:AbortSignal.timeout(15e3)})}catch(e){return{ok:!1,code:`runtime`,error:r(e)}}return s(`signed out of ${n.name}`),{ok:!0}},close(){for(let e of l.values())e.cancel();l.clear()}}}exports.createProviderAuth=s;
|
|
2
|
+
//# sourceMappingURL=providerAuth.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providerAuth.cjs","names":["createLoginFlow","crypto"],"sources":["../../../src/adapters/providerAuth.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport { createLoginFlow, type LoginFlow } from '../services/loginFlow.ts';\nimport type {\n AuthMethodType,\n AuthRuntime,\n AuthRuntimeProvider,\n LoginFlowSnapshot,\n ProviderAuthMethod,\n ProviderAuthSummary,\n} from '../types/auth.ts';\n\n/** Matches the timeout Pi's own /logout gives the credential store. */\nconst LOGOUT_TIMEOUT_MS = 15_000;\n\nexport type StartLoginOutcome =\n | { ok: true; flow: LoginFlowSnapshot }\n | { ok: false; code: 'unknown_provider' | 'unsupported_method' | 'busy'; error: string };\n\nexport type LogoutOutcome = { ok: true } | { ok: false; code: 'unknown_provider' | 'runtime'; error: string };\n\nexport type AnswerOutcome = 'answered' | 'unknown_flow' | 'not_waiting';\n\n/** Provider credentials as the settings page sees them, backed by one Pi runtime. */\nexport interface ProviderAuth {\n /** Every provider Pi knows, with its current auth state; re-read from disk each call. */\n listProviders(): Promise<ProviderAuthSummary[]>;\n startLogin(providerId: string, type: AuthMethodType): Promise<StartLoginOutcome>;\n getLogin(flowId: string): LoginFlowSnapshot | undefined;\n answerLogin(flowId: string, promptId: string, value: string): AnswerOutcome;\n /** Cancels the flow and returns how it stands, or undefined for an unknown flow. */\n cancelLogin(flowId: string): LoginFlowSnapshot | undefined;\n logout(providerId: string): Promise<LogoutOutcome>;\n /** Abandons every running flow; the hub is shutting down. */\n close(): void;\n}\n\nexport interface ProviderAuthOptions {\n /** Test seam over Pi's ModelRuntime; the default loads the real one over auth.json. */\n runtime?: () => Promise<AuthRuntime>;\n onNotice?: (message: string) => void;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Pi's runtime, imported on first use: the hub should come up without paying\n * for the provider catalog, and a machine without Pi installed still serves\n * sessions, just not the providers page.\n */\nasync function loadPiRuntime(): Promise<AuthRuntime> {\n const { ModelRuntime } = await import('@earendil-works/pi-coding-agent');\n return ModelRuntime.create();\n}\n\n/** The methods a page can start, in the order the TUI's selector lists them. */\nfunction methodsOf(provider: AuthRuntimeProvider): ProviderAuthMethod[] {\n const methods: ProviderAuthMethod[] = [];\n const { oauth, apiKey } = provider.auth;\n if (oauth) methods.push({ type: 'oauth', label: oauth.loginLabel ?? oauth.name });\n if (apiKey?.login) methods.push({ type: 'api_key', label: apiKey.name });\n return methods;\n}\n\n/** Mirrors the status the TUI's login selector shows beside each provider. */\nfunction summarize(runtime: AuthRuntime, provider: AuthRuntimeProvider): ProviderAuthSummary {\n const summary: ProviderAuthSummary = { id: provider.id, name: provider.name, methods: methodsOf(provider) };\n const status = runtime.getProviderAuthStatus(provider.id);\n if (status.configured) {\n summary.authenticated = {\n type: runtime.isUsingOAuth(provider.id) ? 'oauth' : 'api_key',\n source: status.label ?? status.source,\n };\n }\n return summary;\n}\n\nexport function createProviderAuth(options: ProviderAuthOptions = {}): ProviderAuth {\n const notice = options.onNotice ?? ((): void => {});\n const load = options.runtime ?? loadPiRuntime;\n const flows = new Map<string, LoginFlow>();\n let loading: Promise<AuthRuntime> | undefined;\n\n // A failed load is forgotten so the next request tries again rather than\n // pinning the page on a startup error.\n const runtime = (): Promise<AuthRuntime> => {\n loading ??= load().catch((error: unknown) => {\n loading = undefined;\n throw error;\n });\n return loading;\n };\n\n const findProvider = (current: AuthRuntime, providerId: string): AuthRuntimeProvider | undefined =>\n current.getProviders().find((provider) => provider.id === providerId);\n\n const runningFor = (providerId: string): LoginFlow | undefined =>\n [...flows.values()].find((flow) => flow.providerId === providerId && flow.snapshot().status === 'running');\n\n return {\n async listProviders() {\n const current = await runtime();\n // Another client may have stored a credential since the last look (a\n // TUI /login, a hand-edited auth.json); the stale snapshot still lists\n // on a failed refresh, which the notice explains.\n try {\n await current.refresh({ allowNetwork: false });\n } catch (error) {\n notice(`provider auth refresh failed: ${describe(error)}`);\n }\n return current\n .getProviders()\n .map((provider) => summarize(current, provider))\n .sort((left, right) => left.name.localeCompare(right.name));\n },\n\n async startLogin(providerId, type) {\n const current = await runtime();\n const provider = findProvider(current, providerId);\n if (!provider) return { ok: false, code: 'unknown_provider', error: `Unknown provider: ${providerId}` };\n if (!methodsOf(provider).some((method) => method.type === type)) {\n return { ok: false, code: 'unsupported_method', error: `${provider.name} has no ${type} login.` };\n }\n if (runningFor(providerId)) {\n return { ok: false, code: 'busy', error: `A login for ${provider.name} is already in progress.` };\n }\n // Finished flows live until the next start: long enough for the page to\n // read how they ended, short enough that the map never grows.\n for (const [id, flow] of flows) if (flow.snapshot().status !== 'running') flows.delete(id);\n\n const flow = createLoginFlow({ id: crypto.randomUUID(), providerId, providerName: provider.name, type });\n flows.set(flow.id, flow);\n current.login(providerId, type, flow.interaction).then(\n () => {\n flow.settle({ ok: true });\n notice(`signed in to ${provider.name} (${type})`);\n },\n (error: unknown) => {\n flow.settle({ ok: false, error: describe(error) });\n },\n );\n return { ok: true, flow: flow.snapshot() };\n },\n\n getLogin(flowId) {\n return flows.get(flowId)?.snapshot();\n },\n\n answerLogin(flowId, promptId, value) {\n const flow = flows.get(flowId);\n if (!flow) return 'unknown_flow';\n return flow.answer(promptId, value) ? 'answered' : 'not_waiting';\n },\n\n cancelLogin(flowId) {\n const flow = flows.get(flowId);\n if (!flow) return undefined;\n flow.cancel();\n return flow.snapshot();\n },\n\n async logout(providerId) {\n const current = await runtime();\n const provider = findProvider(current, providerId);\n if (!provider) return { ok: false, code: 'unknown_provider', error: `Unknown provider: ${providerId}` };\n try {\n await current.logout(providerId, { signal: AbortSignal.timeout(LOGOUT_TIMEOUT_MS) });\n } catch (error) {\n return { ok: false, code: 'runtime', error: describe(error) };\n }\n notice(`signed out of ${provider.name}`);\n return { ok: true };\n },\n\n close() {\n for (const flow of flows.values()) flow.cancel();\n flows.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createLoginFlow as e}from"../services/loginFlow.mjs";import t from"node:crypto";function n(e){return e instanceof Error?e.message:String(e)}async function r(){let{ModelRuntime:e}=await import(`@earendil-works/pi-coding-agent`);return e.create()}function i(e){let t=[],{oauth:n,apiKey:r}=e.auth;return n&&t.push({type:`oauth`,label:n.loginLabel??n.name}),r?.login&&t.push({type:`api_key`,label:r.name}),t}function a(e,t){let n={id:t.id,name:t.name,methods:i(t)},r=e.getProviderAuthStatus(t.id);return r.configured&&(n.authenticated={type:e.isUsingOAuth(t.id)?`oauth`:`api_key`,source:r.label??r.source}),n}function o(o={}){let s=o.onNotice??(()=>{}),c=o.runtime??r,l=new Map,u,d=()=>(u??=c().catch(e=>{throw u=void 0,e}),u),f=(e,t)=>e.getProviders().find(e=>e.id===t),p=e=>[...l.values()].find(t=>t.providerId===e&&t.snapshot().status===`running`);return{async listProviders(){let e=await d();try{await e.refresh({allowNetwork:!1})}catch(e){s(`provider auth refresh failed: ${n(e)}`)}return e.getProviders().map(t=>a(e,t)).sort((e,t)=>e.name.localeCompare(t.name))},async startLogin(r,a){let o=await d(),c=f(o,r);if(!c)return{ok:!1,code:`unknown_provider`,error:`Unknown provider: ${r}`};if(!i(c).some(e=>e.type===a))return{ok:!1,code:`unsupported_method`,error:`${c.name} has no ${a} login.`};if(p(r))return{ok:!1,code:`busy`,error:`A login for ${c.name} is already in progress.`};for(let[e,t]of l)t.snapshot().status!==`running`&&l.delete(e);let u=e({id:t.randomUUID(),providerId:r,providerName:c.name,type:a});return l.set(u.id,u),o.login(r,a,u.interaction).then(()=>{u.settle({ok:!0}),s(`signed in to ${c.name} (${a})`)},e=>{u.settle({ok:!1,error:n(e)})}),{ok:!0,flow:u.snapshot()}},getLogin(e){return l.get(e)?.snapshot()},answerLogin(e,t,n){let r=l.get(e);return r?r.answer(t,n)?`answered`:`not_waiting`:`unknown_flow`},cancelLogin(e){let t=l.get(e);if(t)return t.cancel(),t.snapshot()},async logout(e){let t=await d(),r=f(t,e);if(!r)return{ok:!1,code:`unknown_provider`,error:`Unknown provider: ${e}`};try{await t.logout(e,{signal:AbortSignal.timeout(15e3)})}catch(e){return{ok:!1,code:`runtime`,error:n(e)}}return s(`signed out of ${r.name}`),{ok:!0}},close(){for(let e of l.values())e.cancel();l.clear()}}}export{o as createProviderAuth};
|
|
2
|
+
//# sourceMappingURL=providerAuth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providerAuth.mjs","names":[],"sources":["../../../src/adapters/providerAuth.ts"],"sourcesContent":["import crypto from 'node:crypto';\nimport { createLoginFlow, type LoginFlow } from '../services/loginFlow.ts';\nimport type {\n AuthMethodType,\n AuthRuntime,\n AuthRuntimeProvider,\n LoginFlowSnapshot,\n ProviderAuthMethod,\n ProviderAuthSummary,\n} from '../types/auth.ts';\n\n/** Matches the timeout Pi's own /logout gives the credential store. */\nconst LOGOUT_TIMEOUT_MS = 15_000;\n\nexport type StartLoginOutcome =\n | { ok: true; flow: LoginFlowSnapshot }\n | { ok: false; code: 'unknown_provider' | 'unsupported_method' | 'busy'; error: string };\n\nexport type LogoutOutcome = { ok: true } | { ok: false; code: 'unknown_provider' | 'runtime'; error: string };\n\nexport type AnswerOutcome = 'answered' | 'unknown_flow' | 'not_waiting';\n\n/** Provider credentials as the settings page sees them, backed by one Pi runtime. */\nexport interface ProviderAuth {\n /** Every provider Pi knows, with its current auth state; re-read from disk each call. */\n listProviders(): Promise<ProviderAuthSummary[]>;\n startLogin(providerId: string, type: AuthMethodType): Promise<StartLoginOutcome>;\n getLogin(flowId: string): LoginFlowSnapshot | undefined;\n answerLogin(flowId: string, promptId: string, value: string): AnswerOutcome;\n /** Cancels the flow and returns how it stands, or undefined for an unknown flow. */\n cancelLogin(flowId: string): LoginFlowSnapshot | undefined;\n logout(providerId: string): Promise<LogoutOutcome>;\n /** Abandons every running flow; the hub is shutting down. */\n close(): void;\n}\n\nexport interface ProviderAuthOptions {\n /** Test seam over Pi's ModelRuntime; the default loads the real one over auth.json. */\n runtime?: () => Promise<AuthRuntime>;\n onNotice?: (message: string) => void;\n}\n\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Pi's runtime, imported on first use: the hub should come up without paying\n * for the provider catalog, and a machine without Pi installed still serves\n * sessions, just not the providers page.\n */\nasync function loadPiRuntime(): Promise<AuthRuntime> {\n const { ModelRuntime } = await import('@earendil-works/pi-coding-agent');\n return ModelRuntime.create();\n}\n\n/** The methods a page can start, in the order the TUI's selector lists them. */\nfunction methodsOf(provider: AuthRuntimeProvider): ProviderAuthMethod[] {\n const methods: ProviderAuthMethod[] = [];\n const { oauth, apiKey } = provider.auth;\n if (oauth) methods.push({ type: 'oauth', label: oauth.loginLabel ?? oauth.name });\n if (apiKey?.login) methods.push({ type: 'api_key', label: apiKey.name });\n return methods;\n}\n\n/** Mirrors the status the TUI's login selector shows beside each provider. */\nfunction summarize(runtime: AuthRuntime, provider: AuthRuntimeProvider): ProviderAuthSummary {\n const summary: ProviderAuthSummary = { id: provider.id, name: provider.name, methods: methodsOf(provider) };\n const status = runtime.getProviderAuthStatus(provider.id);\n if (status.configured) {\n summary.authenticated = {\n type: runtime.isUsingOAuth(provider.id) ? 'oauth' : 'api_key',\n source: status.label ?? status.source,\n };\n }\n return summary;\n}\n\nexport function createProviderAuth(options: ProviderAuthOptions = {}): ProviderAuth {\n const notice = options.onNotice ?? ((): void => {});\n const load = options.runtime ?? loadPiRuntime;\n const flows = new Map<string, LoginFlow>();\n let loading: Promise<AuthRuntime> | undefined;\n\n // A failed load is forgotten so the next request tries again rather than\n // pinning the page on a startup error.\n const runtime = (): Promise<AuthRuntime> => {\n loading ??= load().catch((error: unknown) => {\n loading = undefined;\n throw error;\n });\n return loading;\n };\n\n const findProvider = (current: AuthRuntime, providerId: string): AuthRuntimeProvider | undefined =>\n current.getProviders().find((provider) => provider.id === providerId);\n\n const runningFor = (providerId: string): LoginFlow | undefined =>\n [...flows.values()].find((flow) => flow.providerId === providerId && flow.snapshot().status === 'running');\n\n return {\n async listProviders() {\n const current = await runtime();\n // Another client may have stored a credential since the last look (a\n // TUI /login, a hand-edited auth.json); the stale snapshot still lists\n // on a failed refresh, which the notice explains.\n try {\n await current.refresh({ allowNetwork: false });\n } catch (error) {\n notice(`provider auth refresh failed: ${describe(error)}`);\n }\n return current\n .getProviders()\n .map((provider) => summarize(current, provider))\n .sort((left, right) => left.name.localeCompare(right.name));\n },\n\n async startLogin(providerId, type) {\n const current = await runtime();\n const provider = findProvider(current, providerId);\n if (!provider) return { ok: false, code: 'unknown_provider', error: `Unknown provider: ${providerId}` };\n if (!methodsOf(provider).some((method) => method.type === type)) {\n return { ok: false, code: 'unsupported_method', error: `${provider.name} has no ${type} login.` };\n }\n if (runningFor(providerId)) {\n return { ok: false, code: 'busy', error: `A login for ${provider.name} is already in progress.` };\n }\n // Finished flows live until the next start: long enough for the page to\n // read how they ended, short enough that the map never grows.\n for (const [id, flow] of flows) if (flow.snapshot().status !== 'running') flows.delete(id);\n\n const flow = createLoginFlow({ id: crypto.randomUUID(), providerId, providerName: provider.name, type });\n flows.set(flow.id, flow);\n current.login(providerId, type, flow.interaction).then(\n () => {\n flow.settle({ ok: true });\n notice(`signed in to ${provider.name} (${type})`);\n },\n (error: unknown) => {\n flow.settle({ ok: false, error: describe(error) });\n },\n );\n return { ok: true, flow: flow.snapshot() };\n },\n\n getLogin(flowId) {\n return flows.get(flowId)?.snapshot();\n },\n\n answerLogin(flowId, promptId, value) {\n const flow = flows.get(flowId);\n if (!flow) return 'unknown_flow';\n return flow.answer(promptId, value) ? 'answered' : 'not_waiting';\n },\n\n cancelLogin(flowId) {\n const flow = flows.get(flowId);\n if (!flow) return undefined;\n flow.cancel();\n return flow.snapshot();\n },\n\n async logout(providerId) {\n const current = await runtime();\n const provider = findProvider(current, providerId);\n if (!provider) return { ok: false, code: 'unknown_provider', error: `Unknown provider: ${providerId}` };\n try {\n await current.logout(providerId, { signal: AbortSignal.timeout(LOGOUT_TIMEOUT_MS) });\n } catch (error) {\n return { ok: false, code: 'runtime', error: describe(error) };\n }\n notice(`signed out of ${provider.name}`);\n return { ok: true };\n },\n\n close() {\n for (const flow of flows.values()) flow.cancel();\n flows.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/registryStore.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);function i(e){try{return process.kill(e,0),!0}catch(e){return e.code!==`ESRCH`}}function a(e,a){let o=t.sessionRecordsDir(e),s,c,l,u,d=!1,f=()=>{let e;try{e=n.default.readdirSync(o)}catch{return[]}let s=[];for(let c of e.filter(t.isRecordFileName)){let e;try{e=n.default.readFileSync(r.default.join(o,c),`utf8`)}catch{continue}let l=t.parseSessionRecord(e);if(l){if(!i(l.pid)){n.default.rmSync(r.default.join(o,c),{force:!0}),a?.(`removed stale record for session ${l.id}`);continue}s.push(l)}}return s.sort((e,t)=>e.createdAt.localeCompare(t.createdAt)||e.id.localeCompare(t.id))},p=()=>{d||s?.(f())},m=()=>{if(!(c||d))try{c=n.default.watch(o,()=>{l&&clearTimeout(l),l=setTimeout(p,50)}),c.on(`error`,()=>{c?.close(),c=void 0})}catch{}};return{subscribe(e){s=e,m(),p(),u=setInterval(()=>{m(),p()},2e3)},close(){d=!0,l&&clearTimeout(l),u&&clearInterval(u),c?.close(),s=void 0}}}exports.watchRegistry=a;
|
|
2
|
+
//# sourceMappingURL=registryWatcher.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryWatcher.cjs","names":["sessionRecordsDir","fs","isRecordFileName","path","parseSessionRecord"],"sources":["../../../src/adapters/registryWatcher.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { isRecordFileName, parseSessionRecord, sessionRecordsDir } from '../services/registryStore.ts';\nimport type { SessionRecord } from '../types/registry.ts';\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 50;\n\n/** Where the hub learns which sessions exist: a watcher in hub mode, a fixed record otherwise. */\nexport interface RecordSource {\n /** Calls back with the full current record set, immediately and on every change. */\n subscribe(listener: (records: SessionRecord[]) => void): void;\n close(): void;\n}\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the pid exists but belongs to someone else; only ESRCH\n // proves the server is gone.\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n}\n\n/**\n * Watches the registry directory for session records.\n *\n * The 2s poll is the source of truth; fs.watch merely accelerates it, because\n * on macOS watch events around atomic renames are best-effort. Records whose\n * pid is gone are stale by definition (a crashed server cannot withdraw its\n * own record) and are deleted here, the one janitor in the system.\n */\nexport function watchRegistry(registryDir: string, onNotice?: (message: string) => void): RecordSource {\n const recordsDir = sessionRecordsDir(registryDir);\n let listener: ((records: SessionRecord[]) => void) | undefined;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let poll: NodeJS.Timeout | undefined;\n let closed = false;\n\n const scan = (): SessionRecord[] => {\n let names: string[];\n try {\n names = fs.readdirSync(recordsDir);\n } catch {\n // The directory appears with the first server; an empty registry is not\n // an error.\n return [];\n }\n const records: SessionRecord[] = [];\n for (const name of names.filter(isRecordFileName)) {\n let raw: string;\n try {\n raw = fs.readFileSync(path.join(recordsDir, name), 'utf8');\n } catch {\n continue; // Withdrawn between readdir and read; the next scan settles it.\n }\n const record = parseSessionRecord(raw);\n if (!record) continue;\n if (!pidAlive(record.pid)) {\n fs.rmSync(path.join(recordsDir, name), { force: true });\n onNotice?.(`removed stale record for session ${record.id}`);\n continue;\n }\n records.push(record);\n }\n return records.sort(\n (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),\n );\n };\n\n const emit = (): void => {\n if (!closed) listener?.(scan());\n };\n\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(recordsDir, () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the dir is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n return {\n subscribe(next) {\n listener = next;\n ensureWatcher();\n emit();\n poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, POLL_MS);\n },\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n if (poll) clearInterval(poll);\n watcher?.close();\n listener = undefined;\n },\n };\n}\n"],"mappings":"yLAeA,SAAS,EAAS,EAAsB,CACtC,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,CAAC,EACZ,EACT,OAAS,EAAO,CAGd,OAAQ,EAAgC,OAAS,OACnD,CACF,CAUA,SAAgB,EAAc,EAAqB,EAAoD,CACrG,IAAM,EAAaA,EAAAA,kBAAkB,CAAW,EAC5C,EACA,EACA,EACA,EACA,EAAS,GAEP,MAA8B,CAClC,IAAI,EACJ,GAAI,CACF,EAAQC,EAAAA,QAAG,YAAY,CAAU,CACnC,MAAQ,CAGN,MAAO,CAAC,CACV,CACA,IAAM,EAA2B,CAAC,EAClC,IAAK,IAAM,KAAQ,EAAM,OAAOC,EAAAA,gBAAgB,EAAG,CACjD,IAAI,EACJ,GAAI,CACF,EAAMD,EAAAA,QAAG,aAAaE,EAAAA,QAAK,KAAK,EAAY,CAAI,EAAG,MAAM,CAC3D,MAAQ,CACN,QACF,CACA,IAAM,EAASC,EAAAA,mBAAmB,CAAG,EAChC,KACL,IAAI,CAAC,EAAS,EAAO,GAAG,EAAG,CACzB,EAAA,QAAG,OAAOD,EAAAA,QAAK,KAAK,EAAY,CAAI,EAAG,CAAE,MAAO,EAAK,CAAC,EACtD,IAAW,oCAAoC,EAAO,IAAI,EAC1D,QACF,CACA,EAAQ,KAAK,CAAM,CADnB,CAEF,CACA,OAAO,EAAQ,MACZ,EAAM,IAAU,EAAK,UAAU,cAAc,EAAM,SAAS,GAAK,EAAK,GAAG,cAAc,EAAM,EAAE,CAClG,CACF,EAEM,MAAmB,CAClB,GAAQ,IAAW,EAAK,CAAC,CAChC,EAEM,MAA4B,CAC5B,QAAW,GACf,GAAI,CACF,EAAUF,EAAAA,QAAG,MAAM,MAAkB,CAC/B,GAAU,aAAa,CAAQ,EACnC,EAAW,WAAW,EAAM,EAAW,CACzC,CAAC,EACD,EAAQ,GAAG,YAAe,CACxB,GAAS,MAAM,EACf,EAAU,IAAA,EACZ,CAAC,CACH,MAAQ,CAER,CACF,EAEA,MAAO,CACL,UAAU,EAAM,CACd,EAAW,EACX,EAAc,EACd,EAAK,EACL,EAAO,gBAAkB,CACvB,EAAc,EACd,EAAK,CACP,EAAG,GAAO,CACZ,EACA,OAAQ,CACN,EAAS,GACL,GAAU,aAAa,CAAQ,EAC/B,GAAM,cAAc,CAAI,EAC5B,GAAS,MAAM,EACf,EAAW,IAAA,EACb,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{isRecordFileName as e,parseSessionRecord as t,sessionRecordsDir as n}from"../services/registryStore.mjs";import r from"node:fs";import i from"node:path";function a(e){try{return process.kill(e,0),!0}catch(e){return e.code!==`ESRCH`}}function o(o,s){let c=n(o),l,u,d,f,p=!1,m=()=>{let n;try{n=r.readdirSync(c)}catch{return[]}let o=[];for(let l of n.filter(e)){let e;try{e=r.readFileSync(i.join(c,l),`utf8`)}catch{continue}let n=t(e);if(n){if(!a(n.pid)){r.rmSync(i.join(c,l),{force:!0}),s?.(`removed stale record for session ${n.id}`);continue}o.push(n)}}return o.sort((e,t)=>e.createdAt.localeCompare(t.createdAt)||e.id.localeCompare(t.id))},h=()=>{p||l?.(m())},g=()=>{if(!(u||p))try{u=r.watch(c,()=>{d&&clearTimeout(d),d=setTimeout(h,50)}),u.on(`error`,()=>{u?.close(),u=void 0})}catch{}};return{subscribe(e){l=e,g(),h(),f=setInterval(()=>{g(),h()},2e3)},close(){p=!0,d&&clearTimeout(d),f&&clearInterval(f),u?.close(),l=void 0}}}export{o as watchRegistry};
|
|
2
|
+
//# sourceMappingURL=registryWatcher.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registryWatcher.mjs","names":[],"sources":["../../../src/adapters/registryWatcher.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { isRecordFileName, parseSessionRecord, sessionRecordsDir } from '../services/registryStore.ts';\nimport type { SessionRecord } from '../types/registry.ts';\n\nconst POLL_MS = 2000;\nconst DEBOUNCE_MS = 50;\n\n/** Where the hub learns which sessions exist: a watcher in hub mode, a fixed record otherwise. */\nexport interface RecordSource {\n /** Calls back with the full current record set, immediately and on every change. */\n subscribe(listener: (records: SessionRecord[]) => void): void;\n close(): void;\n}\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the pid exists but belongs to someone else; only ESRCH\n // proves the server is gone.\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n}\n\n/**\n * Watches the registry directory for session records.\n *\n * The 2s poll is the source of truth; fs.watch merely accelerates it, because\n * on macOS watch events around atomic renames are best-effort. Records whose\n * pid is gone are stale by definition (a crashed server cannot withdraw its\n * own record) and are deleted here, the one janitor in the system.\n */\nexport function watchRegistry(registryDir: string, onNotice?: (message: string) => void): RecordSource {\n const recordsDir = sessionRecordsDir(registryDir);\n let listener: ((records: SessionRecord[]) => void) | undefined;\n let watcher: fs.FSWatcher | undefined;\n let debounce: NodeJS.Timeout | undefined;\n let poll: NodeJS.Timeout | undefined;\n let closed = false;\n\n const scan = (): SessionRecord[] => {\n let names: string[];\n try {\n names = fs.readdirSync(recordsDir);\n } catch {\n // The directory appears with the first server; an empty registry is not\n // an error.\n return [];\n }\n const records: SessionRecord[] = [];\n for (const name of names.filter(isRecordFileName)) {\n let raw: string;\n try {\n raw = fs.readFileSync(path.join(recordsDir, name), 'utf8');\n } catch {\n continue; // Withdrawn between readdir and read; the next scan settles it.\n }\n const record = parseSessionRecord(raw);\n if (!record) continue;\n if (!pidAlive(record.pid)) {\n fs.rmSync(path.join(recordsDir, name), { force: true });\n onNotice?.(`removed stale record for session ${record.id}`);\n continue;\n }\n records.push(record);\n }\n return records.sort(\n (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),\n );\n };\n\n const emit = (): void => {\n if (!closed) listener?.(scan());\n };\n\n const ensureWatcher = (): void => {\n if (watcher || closed) return;\n try {\n watcher = fs.watch(recordsDir, () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(emit, DEBOUNCE_MS);\n });\n watcher.on('error', () => {\n watcher?.close();\n watcher = undefined; // The poll re-establishes it once the dir is back.\n });\n } catch {\n // The directory does not exist yet; the poll keeps trying.\n }\n };\n\n return {\n subscribe(next) {\n listener = next;\n ensureWatcher();\n emit();\n poll = setInterval(() => {\n ensureWatcher();\n emit();\n }, POLL_MS);\n },\n close() {\n closed = true;\n if (debounce) clearTimeout(debounce);\n if (poll) clearInterval(poll);\n watcher?.close();\n listener = undefined;\n },\n };\n}\n"],"mappings":"gKAeA,SAAS,EAAS,EAAsB,CACtC,GAAI,CAEF,OADA,QAAQ,KAAK,EAAK,CAAC,EACZ,EACT,OAAS,EAAO,CAGd,OAAQ,EAAgC,OAAS,OACnD,CACF,CAUA,SAAgB,EAAc,EAAqB,EAAoD,CACrG,IAAM,EAAa,EAAkB,CAAW,EAC5C,EACA,EACA,EACA,EACA,EAAS,GAEP,MAA8B,CAClC,IAAI,EACJ,GAAI,CACF,EAAQ,EAAG,YAAY,CAAU,CACnC,MAAQ,CAGN,MAAO,CAAC,CACV,CACA,IAAM,EAA2B,CAAC,EAClC,IAAK,IAAM,KAAQ,EAAM,OAAO,CAAgB,EAAG,CACjD,IAAI,EACJ,GAAI,CACF,EAAM,EAAG,aAAa,EAAK,KAAK,EAAY,CAAI,EAAG,MAAM,CAC3D,MAAQ,CACN,QACF,CACA,IAAM,EAAS,EAAmB,CAAG,EAChC,KACL,IAAI,CAAC,EAAS,EAAO,GAAG,EAAG,CACzB,EAAG,OAAO,EAAK,KAAK,EAAY,CAAI,EAAG,CAAE,MAAO,EAAK,CAAC,EACtD,IAAW,oCAAoC,EAAO,IAAI,EAC1D,QACF,CACA,EAAQ,KAAK,CAAM,CADnB,CAEF,CACA,OAAO,EAAQ,MACZ,EAAM,IAAU,EAAK,UAAU,cAAc,EAAM,SAAS,GAAK,EAAK,GAAG,cAAc,EAAM,EAAE,CAClG,CACF,EAEM,MAAmB,CAClB,GAAQ,IAAW,EAAK,CAAC,CAChC,EAEM,MAA4B,CAC5B,QAAW,GACf,GAAI,CACF,EAAU,EAAG,MAAM,MAAkB,CAC/B,GAAU,aAAa,CAAQ,EACnC,EAAW,WAAW,EAAM,EAAW,CACzC,CAAC,EACD,EAAQ,GAAG,YAAe,CACxB,GAAS,MAAM,EACf,EAAU,IAAA,EACZ,CAAC,CACH,MAAQ,CAER,CACF,EAEA,MAAO,CACL,UAAU,EAAM,CACd,EAAW,EACX,EAAc,EACd,EAAK,EACL,EAAO,gBAAkB,CACvB,EAAc,EACd,EAAK,CACP,EAAG,GAAO,CACZ,EACA,OAAQ,CACN,EAAS,GACL,GAAU,aAAa,CAAQ,EAC/B,GAAM,cAAc,CAAI,EAC5B,GAAS,MAAM,EACf,EAAW,IAAA,EACb,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../services/registryStore.cjs"),n=require("./bundledServer.cjs");let r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:child_process"),o=require("node:crypto");o=e.__toESM(o,1);const s=`doompi-server`,c=1e4;function l(e){try{return r.default.readFileSync(e,`utf8`).slice(-400).trim()}catch{return``}}function u(e){return{spawn(u){let d;try{d=r.default.statSync(u.cwd)}catch{return Promise.resolve({ok:!1,code:`invalid_request`,error:`No such directory: ${u.cwd}`})}if(!i.default.isAbsolute(u.cwd)||!d.isDirectory())return Promise.resolve({ok:!1,code:`invalid_request`,error:`The working directory must be an absolute path to a directory, received "${u.cwd}".`});let f;try{f=e.command?{command:e.command,args:[],environment:process.env}:n.defaultServerLaunch(u.cwd,process.env)}catch(e){return Promise.resolve({ok:!1,code:`spawn_failed`,error:`Could not locate ${s}: ${e instanceof Error?e.message:String(e)}`})}let p=e.command??s,m=o.default.randomUUID(),h=i.default.join(e.registryDir,`spawned`),g=m.slice(0,8);for(;r.default.existsSync(i.default.join(h,g))&&g.length<m.length;)g=m.slice(0,g.length+8);let _=i.default.join(h,g),v=i.default.join(_,`s.sock`);if(v.length>103)return Promise.resolve({ok:!1,code:`spawn_failed`,error:`The socket path would exceed the unix limit (${v.length} > 103); use a shorter --registry-dir.`});let y=i.default.join(_,`token`),b=i.default.join(_,`server.log`);r.default.mkdirSync(_,{recursive:!0,mode:448}),r.default.writeFileSync(y,o.default.randomBytes(32).toString(`hex`),{mode:384});let x=r.default.openSync(b,`a`,384),S=[`--listen`,v,`--auth-token-file`,y,`--session-id`,m,`--name`,u.name??`untitled`,`--registry-dir`,e.registryDir],C=(0,a.spawn)(f.command,[...f.args,...S],{cwd:u.cwd,detached:!0,stdio:[`ignore`,x,x],env:f.environment});return r.default.closeSync(x),new Promise(n=>{let i=!1,a=t=>{i||(i=!0,clearInterval(s),clearTimeout(d),t.ok&&e.onNotice?.(`created session ${m} in ${u.cwd}`),n(t))};C.once(`error`,e=>a({ok:!1,code:`spawn_failed`,error:`Could not start ${p}: ${e.message}`})),C.once(`exit`,e=>{let t=l(b);a({ok:!1,code:`spawn_failed`,error:`${p} exited with code ${e??`unknown`} before registering.${t?` Log: ${t}`:``}`})}),C.unref();let o=t.sessionRecordPath(e.registryDir,m),s=setInterval(()=>{r.default.existsSync(o)&&a({ok:!0,sessionId:m})},100),d=setTimeout(()=>{let e=l(b);a({ok:!1,code:`spawn_failed`,error:`${p} did not register within ${c/1e3}s.${e?` Log: ${e}`:``}`})},c)})}}}exports.createServerSpawner=u;
|
|
2
|
+
//# sourceMappingURL=serverSpawner.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverSpawner.cjs","names":["fs","path","defaultServerLaunch","crypto","spawn","sessionRecordPath"],"sources":["../../../src/adapters/serverSpawner.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { sessionRecordPath } from '../services/registryStore.ts';\nimport { type BundledServerLaunch, defaultServerLaunch } from './bundledServer.ts';\n\n/** Names the server in messages; the resolved command is a node path nobody would recognise. */\nconst SERVER_LABEL = 'doompi-server';\nconst SPAWNED_SEGMENT = 'spawned';\nconst SOCKET_FILE = 's.sock';\nconst TOKEN_FILE = 'token';\nconst LOG_FILE = 'server.log';\nconst DIR_NAME_PREFIX_LENGTH = 8;\nconst OWNER_ONLY_FILE = 0o600;\nconst OWNER_ONLY_DIR = 0o700;\nconst TOKEN_BYTES = 32;\n// macOS caps sun_path around 104 bytes; failing here beats a cryptic bind error.\nconst MAX_SOCKET_PATH = 103;\nconst RECORD_WAIT_MS = 10_000;\nconst RECORD_POLL_MS = 100;\nconst LOG_TAIL_CHARS = 400;\n\nexport type SpawnOutcome =\n | { ok: true; sessionId: string }\n | { ok: false; code: 'invalid_request' | 'spawn_failed'; error: string };\n\nexport interface SpawnSessionInput {\n cwd: string;\n name?: string;\n}\n\n/** Launches doompi-server processes for sessions created from the page. */\nexport interface SessionSpawner {\n spawn(input: SpawnSessionInput): Promise<SpawnOutcome>;\n}\n\nexport interface ServerSpawnerOptions {\n registryDir: string;\n /** Overridable so tests can stand in a fake server; defaults to the installation's own doompi-server. */\n command?: string;\n onNotice?: (message: string) => void;\n}\n\nfunction logTail(logPath: string): string {\n try {\n return fs.readFileSync(logPath, 'utf8').slice(-LOG_TAIL_CHARS).trim();\n } catch {\n return '';\n }\n}\n\n/**\n * Creates sessions the way a terminal would: by starting a doompi-server.\n *\n * The spawned server is detached and owns its own lifetime; it registers\n * itself like any other, which is why success is defined as \"its record\n * appeared\", not as anything about the child process. The hub stopping later\n * does not take created sessions down with it.\n *\n * Without an explicit command the server comes from this installation rather\n * than from PATH, so the cockpit needs no flag to launch the build it belongs\n * to. The resolution happens per session because it reads the session's own\n * working directory.\n */\nexport function createServerSpawner(options: ServerSpawnerOptions): SessionSpawner {\n return {\n spawn(input) {\n let stats: fs.Stats;\n try {\n stats = fs.statSync(input.cwd);\n } catch {\n return Promise.resolve({ ok: false, code: 'invalid_request', error: `No such directory: ${input.cwd}` });\n }\n if (!path.isAbsolute(input.cwd) || !stats.isDirectory()) {\n return Promise.resolve({\n ok: false,\n code: 'invalid_request',\n error: `The working directory must be an absolute path to a directory, received \"${input.cwd}\".`,\n });\n }\n\n // Resolved before anything is written, so a broken installation fails\n // without leaving a session directory nobody will ever use.\n let launch: BundledServerLaunch;\n try {\n launch = options.command\n ? { command: options.command, args: [], environment: process.env }\n : defaultServerLaunch(input.cwd, process.env);\n } catch (error) {\n return Promise.resolve({\n ok: false,\n code: 'spawn_failed',\n error: `Could not locate ${SERVER_LABEL}: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n const command = options.command ?? SERVER_LABEL;\n\n const sessionId = crypto.randomUUID();\n // The directory borrows only a prefix of the id: sun_path is capped\n // around 104 bytes and every byte of the session dir counts against it.\n const spawnedRoot = path.join(options.registryDir, SPAWNED_SEGMENT);\n let dirName = sessionId.slice(0, DIR_NAME_PREFIX_LENGTH);\n while (fs.existsSync(path.join(spawnedRoot, dirName)) && dirName.length < sessionId.length) {\n dirName = sessionId.slice(0, dirName.length + DIR_NAME_PREFIX_LENGTH);\n }\n const sessionDir = path.join(spawnedRoot, dirName);\n const socketPath = path.join(sessionDir, SOCKET_FILE);\n if (socketPath.length > MAX_SOCKET_PATH) {\n return Promise.resolve({\n ok: false,\n code: 'spawn_failed',\n error: `The socket path would exceed the unix limit (${socketPath.length} > ${MAX_SOCKET_PATH}); use a shorter --registry-dir.`,\n });\n }\n const tokenFile = path.join(sessionDir, TOKEN_FILE);\n const logPath = path.join(sessionDir, LOG_FILE);\n fs.mkdirSync(sessionDir, { recursive: true, mode: OWNER_ONLY_DIR });\n fs.writeFileSync(tokenFile, crypto.randomBytes(TOKEN_BYTES).toString('hex'), { mode: OWNER_ONLY_FILE });\n\n const log = fs.openSync(logPath, 'a', OWNER_ONLY_FILE);\n const args = [\n '--listen',\n socketPath,\n '--auth-token-file',\n tokenFile,\n '--session-id',\n sessionId,\n '--name',\n input.name ?? 'untitled',\n '--registry-dir',\n options.registryDir,\n ];\n const child = spawn(launch.command, [...launch.args, ...args], {\n cwd: input.cwd,\n detached: true,\n stdio: ['ignore', log, log],\n env: launch.environment,\n });\n fs.closeSync(log);\n\n return new Promise((resolve) => {\n let settled = false;\n const finish = (outcome: SpawnOutcome): void => {\n if (settled) return;\n settled = true;\n clearInterval(pollTimer);\n clearTimeout(deadline);\n if (outcome.ok) options.onNotice?.(`created session ${sessionId} in ${input.cwd}`);\n resolve(outcome);\n };\n\n child.once('error', (error) =>\n finish({ ok: false, code: 'spawn_failed', error: `Could not start ${command}: ${error.message}` }),\n );\n child.once('exit', (code) => {\n const tail = logTail(logPath);\n finish({\n ok: false,\n code: 'spawn_failed',\n error: `${command} exited with code ${code ?? 'unknown'} before registering.${tail ? ` Log: ${tail}` : ''}`,\n });\n });\n child.unref();\n\n const recordFile = sessionRecordPath(options.registryDir, sessionId);\n const pollTimer = setInterval(() => {\n if (fs.existsSync(recordFile)) finish({ ok: true, sessionId });\n }, RECORD_POLL_MS);\n const deadline = setTimeout(() => {\n const tail = logTail(logPath);\n finish({\n ok: false,\n code: 'spawn_failed',\n error: `${command} did not register within ${RECORD_WAIT_MS / 1000}s.${tail ? ` Log: ${tail}` : ''}`,\n });\n }, RECORD_WAIT_MS);\n });\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{sessionRecordPath as e}from"../services/registryStore.mjs";import{defaultServerLaunch as t}from"./bundledServer.mjs";import n from"node:fs";import r from"node:path";import{spawn as i}from"node:child_process";import a from"node:crypto";const o=`doompi-server`,s=1e4;function c(e){try{return n.readFileSync(e,`utf8`).slice(-400).trim()}catch{return``}}function l(l){return{spawn(u){let d;try{d=n.statSync(u.cwd)}catch{return Promise.resolve({ok:!1,code:`invalid_request`,error:`No such directory: ${u.cwd}`})}if(!r.isAbsolute(u.cwd)||!d.isDirectory())return Promise.resolve({ok:!1,code:`invalid_request`,error:`The working directory must be an absolute path to a directory, received "${u.cwd}".`});let f;try{f=l.command?{command:l.command,args:[],environment:process.env}:t(u.cwd,process.env)}catch(e){return Promise.resolve({ok:!1,code:`spawn_failed`,error:`Could not locate ${o}: ${e instanceof Error?e.message:String(e)}`})}let p=l.command??o,m=a.randomUUID(),h=r.join(l.registryDir,`spawned`),g=m.slice(0,8);for(;n.existsSync(r.join(h,g))&&g.length<m.length;)g=m.slice(0,g.length+8);let _=r.join(h,g),v=r.join(_,`s.sock`);if(v.length>103)return Promise.resolve({ok:!1,code:`spawn_failed`,error:`The socket path would exceed the unix limit (${v.length} > 103); use a shorter --registry-dir.`});let y=r.join(_,`token`),b=r.join(_,`server.log`);n.mkdirSync(_,{recursive:!0,mode:448}),n.writeFileSync(y,a.randomBytes(32).toString(`hex`),{mode:384});let x=n.openSync(b,`a`,384),S=[`--listen`,v,`--auth-token-file`,y,`--session-id`,m,`--name`,u.name??`untitled`,`--registry-dir`,l.registryDir],C=i(f.command,[...f.args,...S],{cwd:u.cwd,detached:!0,stdio:[`ignore`,x,x],env:f.environment});return n.closeSync(x),new Promise(t=>{let r=!1,i=e=>{r||(r=!0,clearInterval(o),clearTimeout(d),e.ok&&l.onNotice?.(`created session ${m} in ${u.cwd}`),t(e))};C.once(`error`,e=>i({ok:!1,code:`spawn_failed`,error:`Could not start ${p}: ${e.message}`})),C.once(`exit`,e=>{let t=c(b);i({ok:!1,code:`spawn_failed`,error:`${p} exited with code ${e??`unknown`} before registering.${t?` Log: ${t}`:``}`})}),C.unref();let a=e(l.registryDir,m),o=setInterval(()=>{n.existsSync(a)&&i({ok:!0,sessionId:m})},100),d=setTimeout(()=>{let e=c(b);i({ok:!1,code:`spawn_failed`,error:`${p} did not register within ${s/1e3}s.${e?` Log: ${e}`:``}`})},s)})}}}export{l as createServerSpawner};
|
|
2
|
+
//# sourceMappingURL=serverSpawner.mjs.map
|