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