@agimon-ai/doompi-web 0.0.1-alpha.0 → 0.0.1-alpha.10
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/README.md +261 -44
- package/dist/bin/serve.cjs +1 -1
- package/dist/bin/serve.cjs.map +1 -1
- package/dist/bin/serve.mjs +1 -1
- package/dist/bin/serve.mjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/pwa/icon-192.png +0 -0
- package/dist/pwa/icon-512.png +0 -0
- package/dist/pwa/index.html +104 -0
- package/dist/pwa/manifest.webmanifest +23 -0
- package/dist/pwa/pwa.js +21 -0
- package/dist/pwa/sw.js +2 -0
- package/dist/src/adapters/bundlePublication.cjs +3 -0
- package/dist/src/adapters/bundlePublication.cjs.map +1 -0
- package/dist/src/adapters/bundlePublication.mjs +3 -0
- package/dist/src/adapters/bundlePublication.mjs.map +1 -0
- package/dist/src/adapters/cockpitContainer.cjs +2 -0
- package/dist/src/adapters/cockpitContainer.cjs.map +1 -0
- package/dist/src/adapters/cockpitContainer.mjs +2 -0
- package/dist/src/adapters/cockpitContainer.mjs.map +1 -0
- package/dist/src/adapters/cockpitHandoff.cjs +2 -0
- package/dist/src/adapters/cockpitHandoff.cjs.map +1 -0
- package/dist/src/adapters/cockpitHandoff.mjs +2 -0
- package/dist/src/adapters/cockpitHandoff.mjs.map +1 -0
- package/dist/src/adapters/deviceAuth.cjs +2 -0
- package/dist/src/adapters/deviceAuth.cjs.map +1 -0
- package/dist/src/adapters/deviceAuth.mjs +2 -0
- package/dist/src/adapters/deviceAuth.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +1 -1
- package/dist/src/adapters/directoryListing.cjs.map +1 -1
- package/dist/src/adapters/directoryListing.mjs +1 -1
- package/dist/src/adapters/directoryListing.mjs.map +1 -1
- package/dist/src/adapters/doomInitialization.cjs +2 -0
- package/dist/src/adapters/doomInitialization.cjs.map +1 -0
- package/dist/src/adapters/doomInitialization.mjs +2 -0
- package/dist/src/adapters/doomInitialization.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -1
- package/dist/src/adapters/httpServer.cjs.map +1 -1
- package/dist/src/adapters/httpServer.d.cts +2 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -1
- package/dist/src/adapters/httpServer.d.mts +2 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -1
- package/dist/src/adapters/httpServer.mjs +2 -1
- package/dist/src/adapters/httpServer.mjs.map +1 -1
- package/dist/src/adapters/hubProbe.cjs +1 -1
- package/dist/src/adapters/hubProbe.cjs.map +1 -1
- package/dist/src/adapters/hubProbe.mjs +1 -1
- package/dist/src/adapters/hubProbe.mjs.map +1 -1
- package/dist/src/adapters/packageApiProxy.cjs +1 -1
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -1
- package/dist/src/adapters/packageApiProxy.mjs +1 -1
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -1
- package/dist/src/adapters/providerAuth.cjs +1 -1
- package/dist/src/adapters/providerAuth.cjs.map +1 -1
- package/dist/src/adapters/providerAuth.mjs +1 -1
- package/dist/src/adapters/providerAuth.mjs.map +1 -1
- package/dist/src/adapters/registryWatcher.cjs +1 -1
- package/dist/src/adapters/registryWatcher.cjs.map +1 -1
- package/dist/src/adapters/registryWatcher.d.cts +1 -0
- package/dist/src/adapters/registryWatcher.d.mts +1 -0
- package/dist/src/adapters/registryWatcher.mjs +1 -1
- package/dist/src/adapters/registryWatcher.mjs.map +1 -1
- package/dist/src/adapters/remoteAccess.cjs +2 -0
- package/dist/src/adapters/remoteAccess.cjs.map +1 -0
- package/dist/src/adapters/remoteAccess.mjs +2 -0
- package/dist/src/adapters/remoteAccess.mjs.map +1 -0
- package/dist/src/adapters/remoteAccessStore.cjs +2 -0
- package/dist/src/adapters/remoteAccessStore.cjs.map +1 -0
- package/dist/src/adapters/remoteAccessStore.mjs +2 -0
- package/dist/src/adapters/remoteAccessStore.mjs.map +1 -0
- package/dist/src/adapters/remoteGuard.cjs +2 -0
- package/dist/src/adapters/remoteGuard.cjs.map +1 -0
- package/dist/src/adapters/remoteGuard.mjs +2 -0
- package/dist/src/adapters/remoteGuard.mjs.map +1 -0
- package/dist/src/adapters/remoteRoutes.cjs +2 -0
- package/dist/src/adapters/remoteRoutes.cjs.map +1 -0
- package/dist/src/adapters/remoteRoutes.mjs +2 -0
- package/dist/src/adapters/remoteRoutes.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +1 -1
- package/dist/src/adapters/serverSpawner.cjs.map +1 -1
- package/dist/src/adapters/serverSpawner.d.cts +1 -0
- package/dist/src/adapters/serverSpawner.d.mts +1 -0
- package/dist/src/adapters/serverSpawner.mjs +1 -1
- package/dist/src/adapters/serverSpawner.mjs.map +1 -1
- package/dist/src/adapters/sessionFiles.cjs +1 -1
- package/dist/src/adapters/sessionFiles.cjs.map +1 -1
- package/dist/src/adapters/sessionFiles.mjs +1 -1
- package/dist/src/adapters/sessionFiles.mjs.map +1 -1
- package/dist/src/adapters/sessionHub.cjs +1 -1
- package/dist/src/adapters/sessionHub.cjs.map +1 -1
- package/dist/src/adapters/sessionHub.d.cts +6 -0
- package/dist/src/adapters/sessionHub.d.mts +6 -0
- package/dist/src/adapters/sessionHub.mjs +1 -1
- package/dist/src/adapters/sessionHub.mjs.map +1 -1
- package/dist/src/adapters/sessionRelaunch.cjs +2 -0
- package/dist/src/adapters/sessionRelaunch.cjs.map +1 -0
- package/dist/src/adapters/sessionRelaunch.mjs +2 -0
- package/dist/src/adapters/sessionRelaunch.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +1 -1
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -1
- package/dist/src/adapters/sessionSocketClient.mjs +1 -1
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -1
- package/dist/src/adapters/settingsRoutes.cjs +2 -0
- package/dist/src/adapters/settingsRoutes.cjs.map +1 -0
- package/dist/src/adapters/settingsRoutes.mjs +2 -0
- package/dist/src/adapters/settingsRoutes.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +1 -1
- package/dist/src/adapters/syncGuard.cjs.map +1 -1
- package/dist/src/adapters/syncGuard.mjs +1 -1
- package/dist/src/adapters/syncGuard.mjs.map +1 -1
- package/dist/src/adapters/tunnelProcess.cjs +2 -0
- package/dist/src/adapters/tunnelProcess.cjs.map +1 -0
- package/dist/src/adapters/tunnelProcess.mjs +2 -0
- package/dist/src/adapters/tunnelProcess.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +1 -1
- package/dist/src/adapters/webBundler.cjs.map +1 -1
- package/dist/src/adapters/webBundler.d.cts +1 -1
- package/dist/src/adapters/webBundler.d.cts.map +1 -1
- package/dist/src/adapters/webBundler.d.mts +1 -1
- package/dist/src/adapters/webBundler.d.mts.map +1 -1
- package/dist/src/adapters/webBundler.mjs +1 -1
- package/dist/src/adapters/webBundler.mjs.map +1 -1
- package/dist/src/adapters/webComposition.cjs +3 -0
- package/dist/src/adapters/webComposition.cjs.map +1 -0
- package/dist/src/adapters/webComposition.mjs +3 -0
- package/dist/src/adapters/webComposition.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +1 -1
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -1
- package/dist/src/adapters/webHubPluginLoader.mjs +1 -1
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -1
- package/dist/src/adapters/webPluginGenerate.cjs +2 -2
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -1
- package/dist/src/adapters/webPluginGenerate.mjs +2 -2
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -1
- package/dist/src/adapters/webPluginScan.cjs +1 -1
- package/dist/src/adapters/webPluginScan.cjs.map +1 -1
- package/dist/src/adapters/webPluginScan.mjs +1 -1
- package/dist/src/adapters/webPluginScan.mjs.map +1 -1
- package/dist/src/adapters/webPluginVite.cjs +1 -1
- package/dist/src/adapters/webPluginVite.cjs.map +1 -1
- package/dist/src/adapters/webPluginVite.mjs +1 -1
- package/dist/src/adapters/webPluginVite.mjs.map +1 -1
- package/dist/src/adapters/webPush.cjs +2 -0
- package/dist/src/adapters/webPush.cjs.map +1 -0
- package/dist/src/adapters/webPush.mjs +2 -0
- package/dist/src/adapters/webPush.mjs.map +1 -0
- package/dist/src/adapters/webTelemetry.cjs +2 -0
- package/dist/src/adapters/webTelemetry.cjs.map +1 -0
- package/dist/src/adapters/webTelemetry.mjs +2 -0
- package/dist/src/adapters/webTelemetry.mjs.map +1 -0
- package/dist/src/adapters/webauthn.cjs +2 -0
- package/dist/src/adapters/webauthn.cjs.map +1 -0
- package/dist/src/adapters/webauthn.mjs +2 -0
- package/dist/src/adapters/webauthn.mjs.map +1 -0
- package/dist/src/services/deviceSessions.cjs +2 -0
- package/dist/src/services/deviceSessions.cjs.map +1 -0
- package/dist/src/services/deviceSessions.mjs +2 -0
- package/dist/src/services/deviceSessions.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +1 -1
- package/dist/src/services/fileMatch.cjs.map +1 -1
- package/dist/src/services/fileMatch.mjs +1 -1
- package/dist/src/services/fileMatch.mjs.map +1 -1
- package/dist/src/services/fileMedia.cjs +1 -1
- package/dist/src/services/fileMedia.cjs.map +1 -1
- package/dist/src/services/fileMedia.mjs +1 -1
- package/dist/src/services/fileMedia.mjs.map +1 -1
- package/dist/src/services/pairingFlow.cjs +2 -0
- package/dist/src/services/pairingFlow.cjs.map +1 -0
- package/dist/src/services/pairingFlow.mjs +2 -0
- package/dist/src/services/pairingFlow.mjs.map +1 -0
- package/dist/src/services/pairingPage.cjs +385 -0
- package/dist/src/services/pairingPage.cjs.map +1 -0
- package/dist/src/services/pairingPage.mjs +385 -0
- package/dist/src/services/pairingPage.mjs.map +1 -0
- package/dist/src/services/pathScope.cjs +2 -0
- package/dist/src/services/pathScope.cjs.map +1 -0
- package/dist/src/services/pathScope.mjs +2 -0
- package/dist/src/services/pathScope.mjs.map +1 -0
- package/dist/src/services/remoteAccessSettings.cjs +2 -0
- package/dist/src/services/remoteAccessSettings.cjs.map +1 -0
- package/dist/src/services/remoteAccessSettings.mjs +2 -0
- package/dist/src/services/remoteAccessSettings.mjs.map +1 -0
- package/dist/src/services/remoteGuardPolicy.cjs +2 -0
- package/dist/src/services/remoteGuardPolicy.cjs.map +1 -0
- package/dist/src/services/remoteGuardPolicy.mjs +2 -0
- package/dist/src/services/remoteGuardPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +14 -1
- package/dist/src/services/serveOptions.cjs.map +1 -1
- package/dist/src/services/serveOptions.d.cts +8 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -1
- package/dist/src/services/serveOptions.d.mts +8 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -1
- package/dist/src/services/serveOptions.mjs +14 -1
- package/dist/src/services/serveOptions.mjs.map +1 -1
- package/dist/src/services/sessionMigration.cjs +2 -0
- package/dist/src/services/sessionMigration.cjs.map +1 -0
- package/dist/src/services/sessionMigration.mjs +2 -0
- package/dist/src/services/sessionMigration.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +1 -1
- package/dist/src/services/staticAssets.cjs.map +1 -1
- package/dist/src/services/staticAssets.mjs +1 -1
- package/dist/src/services/staticAssets.mjs.map +1 -1
- package/dist/src/services/tunnelOutput.cjs +2 -0
- package/dist/src/services/tunnelOutput.cjs.map +1 -0
- package/dist/src/services/tunnelOutput.mjs +2 -0
- package/dist/src/services/tunnelOutput.mjs.map +1 -0
- package/dist/src/services/webauthnPolicy.cjs +2 -0
- package/dist/src/services/webauthnPolicy.cjs.map +1 -0
- package/dist/src/services/webauthnPolicy.mjs +2 -0
- package/dist/src/services/webauthnPolicy.mjs.map +1 -0
- package/dist/src/types/auth.cjs.map +1 -1
- package/dist/src/types/auth.d.cts +6 -0
- package/dist/src/types/auth.d.cts.map +1 -1
- package/dist/src/types/auth.d.mts +6 -0
- package/dist/src/types/auth.d.mts.map +1 -1
- package/dist/src/types/auth.mjs.map +1 -1
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -1
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -1
- package/dist/src/types/hub.cjs +1 -1
- package/dist/src/types/hub.cjs.map +1 -1
- package/dist/src/types/hub.d.cts +1 -1
- package/dist/src/types/hub.d.cts.map +1 -1
- package/dist/src/types/hub.d.mts +1 -1
- package/dist/src/types/hub.d.mts.map +1 -1
- package/dist/src/types/hub.mjs +1 -1
- package/dist/src/types/hub.mjs.map +1 -1
- package/dist/src/types/notification.cjs +2 -0
- package/dist/src/types/notification.cjs.map +1 -0
- package/dist/src/types/notification.mjs +2 -0
- package/dist/src/types/notification.mjs.map +1 -0
- package/dist/src/types/remoteAccess.cjs +2 -0
- package/dist/src/types/remoteAccess.cjs.map +1 -0
- package/dist/src/types/remoteAccess.d.cts +88 -0
- package/dist/src/types/remoteAccess.d.cts.map +1 -0
- package/dist/src/types/remoteAccess.d.mts +88 -0
- package/dist/src/types/remoteAccess.d.mts.map +1 -0
- package/dist/src/types/remoteAccess.mjs +2 -0
- package/dist/src/types/remoteAccess.mjs.map +1 -0
- package/dist/src/types/session.cjs +1 -1
- package/dist/src/types/session.mjs +1 -1
- package/dist/src/types/session.mjs.map +1 -1
- package/dist/src/types/settings.cjs +2 -0
- package/dist/src/types/settings.cjs.map +1 -0
- package/dist/src/types/settings.d.cts +1 -0
- package/dist/src/types/settings.d.mts +1 -0
- package/dist/src/types/settings.mjs +2 -0
- package/dist/src/types/settings.mjs.map +1 -0
- package/dist/web/assets/CodeEditorView-Drbj5Jmq.js +15 -0
- package/dist/web/assets/CodeEditorView-Drbj5Jmq.js.map +1 -0
- package/dist/web/assets/dist-7iNxebeg.js +2 -0
- package/dist/web/assets/dist-7iNxebeg.js.map +1 -0
- package/dist/web/assets/dist-BCRyBbdR.js +2 -0
- package/dist/web/assets/dist-BCRyBbdR.js.map +1 -0
- package/dist/web/assets/dist-BiFP6LQz.js +24 -0
- package/dist/web/assets/dist-BiFP6LQz.js.map +1 -0
- package/dist/web/assets/dist-C2KhFiEs.js +1 -0
- package/dist/web/assets/dist-CM-9xFgG.js +2 -0
- package/dist/web/assets/dist-CM-9xFgG.js.map +1 -0
- package/dist/web/assets/dist-ClQi4oJ4.js +10 -0
- package/dist/web/assets/dist-ClQi4oJ4.js.map +1 -0
- package/dist/web/assets/dist-CyY9Rnb9.js +2 -0
- package/dist/web/assets/dist-CyY9Rnb9.js.map +1 -0
- package/dist/web/assets/dist-D_AIHDMX.js +2 -0
- package/dist/web/assets/dist-D_AIHDMX.js.map +1 -0
- package/dist/web/assets/dist-DjFwY_To.js +2 -0
- package/dist/web/assets/dist-DjFwY_To.js.map +1 -0
- package/dist/web/assets/dist-joNLZ1a7.js +7 -0
- package/dist/web/assets/dist-joNLZ1a7.js.map +1 -0
- package/dist/web/assets/dockerfile-BDePf5PZ.js +2 -0
- package/dist/web/assets/dockerfile-BDePf5PZ.js.map +1 -0
- package/dist/web/assets/index-BuzTi9Gb.css +2 -0
- package/dist/web/assets/index-Ek5bNpFT.js +101 -0
- package/dist/web/assets/index-Ek5bNpFT.js.map +1 -0
- package/dist/web/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/web/assets/shell-DwuoZtxw.js +2 -0
- package/dist/web/assets/shell-DwuoZtxw.js.map +1 -0
- package/dist/web/assets/toml-BPTmHmyx.js +2 -0
- package/dist/web/assets/toml-BPTmHmyx.js.map +1 -0
- package/dist/web/index.html +5 -3
- package/llms.txt +6 -1
- package/package.json +26 -17
- package/src/adapters/bundlePublication.ts +156 -0
- package/src/adapters/cockpitContainer.ts +209 -0
- package/src/adapters/cockpitHandoff.ts +66 -0
- package/src/adapters/deviceAuth.ts +171 -0
- package/src/adapters/directoryListing.ts +25 -8
- package/src/adapters/doomInitialization.ts +49 -0
- package/src/adapters/httpServer.ts +990 -115
- package/src/adapters/hubProbe.ts +34 -5
- package/src/adapters/packageApiProxy.ts +14 -3
- package/src/adapters/providerAuth.ts +20 -0
- package/src/adapters/registryWatcher.ts +33 -30
- package/src/adapters/remoteAccess.ts +544 -0
- package/src/adapters/remoteAccessStore.ts +161 -0
- package/src/adapters/remoteGuard.ts +179 -0
- package/src/adapters/remoteRoutes.ts +491 -0
- package/src/adapters/serverSpawner.ts +67 -6
- package/src/adapters/sessionFiles.ts +26 -5
- package/src/adapters/sessionHub.ts +331 -97
- package/src/adapters/sessionRelaunch.ts +67 -0
- package/src/adapters/sessionSocketClient.ts +3 -1
- package/src/adapters/settingsRoutes.ts +383 -0
- package/src/adapters/syncGuard.ts +127 -38
- package/src/adapters/tunnelProcess.ts +552 -0
- package/src/adapters/webBundler.ts +11 -2
- package/src/adapters/webComposition.ts +240 -0
- package/src/adapters/webHubPluginLoader.ts +6 -2
- package/src/adapters/webPluginGenerate.ts +16 -1
- package/src/adapters/webPluginScan.ts +6 -2
- package/src/adapters/webPluginVite.ts +24 -9
- package/src/adapters/webPush.ts +127 -0
- package/src/adapters/webTelemetry.ts +131 -0
- package/src/adapters/webauthn.ts +297 -0
- package/src/bin/serve.ts +169 -12
- package/src/pwa/bundleCache.ts +79 -0
- package/src/pwa/index.html +104 -0
- package/src/pwa/main.ts +163 -0
- package/src/pwa/public/icon-192.png +0 -0
- package/src/pwa/public/icon-512.png +0 -0
- package/src/pwa/public/manifest.webmanifest +23 -0
- package/src/pwa/qrScanner.ts +65 -0
- package/src/pwa/serviceWorker.ts +314 -0
- package/src/pwa/workerClient.ts +60 -0
- package/src/services/deviceSessions.ts +135 -0
- package/src/services/fileMatch.ts +5 -1
- package/src/services/fileMedia.ts +1 -0
- package/src/services/pairingFlow.ts +196 -0
- package/src/services/pairingPage.ts +467 -0
- package/src/services/pathScope.ts +23 -0
- package/src/services/remoteAccessSettings.ts +185 -0
- package/src/services/remoteGuardPolicy.ts +211 -0
- package/src/services/serveOptions.ts +64 -2
- package/src/services/sessionMigration.ts +48 -0
- package/src/services/staticAssets.ts +1 -0
- package/src/services/tunnelOutput.ts +74 -0
- package/src/services/webauthnPolicy.ts +135 -0
- package/src/types/auth.ts +3 -0
- package/src/types/bridge.ts +29 -0
- package/src/types/bundle.ts +28 -0
- package/src/types/hub.ts +68 -3
- package/src/types/notification.ts +30 -0
- package/src/types/remoteAccess.ts +205 -0
- package/src/types/settings.ts +120 -0
- package/src/web/app/Providers.tsx +33 -6
- package/src/web/app/protocolRuntime.ts +14 -3
- package/src/web/app/sessionRuntime.ts +101 -4
- package/src/web/components/QrCode.tsx +53 -0
- package/src/web/components/RemoteAccessButton.tsx +46 -0
- package/src/web/features/activity/ActivityDock.tsx +121 -86
- package/src/web/features/activity/ContextPanel.tsx +152 -0
- package/src/web/features/activity/DockTabs.tsx +39 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +22 -8
- package/src/web/features/leader/CommandPalette.tsx +13 -6
- package/src/web/features/remote/HandoverProgress.tsx +25 -0
- package/src/web/features/remote/PairedDeviceList.tsx +39 -0
- package/src/web/features/remote/PairingApprovalDialog.tsx +58 -0
- package/src/web/features/remote/RemoteAccessDialog.tsx +113 -0
- package/src/web/features/remote/RemoteAccessOptions.tsx +130 -0
- package/src/web/features/remote/RemoteAccessPairing.tsx +75 -0
- package/src/web/features/remote/RemoteBanner.tsx +50 -0
- package/src/web/features/remote/SandboxWorkspaces.tsx +84 -0
- package/src/web/features/selection/SelectionBar.tsx +14 -11
- package/src/web/features/session/Composer.tsx +426 -69
- package/src/web/features/session/ComposerPrompt.tsx +50 -0
- package/src/web/features/session/MentionPreviews.tsx +74 -17
- package/src/web/features/session/MessageMarkdown.tsx +31 -0
- package/src/web/features/session/QueueSheet.tsx +130 -0
- package/src/web/features/session/ThreadView.tsx +15 -1
- package/src/web/features/session/Timeline.tsx +221 -104
- package/src/web/features/session/ToolCard.tsx +157 -8
- package/src/web/features/sessions/NewSessionDialog.tsx +32 -15
- package/src/web/features/sessions/ResumeSessionDialog.tsx +150 -0
- package/src/web/features/sessions/SessionRail.tsx +143 -36
- package/src/web/features/sessions/WelcomePanel.tsx +32 -0
- package/src/web/features/settings/AppearanceSettings.tsx +1 -1
- package/src/web/features/settings/ContributedSettings.tsx +368 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +7 -4
- package/src/web/features/settings/NotificationSettings.tsx +74 -0
- package/src/web/features/settings/PluginSettings.tsx +6 -2
- package/src/web/features/settings/ProviderSettings.tsx +12 -4
- package/src/web/features/settings/RemoteControlSettings.tsx +40 -0
- package/src/web/features/settings/RepositorySettings.tsx +374 -0
- package/src/web/features/settings/RepositoryWorkspace.tsx +155 -0
- package/src/web/features/settings/SettingsMenu.tsx +97 -26
- package/src/web/features/settings/SettingsPanelHost.tsx +36 -0
- package/src/web/features/settings/TunnelSettings.tsx +155 -0
- package/src/web/features/status/TopBar.tsx +52 -59
- package/src/web/index.html +2 -1
- package/src/web/lib/authApi.ts +4 -1
- package/src/web/lib/browserNotifications.ts +47 -0
- package/src/web/lib/browserTelemetry.ts +95 -0
- package/src/web/lib/commands.ts +51 -6
- package/src/web/lib/composition.ts +137 -11
- package/src/web/lib/contextComposition.ts +200 -0
- package/src/web/lib/fileMentions.ts +7 -3
- package/src/web/lib/hubApi.ts +90 -5
- package/src/web/lib/installStaleChunkRecovery.ts +43 -0
- package/src/web/lib/livePush.ts +116 -0
- package/src/web/lib/piTransport.ts +18 -7
- package/src/web/lib/pluginRegistry.ts +77 -1
- package/src/web/lib/pluginSlotProps.ts +8 -2
- package/src/web/lib/protocolTimeline.ts +22 -4
- package/src/web/lib/qrMatrix.ts +33 -0
- package/src/web/lib/remoteApi.ts +127 -0
- package/src/web/lib/sealedSession.ts +80 -0
- package/src/web/lib/sessionAsset.ts +30 -0
- package/src/web/lib/sessionModel.ts +235 -28
- package/src/web/lib/settingsApi.ts +158 -0
- package/src/web/lib/settingsDraft.ts +58 -0
- package/src/web/lib/settingsSections.ts +132 -6
- package/src/web/lib/stepUp.ts +43 -0
- package/src/web/lib/threadRenderer.ts +4 -3
- package/src/web/lib/timelineGroups.ts +91 -0
- package/src/web/lib/toolPrompt.ts +68 -0
- package/src/web/lib/transport.ts +12 -0
- package/src/web/lib/webauthnClient.ts +107 -0
- package/src/web/lib/wsClient.ts +43 -11
- package/src/web/main.tsx +3 -0
- package/src/web/routes/CockpitPage.tsx +86 -11
- package/src/web/routes/SettingsPage.tsx +106 -19
- package/src/web/stores/composerStore.ts +141 -0
- package/src/web/stores/newSessionStore.ts +29 -0
- package/src/web/stores/remoteAccessStore.ts +205 -0
- package/src/web/stores/sessionStore.ts +185 -24
- package/src/web/stores/sessionsStore.ts +17 -0
- package/src/web/stores/toolPromptStore.ts +28 -0
- package/src/web/stores/uiStore.ts +30 -1
- package/src/web/stores/usePluginSlotProps.ts +20 -9
- package/src/web/stores/useToolPrompt.ts +32 -0
- package/dist/web/assets/index-Cg52ngKv.css +0 -2
- package/dist/web/assets/index-DQO7NTIP.js +0 -90
- package/dist/web/assets/index-DQO7NTIP.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# @agimon-ai/doompi-web
|
|
2
2
|
|
|
3
|
-
Web cockpit for DoomPi
|
|
4
|
-
running [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) through the session
|
|
5
|
-
registry, and multiplexes their authenticated sockets behind one page connection
|
|
3
|
+
Web cockpit for DoomPi. It discovers running [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) sessions, multiplexes their authenticated sockets, and serves them through one browser connection.
|
|
6
4
|
|
|
7
5
|
This package is a composable [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) subsystem.
|
|
8
6
|
It registers no Pi extension and adds nothing to an interactive session.
|
|
9
7
|
|
|
10
|
-
##
|
|
8
|
+
## How it runs
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
to add to `.doom/modes.yaml`. It is a standalone executable you run yourself, as shown under Run
|
|
14
|
-
below, or the process `doompi-server --web` starts for you.
|
|
10
|
+
`doompi-web` is a standalone process, not a Pi extension. Do not add it to `.doom/modes.yaml`. Run it directly, or start it through `doompi-server --web`.
|
|
15
11
|
|
|
16
12
|
## Requirements
|
|
17
13
|
|
|
18
14
|
- Node.js 22.19.0 or newer
|
|
19
|
-
-
|
|
15
|
+
- `doompi-server` sessions to attach to. The cockpit can start them itself.
|
|
20
16
|
|
|
21
17
|
## Install
|
|
22
18
|
|
|
@@ -30,37 +26,33 @@ npm install -g @agimon-ai/doompi-web
|
|
|
30
26
|
doompi-web
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
below is an override. `doompi-web` is the hub. It watches the session registry (`~/.doompi/run` by
|
|
35
|
-
default), attaches to every registered session, shows them all in the rail, and can start new ones
|
|
36
|
-
from the page. Sessions it starts run the `doompi-server` and the agent from this installation, so
|
|
37
|
-
the cockpit and the sessions it creates are always the same build.
|
|
29
|
+
Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and resolves the union of their synchronized cockpit compositions from `~/.pi/.doom/sync/registrations`. The directory that launches `doompi-web` does not select the bundle. Sessions started by the cockpit use the server and agent from the same installation.
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
exits, because one hub already serves every session.
|
|
31
|
+
If `~/.pi/.doom` does not exist, the command runs its bundled `doompi init` before starting. If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
|
|
41
32
|
|
|
42
|
-
|
|
33
|
+
To run this repository's build with its composition pinned and watched:
|
|
43
34
|
|
|
44
35
|
```bash
|
|
45
|
-
pnpm cockpit
|
|
36
|
+
pnpm cockpit:build
|
|
37
|
+
pnpm doompi-web --dir=<PWD>
|
|
46
38
|
```
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
this checkout's builds, which matters: a session on some other `doompi` is a different composition,
|
|
50
|
-
and its extensions, commands, and minor modes are not the ones being edited here, so a change looks
|
|
51
|
-
like it did nothing.
|
|
40
|
+
`--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
|
|
52
41
|
|
|
53
|
-
| Flag | Default
|
|
54
|
-
| ----------------- |
|
|
55
|
-
| `--
|
|
56
|
-
| `--
|
|
57
|
-
| `--
|
|
58
|
-
| `--
|
|
59
|
-
| `--
|
|
42
|
+
| Flag | Default | Meaning |
|
|
43
|
+
| ----------------- | ------------------------ | ------------------------------------------------ |
|
|
44
|
+
| `--dir` | live-session union | Pin, sync, and watch one repository composition |
|
|
45
|
+
| `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
|
|
46
|
+
| `--spawn-command` | this install | Command launching sessions created from the page |
|
|
47
|
+
| `--port` | `7433` | HTTP port |
|
|
48
|
+
| `--host` | `127.0.0.1` | Bind address |
|
|
49
|
+
| `--assets` | synchronized or packaged | Override the built SPA directory |
|
|
50
|
+
| `--state-dir` | `~/.doompi/web` | Remote-access settings and cockpit cache |
|
|
51
|
+
| `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
|
|
52
|
+
| `-h`, `--help` | | Print command help |
|
|
53
|
+
| `-v`, `--version` | | Print the installed package version |
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
yourself, install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server); the hub
|
|
63
|
-
does not need it on PATH to create sessions.
|
|
55
|
+
Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) when you also want to run a session server directly. The hub does not require `doompi-server` on `PATH` to create sessions.
|
|
64
56
|
|
|
65
57
|
## Security
|
|
66
58
|
|
|
@@ -69,6 +61,24 @@ browser. The server binds loopback by default, so reaching the cockpit already m
|
|
|
69
61
|
account on the machine. Remote access means tunnelling the port, for example over SSH; do not bind
|
|
70
62
|
a public address and call it done.
|
|
71
63
|
|
|
64
|
+
Loopback binding is not on its own an authorization boundary, because two request shapes escape it
|
|
65
|
+
and both reach code execution:
|
|
66
|
+
|
|
67
|
+
- **WebSockets are exempt from CORS.** Any page in any browser on this machine can open a socket to
|
|
68
|
+
the cockpit's port, and `session_command` frames carry prompts straight to an agent holding a
|
|
69
|
+
`bash` tool.
|
|
70
|
+
- **A cross-origin POST need not be preflighted.** Sent as `text/plain`, it still parses as JSON
|
|
71
|
+
server-side, so a page could spawn a session in any directory without ever reading a response.
|
|
72
|
+
|
|
73
|
+
Both are refused. Every request and every socket upgrade is checked against an origin and host
|
|
74
|
+
allowlist derived from the port actually bound, plus the vite dev server. A request carrying no
|
|
75
|
+
`Origin` is allowed, because curl, the health probe, and the test harness send none and a browser
|
|
76
|
+
always sends one where it matters; this defends against a hostile web page, not against a hostile
|
|
77
|
+
local program, which can send whatever headers it likes. `DOOMPI_WEB_ALLOW_ORIGIN` takes a
|
|
78
|
+
comma-separated list for setups this package cannot guess. The host check is also the DNS rebinding
|
|
79
|
+
defence, and it is the only check a socket upgrade gets, since the upgrade path never reaches the
|
|
80
|
+
server's own host parsing.
|
|
81
|
+
|
|
72
82
|
## What the cockpit shows
|
|
73
83
|
|
|
74
84
|
Everything comes from Pi's RPC protocol, so a bare Pi session and a DoomPi session both work and
|
|
@@ -85,13 +95,17 @@ neither is shown facts it did not report:
|
|
|
85
95
|
usage, cost, and session identity.
|
|
86
96
|
- **Dialogs:** the extension UI sub-protocol (`select`, `confirm`, `input`, `editor`), which is how
|
|
87
97
|
a permission prompt reaches the browser. Cancelling answers the agent rather than stranding it.
|
|
98
|
+
- **Notifications:** live `doom-notification` entries from every attached session, including sessions
|
|
99
|
+
that are not focused. Browser permission is requested only after the user clicks **allow notifications**
|
|
100
|
+
in Settings. Replayed backlog and historical pages update the timeline but never notify.
|
|
88
101
|
- **Leader Space:** `ctrl+k`, `ctrl+space`, or space in an empty composer. Keys walk the tree the
|
|
89
102
|
installed web plugins declared (`SPC w r` opens the workflows tab, `SPC g e` toggles goal mode),
|
|
90
103
|
backspace climbs, and `/` searches whatever `get_commands` reported to run it as a slash prompt.
|
|
91
|
-
- **Settings:** the gear at the foot of the rail opens the settings pages.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
- **Settings:** the gear at the foot of the rail opens the settings pages. Notifications shows this
|
|
105
|
+
browser's permission and provides the user-click permission action. Providers lists every model
|
|
106
|
+
provider Pi knows with whether it is authenticated, signs in with an API key or OAuth through the
|
|
107
|
+
hub, and signs out. The hub uses the same `auth.json` as the sessions, so a login here is a login for
|
|
108
|
+
every session on the machine.
|
|
95
109
|
|
|
96
110
|
## Attach and reattach
|
|
97
111
|
|
|
@@ -110,6 +124,205 @@ perform the handshake itself: `attach` frames sent from the browser are refused.
|
|
|
110
124
|
them.
|
|
111
125
|
- Subagent, workflow, and runner surfaces report summaries only; per-run detail has no RPC source
|
|
112
126
|
to render from yet.
|
|
127
|
+
- Open pages show full live browser notifications. An installed paired PWA can also receive a generic
|
|
128
|
+
zero-TTL Web Push alert while closed. There is no durable subscription database, outbox, replay, or
|
|
129
|
+
historical delivery.
|
|
130
|
+
|
|
131
|
+
## iPhone Home Screen app
|
|
132
|
+
|
|
133
|
+
Use a stable named HTTPS tunnel such as `doompi.agimon.win`. Quick-tunnel hostnames rotate, so they cannot
|
|
134
|
+
provide durable PWA identity, passkeys, or reliable Push subscriptions. Open `/pair` in Safari, add it to the
|
|
135
|
+
Home Screen, then launch the installed app and scan the QR shown by the host.
|
|
136
|
+
|
|
137
|
+
The install shell, scanner, manifest, and verifier worker are package-owned. The QR pins the host signing key
|
|
138
|
+
and minimum bundle revision. The worker verifies every host and plugin asset before committing it to the
|
|
139
|
+
active cache, and the host never serves its SPA directly. A failed update keeps the last-known-good bundle.
|
|
140
|
+
Signer loss, signer rotation, or a host/container transition requires explicit fingerprint confirmation and
|
|
141
|
+
re-pairing. Session file previews remain sealed `no-store` HTTP Blob URLs and never enter the application
|
|
142
|
+
cache, IndexedDB, a signed manifest, or a Push payload.
|
|
143
|
+
|
|
144
|
+
Closed-app alerts are opt-in under settings. The server keeps subscriptions only in memory and sends fixed
|
|
145
|
+
generic copy with `TTL: 0` only when the device has no connected cockpit socket. Opening the app after a host
|
|
146
|
+
restart re-registers the browser subscription. Disabling alerts, revoking or expiring the device, switching
|
|
147
|
+
off remote access, or rotating process credentials removes it.
|
|
148
|
+
|
|
149
|
+
## Remote access
|
|
150
|
+
|
|
151
|
+
Working from a phone means putting the cockpit on the internet, and the cockpit's socket carries
|
|
152
|
+
prompts to an agent holding a `bash` tool. Reaching it is therefore equivalent to a shell on this
|
|
153
|
+
machine, and the whole design follows from that.
|
|
154
|
+
|
|
155
|
+
Switching it on binds a **second loopback listener** on an ephemeral port and points `cloudflared`
|
|
156
|
+
at it. Port 7433 is untouched, so working at the keyboard is exactly as before, while everything
|
|
157
|
+
arriving on the tunnel listener has to prove it holds a paired session. The two listeners are the
|
|
158
|
+
mechanism, not an implementation detail: a tunnel connects from `127.0.0.1` like every local client,
|
|
159
|
+
so the socket a connection arrived on is the only thing about it that cannot be forged.
|
|
160
|
+
|
|
161
|
+
Pairing takes **two** things, not one:
|
|
162
|
+
|
|
163
|
+
1. The host shows a QR encoding `https://<tunnel-host>/pair#c=<code>&k=<channel-key>&s=<signer>&r=<revision>`.
|
|
164
|
+
Every value rides in the URL fragment, which no browser sends to a server, so the relay cannot replace the
|
|
165
|
+
channel or bundle key without changing what was shown on the host. Manual pairing requires comparing the
|
|
166
|
+
signing-key fingerprint shown on both devices.
|
|
167
|
+
2. Scanning raises an approve or deny prompt **on the host**. Nothing is paired until someone at the machine
|
|
168
|
+
answers. A screen is visible over a shoulder and in a screenshare, so the code alone is one factor for a
|
|
169
|
+
credential that ends in shell access.
|
|
170
|
+
|
|
171
|
+
A paired device then has the same powers as the person at the keyboard, with one exception: it
|
|
172
|
+
cannot mint pairing codes, approve devices, or change these settings. A device able to approve
|
|
173
|
+
devices could make its own access permanent, which is precisely what host confirmation prevents.
|
|
174
|
+
Turning remote access off and revoking a device are allowed remotely, because a panic button is
|
|
175
|
+
worth more from the couch than from the desk.
|
|
176
|
+
|
|
177
|
+
Two optional time limits, both **off by default** and both in the dialog:
|
|
178
|
+
|
|
179
|
+
- Close the tunnel automatically, after 60 minutes.
|
|
180
|
+
- Expire paired sessions, after 30 minutes idle or 12 hours total.
|
|
181
|
+
|
|
182
|
+
Either switch takes effect retroactively in both directions, because expiry is evaluated against the
|
|
183
|
+
setting in force at the moment of the check rather than stamped onto a session when it was made.
|
|
184
|
+
Turning expiry on therefore drops sessions that are already idle, and turning it off brings them
|
|
185
|
+
back.
|
|
186
|
+
|
|
187
|
+
With both off, a paired device keeps its session until it is revoked, remote access is switched off, or
|
|
188
|
+
the hub restarts. Switching remote access off always revokes every paired session, and that is not a
|
|
189
|
+
toggle.
|
|
190
|
+
|
|
191
|
+
Before reporting success, the tunnel is **self-tested through its own public URL**: the pairing page
|
|
192
|
+
must answer 200 and `/api/health` must answer 401. Anything else means the agent is on the public
|
|
193
|
+
internet unguarded, so the tunnel is killed immediately rather than warned about.
|
|
194
|
+
|
|
195
|
+
Requires `cloudflared` on `PATH` (`brew install cloudflared`). A quick tunnel needs no account but gets a new
|
|
196
|
+
hostname on every start. It does not provide a durable PWA, passkey, or Push identity, and Cloudflare does not
|
|
197
|
+
support SSE on quick tunnels, which breaks workflow and runner-log plugin surfaces. Use a stable named tunnel
|
|
198
|
+
for the Home Screen app.
|
|
199
|
+
|
|
200
|
+
If a named tunnel's connector exits unexpectedly, the hub makes three backoff restart attempts and
|
|
201
|
+
self-tests every replacement. The existing listener and paired sessions stay valid while recovery is
|
|
202
|
+
in progress. A quick tunnel cannot recover transparently because its origin changes, so it still fails
|
|
203
|
+
closed immediately. Exhausting named-tunnel recovery also switches remote access off and revokes the
|
|
204
|
+
paired sessions.
|
|
205
|
+
|
|
206
|
+
### Passkeys and step-up
|
|
207
|
+
|
|
208
|
+
On a named tunnel, a device can enrol a passkey once and then sign in with a gesture instead of
|
|
209
|
+
another QR. Two actions need a fresh gesture even from a live session: writing or clearing a model
|
|
210
|
+
provider's credential, and spawning a session in a directory of the caller's choosing. Both are
|
|
211
|
+
escalation paths out of "drive the agent" and into "own the machine". Ordinary prompting and tool
|
|
212
|
+
approval are deliberately ungated, because a biometric check in that loop gets trained away in a day.
|
|
213
|
+
|
|
214
|
+
A quick tunnel cannot carry a passkey: `rpID` is bound to the hostname and a quick tunnel's rotates
|
|
215
|
+
on every start, so a credential enrolled today would silently stop working tomorrow. The cockpit says
|
|
216
|
+
so rather than letting the ceremony fail in the browser.
|
|
217
|
+
|
|
218
|
+
### Tunnel-provider trust and payload privacy
|
|
219
|
+
|
|
220
|
+
Cloudflare terminates TLS and delivers the package-owned pairing and verifier bootstrap. A browser cannot
|
|
221
|
+
independently authenticate that first executable shell against the same edge that served it: a malicious
|
|
222
|
+
replacement could omit the worker and steal a QR fragment. DoomPi does not claim protection from a malicious
|
|
223
|
+
code-delivery edge.
|
|
224
|
+
|
|
225
|
+
After that bootstrap, **bundle signing is mandatory**. The QR pins the host ECDSA key and revision floor. The
|
|
226
|
+
worker verifies the signed manifest plus every raw asset before any host JavaScript executes, serves only the
|
|
227
|
+
atomically committed verified cache, and retains the last-known-good revision on failure. Cloudflare may
|
|
228
|
+
alter the raw bytes, but altered bytes do not execute.
|
|
229
|
+
|
|
230
|
+
**The payload is also sealed.** A QR carries an ephemeral P-256 public key in the fragment. A returning
|
|
231
|
+
passkey device restores its channel only after the pinned bundle worker accepts the same signer. Both paths
|
|
232
|
+
complete ECDH and derive separate AES-256-GCM keys per direction. Socket frames and API bodies travel as
|
|
233
|
+
ciphertext.
|
|
234
|
+
|
|
235
|
+
The channel implementation lives in `@agimon-ai/doompi-web-security`, which ships the envelope contract,
|
|
236
|
+
a `node:crypto` half, and a WebCrypto half. **A web plugin that calls `fetch` directly sends plaintext to
|
|
237
|
+
the relay**, so plugins use `sealedTransport.fetch` from that package's `./browser` subpath. The host
|
|
238
|
+
cannot enforce this, which is why the import allowlist admits the helper and why it is said here.
|
|
239
|
+
|
|
240
|
+
Cloudflare can still observe the served page and assets, timing, message sizes, connection patterns, and
|
|
241
|
+
tunnel metadata. Do not enable this transport if Cloudflare is outside your trust boundary. Protect a
|
|
242
|
+
named-tunnel account with a passkey or hardware MFA.
|
|
243
|
+
|
|
244
|
+
### What a paired device may browse
|
|
245
|
+
|
|
246
|
+
While a tunnel is up, the new-session picker answers a request arriving on it from one subtree only:
|
|
247
|
+
the directory `doompi-web` was started in. Unpinned, that route hands a paired device a map of the
|
|
248
|
+
machine, every project and client name and checkout, which is worth more to an attacker than most of
|
|
249
|
+
what the cockpit shows.
|
|
250
|
+
|
|
251
|
+
Only that request is pinned. The person at the keyboard already has a shell, so pinning them would
|
|
252
|
+
cost the picker its usefulness and buy nothing, and a contained cockpit needs none of it because its
|
|
253
|
+
mounts are already the boundary.
|
|
254
|
+
|
|
255
|
+
Session creation itself is not narrowed by this. `POST /api/sessions` still accepts any absolute path
|
|
256
|
+
a caller already knows, which is why the container below exists: it is the boundary, this is only the
|
|
257
|
+
end of casual enumeration.
|
|
258
|
+
|
|
259
|
+
### Running the cockpit in a container
|
|
260
|
+
|
|
261
|
+
Off by default. A second switch in the same dialog moves the whole cockpit into a container when
|
|
262
|
+
remote access is turned on: the hub, every session server it spawns, every agent, and `cloudflared`
|
|
263
|
+
itself. This is the answer to the blunt fact that a paired device drives an agent holding a shell,
|
|
264
|
+
which every layer above narrows access to but none of them contains.
|
|
265
|
+
|
|
266
|
+
The list of workspaces is the boundary, and it is the only thing on the host the container can see.
|
|
267
|
+
A hub inside the container cannot create a session in a path that is not mounted, so spawning in an
|
|
268
|
+
arbitrary directory is closed by construction rather than by a check. Not mounted, and deliberately
|
|
269
|
+
so: your home directory, `~/.ssh`, `~/.gitconfig`, the container socket, and every repository you
|
|
270
|
+
did not list.
|
|
271
|
+
|
|
272
|
+
What happens when you turn it on:
|
|
273
|
+
|
|
274
|
+
1. The image is built if this DoomPi version has not built it before, which takes minutes the first
|
|
275
|
+
time. Progress is printed in the terminal that started the cockpit.
|
|
276
|
+
2. The host cockpit answers the request, then stops serving and releases its port.
|
|
277
|
+
3. The container starts, publishing the same port on loopback, and becomes the cockpit. Your browser
|
|
278
|
+
reconnects on its own; there is no new address to remember.
|
|
279
|
+
4. Sessions working inside a mounted workspace are stopped on the host and recreated inside. Sessions
|
|
280
|
+
working anywhere else are named in the terminal and stay on the host, where the contained cockpit
|
|
281
|
+
cannot see them.
|
|
282
|
+
|
|
283
|
+
If the container fails to start, the host cockpit comes back and its sessions are recreated there, so
|
|
284
|
+
a failed move never leaves you with nothing listening. `Ctrl-C` stops the container. A cockpit killed
|
|
285
|
+
outright leaves the container running; the next start finds it through `~/.doompi/web/cockpit-container.json`
|
|
286
|
+
and stops it.
|
|
287
|
+
|
|
288
|
+
Two things carry across the boundary and nothing else does. Provider credentials go through the
|
|
289
|
+
existing broker, which hands the container a per-session token rather than your keys. Git identity is
|
|
290
|
+
passed as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`, so an agent can commit but cannot push, because no
|
|
291
|
+
key crosses.
|
|
292
|
+
|
|
293
|
+
What this does not defend against, stated plainly: anyone who can talk to the container daemon can
|
|
294
|
+
escape any container it runs, so the engine is part of the trusted base. Sessions share one container
|
|
295
|
+
and can read each other's mounted workspaces. A mounted workspace is fully writable, so the agent can
|
|
296
|
+
still destroy the repository you gave it. Network access is unrestricted, exactly as it already is for
|
|
297
|
+
`doompi --sandbox`. And plugin tabs fall back to the built-in set until a bundle is synced inside.
|
|
298
|
+
|
|
299
|
+
Requires `@agimon-ai/doompi-sandbox` in the composition and a container engine on the host.
|
|
300
|
+
|
|
301
|
+
## Planned: WebRTC transport
|
|
302
|
+
|
|
303
|
+
Today the page reaches the hub over two WebSockets, and any remote access means putting a tunnel in
|
|
304
|
+
front of them, which puts the tunnel provider in the path of everything. A WebRTC data channel would
|
|
305
|
+
replace that, with the tunnel demoted to carrying signaling only. Two independent reasons want it,
|
|
306
|
+
and the first is the stronger.
|
|
307
|
+
|
|
308
|
+
**Realtime voice.** `@agimon-ai/doompi-voice` now captures on the selected browser client, sends
|
|
309
|
+
mono PCM16 through its authenticated session media transport, keeps VAD and Whisper on the agent
|
|
310
|
+
host, and plays narration with browser speech synthesis. Voice activation is when the page asks for
|
|
311
|
+
microphone permission, never on load. A future WebRTC implementation can replace the HTTP media
|
|
312
|
+
adapter with Opus tracks while preserving the same client device and transport contracts.
|
|
313
|
+
|
|
314
|
+
**Confidentiality.** A QR shown on the host can carry the DTLS fingerprint, which authenticates the
|
|
315
|
+
peer connection out-of-band. The data path is then peer-to-peer DTLS: the tunnel provider usually
|
|
316
|
+
never carries the traffic and can never read it. That is close to VPN-grade confidentiality without
|
|
317
|
+
asking anyone to install a VPN.
|
|
318
|
+
|
|
319
|
+
The cost is real and worth stating: a WebRTC stack on the Node side (`node-datachannel` or
|
|
320
|
+
`werift`), a second client transport alongside `wsClient.ts` and `piTransport.ts`, ICE and NAT
|
|
321
|
+
traversal debugging, and a TURN relay fallback for the connections that cannot go direct. TURN
|
|
322
|
+
reintroduces a relay, though still one that cannot decrypt. The WebSocket transport stays as the
|
|
323
|
+
fallback for when a data channel cannot be established.
|
|
324
|
+
|
|
325
|
+
No date. This is a design note, not a commitment.
|
|
113
326
|
|
|
114
327
|
## Public API
|
|
115
328
|
|
|
@@ -159,14 +372,17 @@ package declares a `doompiWeb` block in its package.json naming a client entry (
|
|
|
159
372
|
definition: tab, panel, badge, session data channels, tool renderers for the timeline cards of
|
|
160
373
|
the tools the package registers, slots it opens for other plugins, fills into slots others open,
|
|
161
374
|
and the host's own slot contributions) and an optional hub entry (`webHubChannels` plus its built
|
|
162
|
-
dist). This package's own bundle carries no plugins; the full set is assembled on your machine
|
|
163
|
-
`doompi sync` discovers the installed composition's manifests, generates the import entries, and
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
375
|
+
dist). This package's own bundle carries no plugins; the full set is assembled on your machine.
|
|
376
|
+
`doompi sync` discovers the installed composition's manifests, generates the import entries, and builds
|
|
377
|
+
the SPA through the `./bundler` subpath inside the repository/worktree's immutable sync generation.
|
|
378
|
+
The shared server uses the validated registration for its startup repository. `--assets` and
|
|
379
|
+
`DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
|
|
380
|
+
`webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served
|
|
381
|
+
bundle. It names each plugin's built hub entry, imported lazily. A missing plugin package logs a notice
|
|
382
|
+
and its tab is omitted. Session package API routes use the session `cwd` registration, while
|
|
383
|
+
`DOOMPI_API_DIR` remains the explicit API override. `@agimon-ai/doompi-team` is the reference plugin;
|
|
384
|
+
the workflows tab ships with `@agimon-ai/doompi-workflow`. Contracts come from
|
|
385
|
+
`@agimon-ai/doompi-web-contracts`.
|
|
170
386
|
|
|
171
387
|
A tool call's timeline item belongs to the plugin that registers the tool: its `toolRenderers`
|
|
172
388
|
entry names the tool (or claims it with `matches` when the name is only known at runtime) and
|
|
@@ -180,7 +396,8 @@ Plugins are independent. A manifest names no other plugin and `registrationOrder
|
|
|
180
396
|
optional tiebreak (default 1000, then plugin id): every relation between two plugins resolves by
|
|
181
397
|
name once all of them are installed. A plugin opens slots named `<pluginId>.<name>` and renders
|
|
182
398
|
them with the `renderSlot` and `slotData` props its components receive; any plugin fills them, and
|
|
183
|
-
the host's `overlay`, `rail`, `selection-bar`, `
|
|
399
|
+
the host's `overlay`, `rail`, `selection-bar`, `composer-actions`, `activity`, and
|
|
400
|
+
`activity.<group>` slots, without
|
|
184
401
|
knowing whether the owner is installed. A fill into a slot nobody declares, or two plugins wanting
|
|
185
402
|
one tab id, tool name, group name, or Leader Space leaf, never fails the page: the install resolves
|
|
186
403
|
it (the first plugin keeps a shared name, a later binding takes a leaf over as the TUI documents)
|
package/dist/bin/serve.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/
|
|
2
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/doomInitialization.cjs"),c=require("../src/adapters/hubProbe.cjs"),l=require("../src/adapters/sessionRelaunch.cjs");let u=require("node:os");u=e.__toESM(u,1);function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await c.hubAnswers(t,n))return!0;return!1}async function p(){let e=i.parseServeOptions(process.argv.slice(2));if(e.help){process.stdout.write(i.serveHelp());return}if(e.version){process.stdout.write(`${r.packagedVersion()}\n`);return}await s.ensureDoomInitialized({homeDirectory:u.default.homedir(),onNotice:d});let o=`http://${e.host}:${String(e.port)}`,l=e.stateDir??n.defaultRemoteStateDir(),p=a.createCockpitContainer({stateDir:l,onNotice:d});await p.reapStale(),i.isLoopbackHost(e.host)||(d(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let h=await c.probeHub(e.host,e.port);if(h!==void 0){let t=r.packagedVersion(),n=h.version??`an older build`;if(h.version===t){d(`cockpit already running at ${o}`);return}if(h.sessions>0){d(`WARNING: ${n} is already running at ${o} with ${String(h.sessions)} live session(s).`),d(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(h.pid===void 0||!i.isLoopbackHost(e.host)){d(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),d(`WARNING: stop it and run again to serve ${t}.`);return}if(d(`replacing ${n} at ${o} with ${t}`),!await f(h.pid,e.host,e.port)){d(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let g=async()=>{},_=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,compositionDir:e.directory,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:u.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:l,cloudflaredPath:e.cloudflaredPath,onNotice:d,onHandover:t=>{m({serving:n,restart:_,container:p,handover:t,port:e.port,host:e.host,adopt:e=>{g=e}})}});return g=async()=>{await n.close()},n};try{await _()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await c.hubAnswers(e.host,e.port)){d(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let v=()=>{g().then(()=>{process.exit(0)})};process.on(`SIGINT`,v),process.on(`SIGTERM`,v)}async function m(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:d}),await l.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}p().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});
|
|
3
3
|
//# sourceMappingURL=serve.cjs.map
|
package/dist/bin/serve.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.cjs","names":["parseServeOptions","hubAnswers","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","os"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n let server: WebServer;\n try {\n server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n onNotice: notice,\n });\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void server.close().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";kRAWA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAUA,EAAAA,kBAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IAIzD,GAAI,MAAMC,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CAEA,IAAI,EACJ,GAAI,CACF,EAAS,MAAMC,EAAAA,SAAS,CACtB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAaC,EAAAA,mBAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAIC,EAAAA,kBACtB,QAASC,EAAAA,QAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,SAAU,CACZ,CAAC,CACH,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAMJ,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAY,MAAM,CAAC,CAAC,SAAW,CAC7B,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
|
|
1
|
+
{"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/bin/serve.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{
|
|
2
|
+
import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o,serveHelp as s}from"../src/services/serveOptions.mjs";import{createCockpitContainer as c}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as l}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as u}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as d,probeHub as f}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as p}from"../src/adapters/sessionRelaunch.mjs";import m from"node:os";function h(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function g(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await d(t,n))return!0;return!1}async function _(){let l=o(process.argv.slice(2));if(l.help){process.stdout.write(s());return}if(l.version){process.stdout.write(`${r()}\n`);return}await u({homeDirectory:m.homedir(),onNotice:h});let p=`http://${l.host}:${String(l.port)}`,_=l.stateDir??n(),y=c({stateDir:_,onNotice:h});await y.reapStale(),a(l.host)||(h(`WARNING: --host ${l.host} is not loopback, so the cockpit is reachable without pairing.`),h(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let b=await f(l.host,l.port);if(b!==void 0){let e=r(),t=b.version??`an older build`;if(b.version===e){h(`cockpit already running at ${p}`);return}if(b.sessions>0){h(`WARNING: ${t} is already running at ${p} with ${String(b.sessions)} live session(s).`),h(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(b.pid===void 0||!a(l.host)){h(`WARNING: ${t} is already running at ${p} and cannot be replaced automatically.`),h(`WARNING: stop it and run again to serve ${e}.`);return}if(h(`replacing ${t} at ${p} with ${e}`),!await g(b.pid,l.host,l.port)){h(`WARNING: the hub at ${p} did not stop; serve ${e} on another port with --port.`);return}}let x=async()=>{},S=async()=>{let n=await i({port:l.port,host:l.host,assetsDir:l.assetsDir,compositionDir:l.directory,registryDir:t({flagValue:l.registryDir,envValue:process.env[e],homeDir:m.homedir()}),spawnCommand:l.spawnCommand,remoteStateDir:_,cloudflaredPath:l.cloudflaredPath,onNotice:h,onHandover:e=>{v({serving:n,restart:S,container:y,handover:e,port:l.port,host:l.host,adopt:e=>{x=e}})}});return x=async()=>{await n.close()},n};try{await S()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await d(l.host,l.port)){h(`cockpit already running at ${p}`);return}throw Error(`Port ${String(l.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let C=()=>{x().then(()=>{process.exit(0)})};process.on(`SIGINT`,C),process.on(`SIGTERM`,C)}async function v(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:h});if(!t.ok){h(`the cockpit could not move into a container: ${t.error}`),h(`serving from the host again`),await e.restart(),await p({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:h});return}e.adopt(async()=>{await t.container.stop()}),await l({port:e.port,settings:e.handover.settings,onNotice:h}),await p({port:e.port,sessions:e.handover.sessions,onNotice:h}),h(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}_().catch(e=>{h(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
|
|
3
3
|
//# sourceMappingURL=serve.mjs.map
|
package/dist/bin/serve.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n let server: WebServer;\n try {\n server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n onNotice: notice,\n });\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void server.close().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";6SAWA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAU,EAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IAIzD,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CAEA,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAS,CACtB,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAa,EAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAI,GACtB,QAAS,EAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,SAAU,CACZ,CAAC,CACH,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAY,MAAM,CAAC,CAAC,SAAW,CAC7B,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
|
|
1
|
+
{"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.cjs";
|
|
2
2
|
import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.cjs";
|
|
3
|
-
import { serveWeb } from "./src/adapters/httpServer.cjs";
|
|
4
|
-
import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
|
|
5
3
|
import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.cjs";
|
|
6
4
|
import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.cjs";
|
|
5
|
+
import { serveWeb } from "./src/adapters/httpServer.cjs";
|
|
6
|
+
import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
|
|
7
7
|
export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.mjs";
|
|
2
2
|
import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.mjs";
|
|
3
|
-
import { serveWeb } from "./src/adapters/httpServer.mjs";
|
|
4
|
-
import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
|
|
5
3
|
import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.mjs";
|
|
6
4
|
import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.mjs";
|
|
5
|
+
import { serveWeb } from "./src/adapters/httpServer.mjs";
|
|
6
|
+
import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
|
|
7
7
|
export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="theme-color" content="#14161a" />
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
9
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
10
|
+
<link rel="manifest" href="/pwa/manifest.webmanifest" />
|
|
11
|
+
<link rel="apple-touch-icon" href="/pwa/icon-192.png" />
|
|
12
|
+
<title>Pair with DoomPi</title>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
color-scheme: dark;
|
|
16
|
+
}
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
min-height: 100dvh;
|
|
24
|
+
display: grid;
|
|
25
|
+
place-items: center;
|
|
26
|
+
padding: max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right))
|
|
27
|
+
max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left));
|
|
28
|
+
background: #14161a;
|
|
29
|
+
color: #e6e8ec;
|
|
30
|
+
font:
|
|
31
|
+
16px/1.5 ui-sans-serif,
|
|
32
|
+
system-ui,
|
|
33
|
+
-apple-system,
|
|
34
|
+
sans-serif;
|
|
35
|
+
}
|
|
36
|
+
main {
|
|
37
|
+
width: min(32rem, 100%);
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
h1 {
|
|
41
|
+
margin: 0 0 0.5rem;
|
|
42
|
+
font-size: 1.35rem;
|
|
43
|
+
}
|
|
44
|
+
p {
|
|
45
|
+
margin: 0 0 1rem;
|
|
46
|
+
color: #a2a8b4;
|
|
47
|
+
}
|
|
48
|
+
#pwa-status[data-tone='good'] {
|
|
49
|
+
color: #6bd68a;
|
|
50
|
+
}
|
|
51
|
+
#pwa-status[data-tone='bad'] {
|
|
52
|
+
color: #ff7b7b;
|
|
53
|
+
}
|
|
54
|
+
video {
|
|
55
|
+
width: 100%;
|
|
56
|
+
max-height: 24rem;
|
|
57
|
+
margin: 0.75rem 0;
|
|
58
|
+
border: 1px solid #2c313a;
|
|
59
|
+
border-radius: 0.75rem;
|
|
60
|
+
background: #090a0c;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
}
|
|
63
|
+
.actions {
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: 0.75rem;
|
|
66
|
+
margin-top: 1.25rem;
|
|
67
|
+
}
|
|
68
|
+
button {
|
|
69
|
+
flex: 1;
|
|
70
|
+
min-height: 3rem;
|
|
71
|
+
border: 1px solid #2f6feb;
|
|
72
|
+
border-radius: 0.6rem;
|
|
73
|
+
background: #2f6feb;
|
|
74
|
+
color: #fff;
|
|
75
|
+
font: inherit;
|
|
76
|
+
font-weight: 650;
|
|
77
|
+
}
|
|
78
|
+
button.secondary {
|
|
79
|
+
border-color: #2c313a;
|
|
80
|
+
background: #1b1e24;
|
|
81
|
+
}
|
|
82
|
+
button:disabled {
|
|
83
|
+
opacity: 0.55;
|
|
84
|
+
}
|
|
85
|
+
[hidden] {
|
|
86
|
+
display: none !important;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
89
|
+
<script type="module" crossorigin src="/pwa/pwa.js"></script>
|
|
90
|
+
</head>
|
|
91
|
+
<body data-marker="doompi-pwa-shell">
|
|
92
|
+
<main>
|
|
93
|
+
<h1>Pair this device</h1>
|
|
94
|
+
<p>This cockpit can run shell commands on the host. Pair only with your own DoomPi.</p>
|
|
95
|
+
<p id="pwa-status" role="status">Checking browser support.</p>
|
|
96
|
+
<video id="camera" playsinline muted hidden></video>
|
|
97
|
+
<div class="actions">
|
|
98
|
+
<button id="scan" type="button">Scan pairing QR</button>
|
|
99
|
+
<button id="cancel" class="secondary" type="button" hidden>Cancel</button>
|
|
100
|
+
</div>
|
|
101
|
+
<noscript><p>Pairing needs JavaScript.</p></noscript>
|
|
102
|
+
</main>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|