@agimon-ai/doompi-web 0.0.1-alpha.7 → 0.0.1-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +19 -15
  2. package/dist/bin/serve.cjs +1 -1
  3. package/dist/bin/serve.cjs.map +1 -1
  4. package/dist/bin/serve.mjs +1 -1
  5. package/dist/bin/serve.mjs.map +1 -1
  6. package/dist/src/adapters/doomInitialization.cjs +2 -0
  7. package/dist/src/adapters/doomInitialization.cjs.map +1 -0
  8. package/dist/src/adapters/doomInitialization.mjs +2 -0
  9. package/dist/src/adapters/doomInitialization.mjs.map +1 -0
  10. package/dist/src/adapters/httpServer.cjs +2 -2
  11. package/dist/src/adapters/httpServer.cjs.map +1 -1
  12. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  13. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  14. package/dist/src/adapters/httpServer.mjs +2 -2
  15. package/dist/src/adapters/httpServer.mjs.map +1 -1
  16. package/dist/src/adapters/registryWatcher.cjs +1 -1
  17. package/dist/src/adapters/registryWatcher.cjs.map +1 -1
  18. package/dist/src/adapters/registryWatcher.mjs +1 -1
  19. package/dist/src/adapters/registryWatcher.mjs.map +1 -1
  20. package/dist/src/adapters/syncGuard.cjs +1 -1
  21. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  22. package/dist/src/adapters/syncGuard.mjs +1 -1
  23. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  24. package/dist/src/adapters/webBundler.cjs +1 -1
  25. package/dist/src/adapters/webBundler.cjs.map +1 -1
  26. package/dist/src/adapters/webBundler.d.cts.map +1 -1
  27. package/dist/src/adapters/webBundler.d.mts.map +1 -1
  28. package/dist/src/adapters/webBundler.mjs +1 -1
  29. package/dist/src/adapters/webBundler.mjs.map +1 -1
  30. package/dist/src/adapters/webComposition.cjs +3 -0
  31. package/dist/src/adapters/webComposition.cjs.map +1 -0
  32. package/dist/src/adapters/webComposition.mjs +3 -0
  33. package/dist/src/adapters/webComposition.mjs.map +1 -0
  34. package/dist/src/adapters/webPluginScan.cjs +1 -1
  35. package/dist/src/adapters/webPluginScan.cjs.map +1 -1
  36. package/dist/src/adapters/webPluginScan.mjs +1 -1
  37. package/dist/src/adapters/webPluginScan.mjs.map +1 -1
  38. package/dist/src/services/serveOptions.cjs +14 -1
  39. package/dist/src/services/serveOptions.cjs.map +1 -1
  40. package/dist/src/services/serveOptions.d.cts +4 -0
  41. package/dist/src/services/serveOptions.d.cts.map +1 -1
  42. package/dist/src/services/serveOptions.d.mts +4 -0
  43. package/dist/src/services/serveOptions.d.mts.map +1 -1
  44. package/dist/src/services/serveOptions.mjs +14 -1
  45. package/dist/src/services/serveOptions.mjs.map +1 -1
  46. package/dist/src/types/bridge.d.cts +2 -0
  47. package/dist/src/types/bridge.d.cts.map +1 -1
  48. package/dist/src/types/bridge.d.mts +2 -0
  49. package/dist/src/types/bridge.d.mts.map +1 -1
  50. package/package.json +10 -10
  51. package/src/adapters/doomInitialization.ts +49 -0
  52. package/src/adapters/httpServer.ts +83 -56
  53. package/src/adapters/registryWatcher.ts +33 -30
  54. package/src/adapters/syncGuard.ts +10 -0
  55. package/src/adapters/webBundler.ts +3 -0
  56. package/src/adapters/webComposition.ts +240 -0
  57. package/src/adapters/webPluginScan.ts +6 -2
  58. package/src/bin/serve.ts +12 -1
  59. package/src/services/serveOptions.ts +45 -2
  60. package/src/types/bridge.ts +2 -0
package/README.md CHANGED
@@ -26,27 +26,31 @@ npm install -g @agimon-ai/doompi-web
26
26
  doompi-web
27
27
  ```
28
28
 
29
- Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and can start new sessions from the page. Sessions started by the cockpit use the server and agent from the same installation.
29
+ Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and resolves the union of their synchronized cockpit compositions from `~/.pi/.doom/sync/registrations`. The directory that launches `doompi-web` does not select the bundle. Sessions started by the cockpit use the server and agent from the same installation.
30
30
 
31
- If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
31
+ If `~/.pi/.doom` does not exist, the command runs its bundled `doompi init` before starting. If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
32
32
 
33
- To run this repository's build:
33
+ To run this repository's build with its composition pinned and watched:
34
34
 
35
35
  ```bash
36
- pnpm cockpit
36
+ pnpm cockpit:build
37
+ pnpm doompi-web --dir=<PWD>
37
38
  ```
38
39
 
39
- The command builds the required packages and starts the hub on port 7433. Sessions created from the page use this checkout's builds.
40
-
41
- | Flag | Default | Meaning |
42
- | ----------------- | --------------- | ------------------------------------------------ |
43
- | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
44
- | `--spawn-command` | this install | Command launching sessions created from the page |
45
- | `--port` | `7433` | HTTP port |
46
- | `--host` | `127.0.0.1` | Bind address |
47
- | `--assets` | bundled | Override the built SPA directory |
48
- | `--state-dir` | `~/.doompi/web` | Remote-access settings and the tunnel pid file |
49
- | `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
40
+ `--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
41
+
42
+ | Flag | Default | Meaning |
43
+ | ----------------- | ------------------------ | ------------------------------------------------ |
44
+ | `--dir` | live-session union | Pin, sync, and watch one repository composition |
45
+ | `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
46
+ | `--spawn-command` | this install | Command launching sessions created from the page |
47
+ | `--port` | `7433` | HTTP port |
48
+ | `--host` | `127.0.0.1` | Bind address |
49
+ | `--assets` | synchronized or packaged | Override the built SPA directory |
50
+ | `--state-dir` | `~/.doompi/web` | Remote-access settings and cockpit cache |
51
+ | `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
52
+ | `-h`, `--help` | | Print command help |
53
+ | `-v`, `--version` | | Print the installed package version |
50
54
 
51
55
  Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) when you also want to run a session server directly. The hub does not require `doompi-server` on `PATH` to create sessions.
52
56
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/hubProbe.cjs"),c=require("../src/adapters/sessionRelaunch.cjs");let l=require("node:os");l=e.__toESM(l,1);function u(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function d(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 s.hubAnswers(t,n))return!0;return!1}async function f(){let e=i.parseServeOptions(process.argv.slice(2)),o=`http://${e.host}:${String(e.port)}`,c=e.stateDir??n.defaultRemoteStateDir(),f=a.createCockpitContainer({stateDir:c,onNotice:u});await f.reapStale(),i.isLoopbackHost(e.host)||(u(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),u(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let m=await s.probeHub(e.host,e.port);if(m!==void 0){let t=r.packagedVersion(),n=m.version??`an older build`;if(m.version===t){u(`cockpit already running at ${o}`);return}if(m.sessions>0){u(`WARNING: ${n} is already running at ${o} with ${String(m.sessions)} live session(s).`),u(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(m.pid===void 0||!i.isLoopbackHost(e.host)){u(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),u(`WARNING: stop it and run again to serve ${t}.`);return}if(u(`replacing ${n} at ${o} with ${t}`),!await d(m.pid,e.host,e.port)){u(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let h=async()=>{},g=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:l.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:c,cloudflaredPath:e.cloudflaredPath,onNotice:u,onHandover:t=>{p({serving:n,restart:g,container:f,handover:t,port:e.port,host:e.host,adopt:e=>{h=e}})}});return h=async()=>{await n.close()},n};try{await g()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await s.hubAnswers(e.host,e.port)){u(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let _=()=>{h().then(()=>{process.exit(0)})};process.on(`SIGINT`,_),process.on(`SIGTERM`,_)}async function p(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:u});if(!t.ok){u(`the cockpit could not move into a container: ${t.error}`),u(`serving from the host again`),await e.restart(),await c.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:u});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:u}),await c.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:u}),u(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}f().catch(e=>{u(e instanceof Error?e.message:String(e)),process.exitCode=1});
2
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/doomInitialization.cjs"),c=require("../src/adapters/hubProbe.cjs"),l=require("../src/adapters/sessionRelaunch.cjs");let u=require("node:os");u=e.__toESM(u,1);function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await c.hubAnswers(t,n))return!0;return!1}async function p(){let e=i.parseServeOptions(process.argv.slice(2));if(e.help){process.stdout.write(i.serveHelp());return}if(e.version){process.stdout.write(`${r.packagedVersion()}\n`);return}await s.ensureDoomInitialized({homeDirectory:u.default.homedir(),onNotice:d});let o=`http://${e.host}:${String(e.port)}`,l=e.stateDir??n.defaultRemoteStateDir(),p=a.createCockpitContainer({stateDir:l,onNotice:d});await p.reapStale(),i.isLoopbackHost(e.host)||(d(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let h=await c.probeHub(e.host,e.port);if(h!==void 0){let t=r.packagedVersion(),n=h.version??`an older build`;if(h.version===t){d(`cockpit already running at ${o}`);return}if(h.sessions>0){d(`WARNING: ${n} is already running at ${o} with ${String(h.sessions)} live session(s).`),d(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(h.pid===void 0||!i.isLoopbackHost(e.host)){d(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),d(`WARNING: stop it and run again to serve ${t}.`);return}if(d(`replacing ${n} at ${o} with ${t}`),!await f(h.pid,e.host,e.port)){d(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let g=async()=>{},_=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,compositionDir:e.directory,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:u.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:l,cloudflaredPath:e.cloudflaredPath,onNotice:d,onHandover:t=>{m({serving:n,restart:_,container:p,handover:t,port:e.port,host:e.host,adopt:e=>{g=e}})}});return g=async()=>{await n.close()},n};try{await _()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await c.hubAnswers(e.host,e.port)){d(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let v=()=>{g().then(()=>{process.exit(0)})};process.on(`SIGINT`,v),process.on(`SIGTERM`,v)}async function m(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:d}),await l.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}p().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});
3
3
  //# sourceMappingURL=serve.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","packagedVersion","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, 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 } 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 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 registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o}from"../src/services/serveOptions.mjs";import{createCockpitContainer as s}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as c}from"../src/adapters/cockpitHandoff.mjs";import{hubAnswers as l,probeHub as u}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as d}from"../src/adapters/sessionRelaunch.mjs";import f from"node:os";function p(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function m(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 l(t,n))return!0;return!1}async function h(){let c=o(process.argv.slice(2)),d=`http://${c.host}:${String(c.port)}`,h=c.stateDir??n(),_=s({stateDir:h,onNotice:p});await _.reapStale(),a(c.host)||(p(`WARNING: --host ${c.host} is not loopback, so the cockpit is reachable without pairing.`),p(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let v=await u(c.host,c.port);if(v!==void 0){let e=r(),t=v.version??`an older build`;if(v.version===e){p(`cockpit already running at ${d}`);return}if(v.sessions>0){p(`WARNING: ${t} is already running at ${d} with ${String(v.sessions)} live session(s).`),p(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(v.pid===void 0||!a(c.host)){p(`WARNING: ${t} is already running at ${d} and cannot be replaced automatically.`),p(`WARNING: stop it and run again to serve ${e}.`);return}if(p(`replacing ${t} at ${d} with ${e}`),!await m(v.pid,c.host,c.port)){p(`WARNING: the hub at ${d} did not stop; serve ${e} on another port with --port.`);return}}let y=async()=>{},b=async()=>{let n=await i({port:c.port,host:c.host,assetsDir:c.assetsDir,registryDir:t({flagValue:c.registryDir,envValue:process.env[e],homeDir:f.homedir()}),spawnCommand:c.spawnCommand,remoteStateDir:h,cloudflaredPath:c.cloudflaredPath,onNotice:p,onHandover:e=>{g({serving:n,restart:b,container:_,handover:e,port:c.port,host:c.host,adopt:e=>{y=e}})}});return y=async()=>{await n.close()},n};try{await b()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await l(c.host,c.port)){p(`cockpit already running at ${d}`);return}throw Error(`Port ${String(c.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let x=()=>{y().then(()=>{process.exit(0)})};process.on(`SIGINT`,x),process.on(`SIGTERM`,x)}async function g(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:p});if(!t.ok){p(`the cockpit could not move into a container: ${t.error}`),p(`serving from the host again`),await e.restart(),await d({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:p});return}e.adopt(async()=>{await t.container.stop()}),await c({port:e.port,settings:e.handover.settings,onNotice:p}),await d({port:e.port,sessions:e.handover.sessions,onNotice:p}),p(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}h().catch(e=>{p(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
2
+ import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o,serveHelp as s}from"../src/services/serveOptions.mjs";import{createCockpitContainer as c}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as l}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as u}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as d,probeHub as f}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as p}from"../src/adapters/sessionRelaunch.mjs";import m from"node:os";function h(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function g(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await d(t,n))return!0;return!1}async function _(){let l=o(process.argv.slice(2));if(l.help){process.stdout.write(s());return}if(l.version){process.stdout.write(`${r()}\n`);return}await u({homeDirectory:m.homedir(),onNotice:h});let p=`http://${l.host}:${String(l.port)}`,_=l.stateDir??n(),y=c({stateDir:_,onNotice:h});await y.reapStale(),a(l.host)||(h(`WARNING: --host ${l.host} is not loopback, so the cockpit is reachable without pairing.`),h(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let b=await f(l.host,l.port);if(b!==void 0){let e=r(),t=b.version??`an older build`;if(b.version===e){h(`cockpit already running at ${p}`);return}if(b.sessions>0){h(`WARNING: ${t} is already running at ${p} with ${String(b.sessions)} live session(s).`),h(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(b.pid===void 0||!a(l.host)){h(`WARNING: ${t} is already running at ${p} and cannot be replaced automatically.`),h(`WARNING: stop it and run again to serve ${e}.`);return}if(h(`replacing ${t} at ${p} with ${e}`),!await g(b.pid,l.host,l.port)){h(`WARNING: the hub at ${p} did not stop; serve ${e} on another port with --port.`);return}}let x=async()=>{},S=async()=>{let n=await i({port:l.port,host:l.host,assetsDir:l.assetsDir,compositionDir:l.directory,registryDir:t({flagValue:l.registryDir,envValue:process.env[e],homeDir:m.homedir()}),spawnCommand:l.spawnCommand,remoteStateDir:_,cloudflaredPath:l.cloudflaredPath,onNotice:h,onHandover:e=>{v({serving:n,restart:S,container:y,handover:e,port:l.port,host:l.host,adopt:e=>{x=e}})}});return x=async()=>{await n.close()},n};try{await S()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await d(l.host,l.port)){h(`cockpit already running at ${p}`);return}throw Error(`Port ${String(l.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let C=()=>{x().then(()=>{process.exit(0)})};process.on(`SIGINT`,C),process.on(`SIGTERM`,C)}async function v(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:h});if(!t.ok){h(`the cockpit could not move into a container: ${t.error}`),h(`serving from the host again`),await e.restart(),await p({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:h});return}e.adopt(async()=>{await t.container.stop()}),await l({port:e.port,settings:e.handover.settings,onNotice:h}),await p({port:e.port,sessions:e.handover.sessions,onNotice:h}),h(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}_().catch(e=>{h(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
3
3
  //# sourceMappingURL=serve.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { hubAnswers, 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 } 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 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 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.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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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-config"),i=require("node:child_process"),a=require("node:module"),o=require("node:os");o=e.__toESM(o,1);const s=[`dist`,`bin`,`cli.mjs`];function c(){let e=n.default.dirname((0,a.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`));return n.default.join(e,...s)}function l(){return new Promise((e,t)=>{let n=(0,i.spawn)(process.execPath,[c(),`init`],{cwd:o.default.homedir(),stdio:[`ignore`,`inherit`,`inherit`]});n.once(`error`,e=>t(Error(`doompi init could not start: ${e.message}`,{cause:e}))),n.once(`exit`,n=>{n===0?e():t(Error(`doompi init exited with code ${String(n)}`))})})}async function u(e={}){let n=e.homeDirectory??o.default.homedir(),i=(0,r.globalDoomConfigDirectory)(n);return!(e.exists??t.default.existsSync)(i)&&(e.onNotice?.(`${i} is missing; running doompi init`),await(e.runInit??l)(),!0)}exports.ensureDoomInitialized=u;
2
+ //# sourceMappingURL=doomInitialization.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doomInitialization.cjs","names":["path","createRequire","spawn","os","globalDoomConfigDirectory","fs"],"sources":["../../../src/adapters/doomInitialization.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { globalDoomConfigDirectory } from '@agimon-ai/doompi-config';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\n\nexport interface DoomInitializationOptions {\n homeDirectory?: string;\n onNotice?: (message: string) => void;\n /** Test seam over the global configuration directory check. */\n exists?: (directory: string) => boolean;\n /** Test seam over the child command. */\n runInit?: () => Promise<void>;\n}\n\nfunction bundledDoomPiCli(): string {\n const packageRoot = path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`));\n return path.join(packageRoot, ...CLI_SEGMENTS);\n}\n\nfunction runBundledInit(): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [bundledDoomPiCli(), 'init'], {\n cwd: os.homedir(),\n stdio: ['ignore', 'inherit', 'inherit'],\n });\n child.once('error', (error) =>\n reject(new Error(`doompi init could not start: ${error.message}`, { cause: error })),\n );\n child.once('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`doompi init exited with code ${String(code)}`));\n });\n });\n}\n\n/** Ensures the global configuration and managed Pi integration exist before the web hub starts. */\nexport async function ensureDoomInitialized(options: DoomInitializationOptions = {}): Promise<boolean> {\n const homeDirectory = options.homeDirectory ?? os.homedir();\n const configDirectory = globalDoomConfigDirectory(homeDirectory);\n if ((options.exists ?? fs.existsSync)(configDirectory)) return false;\n options.onNotice?.(`${configDirectory} is missing; running doompi init`);\n await (options.runInit ?? runBundledInit)();\n return true;\n}\n"],"mappings":"uRAOA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EAW9C,SAAS,GAA2B,CAClC,IAAM,EAAcA,EAAAA,QAAK,SAAA,EAAQC,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACzG,OAAOD,EAAAA,QAAK,KAAK,EAAa,GAAG,CAAY,CAC/C,CAEA,SAAS,GAAgC,CACvC,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,GAAA,EAAQE,EAAAA,MAAAA,CAAM,QAAQ,SAAU,CAAC,EAAiB,EAAG,MAAM,EAAG,CAClE,IAAKC,EAAAA,QAAG,QAAQ,EAChB,MAAO,CAAC,SAAU,UAAW,SAAS,CACxC,CAAC,EACD,EAAM,KAAK,QAAU,GACnB,EAAW,MAAM,gCAAgC,EAAM,UAAW,CAAE,MAAO,CAAM,CAAC,CAAC,CACrF,EACA,EAAM,KAAK,OAAS,GAAS,CACvB,IAAS,EAAG,EAAQ,EACnB,EAAW,MAAM,gCAAgC,OAAO,CAAI,GAAG,CAAC,CACvE,CAAC,CACH,CAAC,CACH,CAGA,eAAsB,EAAsB,EAAqC,CAAC,EAAqB,CACrG,IAAM,EAAgB,EAAQ,eAAiBA,EAAAA,QAAG,QAAQ,EACpD,GAAA,EAAkBC,EAAAA,0BAAAA,CAA0B,CAAa,EAI/D,MAHA,EAAK,EAAQ,QAAUC,EAAAA,QAAG,WAAA,CAAY,CAAe,IACrD,EAAQ,WAAW,GAAG,EAAgB,iCAAiC,EACvE,MAAO,EAAQ,SAAW,EAAA,CAAgB,EACnC,GACT"}
@@ -0,0 +1,2 @@
1
+ import{createRequire as e}from"node:module";import t from"node:fs";import n from"node:path";import{globalDoomConfigDirectory as r}from"@agimon-ai/doompi-config";import{spawn as i}from"node:child_process";import a from"node:os";const o=[`dist`,`bin`,`cli.mjs`];function s(){let t=n.dirname(e(import.meta.url).resolve(`@agimon-ai/doompi/package.json`));return n.join(t,...o)}function c(){return new Promise((e,t)=>{let n=i(process.execPath,[s(),`init`],{cwd:a.homedir(),stdio:[`ignore`,`inherit`,`inherit`]});n.once(`error`,e=>t(Error(`doompi init could not start: ${e.message}`,{cause:e}))),n.once(`exit`,n=>{n===0?e():t(Error(`doompi init exited with code ${String(n)}`))})})}async function l(e={}){let n=e.homeDirectory??a.homedir(),i=r(n);return!(e.exists??t.existsSync)(i)&&(e.onNotice?.(`${i} is missing; running doompi init`),await(e.runInit??c)(),!0)}export{l as ensureDoomInitialized};
2
+ //# sourceMappingURL=doomInitialization.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doomInitialization.mjs","names":[],"sources":["../../../src/adapters/doomInitialization.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { globalDoomConfigDirectory } from '@agimon-ai/doompi-config';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\n\nexport interface DoomInitializationOptions {\n homeDirectory?: string;\n onNotice?: (message: string) => void;\n /** Test seam over the global configuration directory check. */\n exists?: (directory: string) => boolean;\n /** Test seam over the child command. */\n runInit?: () => Promise<void>;\n}\n\nfunction bundledDoomPiCli(): string {\n const packageRoot = path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`));\n return path.join(packageRoot, ...CLI_SEGMENTS);\n}\n\nfunction runBundledInit(): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [bundledDoomPiCli(), 'init'], {\n cwd: os.homedir(),\n stdio: ['ignore', 'inherit', 'inherit'],\n });\n child.once('error', (error) =>\n reject(new Error(`doompi init could not start: ${error.message}`, { cause: error })),\n );\n child.once('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`doompi init exited with code ${String(code)}`));\n });\n });\n}\n\n/** Ensures the global configuration and managed Pi integration exist before the web hub starts. */\nexport async function ensureDoomInitialized(options: DoomInitializationOptions = {}): Promise<boolean> {\n const homeDirectory = options.homeDirectory ?? os.homedir();\n const configDirectory = globalDoomConfigDirectory(homeDirectory);\n if ((options.exists ?? fs.existsSync)(configDirectory)) return false;\n options.onNotice?.(`${configDirectory} is missing; running doompi init`);\n await (options.runInit ?? runBundledInit)();\n return true;\n}\n"],"mappings":"mOAOA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EAW9C,SAAS,GAA2B,CAClC,IAAM,EAAc,EAAK,QAAQ,EAAc,YAAY,GAAG,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACzG,OAAO,EAAK,KAAK,EAAa,GAAG,CAAY,CAC/C,CAEA,SAAS,GAAgC,CACvC,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,EAAQ,EAAM,QAAQ,SAAU,CAAC,EAAiB,EAAG,MAAM,EAAG,CAClE,IAAK,EAAG,QAAQ,EAChB,MAAO,CAAC,SAAU,UAAW,SAAS,CACxC,CAAC,EACD,EAAM,KAAK,QAAU,GACnB,EAAW,MAAM,gCAAgC,EAAM,UAAW,CAAE,MAAO,CAAM,CAAC,CAAC,CACrF,EACA,EAAM,KAAK,OAAS,GAAS,CACvB,IAAS,EAAG,EAAQ,EACnB,EAAW,MAAM,gCAAgC,OAAO,CAAI,GAAG,CAAC,CACvE,CAAC,CACH,CAAC,CACH,CAGA,eAAsB,EAAsB,EAAqC,CAAC,EAAqB,CACrG,IAAM,EAAgB,EAAQ,eAAiB,EAAG,QAAQ,EACpD,EAAkB,EAA0B,CAAa,EAI/D,MAHA,EAAK,EAAQ,QAAU,EAAG,WAAA,CAAY,CAAe,IACrD,EAAQ,WAAW,GAAG,EAAgB,iCAAiC,EACvE,MAAO,EAAQ,SAAW,EAAA,CAAgB,EACnC,GACT"}
@@ -1,3 +1,3 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./piHubService.cjs"),n=require("./syncGuard.cjs"),r=require("./piWebSocketListener.cjs"),i=require("../types/media.cjs"),a=require("../services/fileMedia.cjs"),o=require("../services/staticAssets.cjs"),s=require("../types/hub.cjs"),c=require("../types/notification.cjs");require("../types/session.cjs");const l=require("./gitStatus.cjs"),u=require("./registryWatcher.cjs"),d=require("./serverSpawner.cjs"),f=require("./packageApiProxy.cjs"),p=require("./sessionHub.cjs"),m=require("../types/remoteAccess.cjs"),h=require("../services/remoteGuardPolicy.cjs"),g=require("../services/sessionMigration.cjs"),ee=require("./authRoutes.cjs"),te=require("./settingsRoutes.cjs"),ne=require("./providerAuth.cjs"),re=require("./remoteGuard.cjs"),ie=require("./remoteAccess.cjs"),ae=require("./bundlePublication.cjs"),oe=require("./webPush.cjs"),se=require("./remoteAccessStore.cjs"),ce=require("./remoteRoutes.cjs"),_=require("./tunnelProcess.cjs"),le=require("./directoryListing.cjs"),v=require("./sessionFiles.cjs"),ue=require("./threadJournals.cjs"),de=require("./webHubPluginLoader.cjs"),y=require("./webTelemetry.cjs");let b=require("node:fs");b=e.__toESM(b,1);let x=require("node:path");x=e.__toESM(x,1);let S=require("node:url"),C=require("@agimon-ai/doompi/services"),fe=require("node:crypto"),pe=require("@hono/node-server"),me=require("@hono/node-ws"),w=require("@earendil-works/pi-coding-agent"),T=require("@earendil-works/pi-server"),he=require("@agimon-ai/doompi/services/syncState"),E=require("@agimon-ai/doompi-web-security"),ge=require("hono"),D=require("hono/cookie"),O=require("hono/body-limit"),k=require("@agimon-ai/doompi-extension-contracts/package-api"),A=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),j=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),M=require("@agimon-ai/doompi/utils/repository");const N=8388608,P=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),F=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function _e(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!P.has(t.method)||typeof t.target!=`string`||!t.target.startsWith(`/`)||t.target.startsWith(`//`)||t.target.includes(`#`)||!Array.isArray(t.headers)||!t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))||t.body!==void 0&&typeof t.body!=`string`)return;let n=new URL(t.target,`http://sealed.doompi.invalid`);if(n.pathname!==m.REMOTE_HTTP_ROUTE&&n.pathname!==m.REMOTE_CHANNEL_ROUTE)return t}function ve(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();F.has(e)||e.startsWith(`sec-`)||n.append(t,r)}for(let e of[`cookie`,`host`,`origin`]){let r=t.req.header(e);r!==void 0&&n.set(e,r)}return n}catch{return}}function ye(e){if(e===void 0)return Buffer.alloc(0);if(!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(e))return;let t=Buffer.from(e,`base64`);return t.toString(`base64`)===e?t:void 0}function I(e){let t=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(b.default.existsSync(x.default.join(t,`package.json`)))return x.default.join(t,`dist`,e);let n=x.default.dirname(t);if(n===t)throw Error(`doompi-web could not locate its own package root.`);t=n}}function L(){return I(`web`)}function be(){return I(`pwa`)}function R(){let e=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let t=x.default.join(e,`package.json`);if(b.default.existsSync(t))try{let e=JSON.parse(b.default.readFileSync(t,`utf8`)),n=B(e)?e.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=x.default.dirname(e);if(n===e)return`unknown`;e=n}}function xe(e,t,n){if(e!==void 0)return e;let r=process.env.DOOMPI_WEB_DIST;if(r!==void 0&&r!==``)return r;let i=t?.webDirectory;return i!=null&&b.default.existsSync(x.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):L()}function z(e){try{return b.default.statSync(e).isFile()?b.default.readFileSync(e):void 0}catch{return}}function B(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Se(e,t){let n=e.firstMessage.split(`
2
- `,1)[0]?.trim();return(e.name?.trim()||n||x.default.basename(t)||`untitled`).slice(0,80)}function V(e){return e instanceof Error?e.message:String(e)}function H(e,t){return`${e}\n${t}`}function Ce(e,t,n,r){return p.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function U(e){return`repo-${(0,fe.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function W(e){let t=new Map,n=()=>{for(let[e,n]of t)t.set(e,{...n,repository:{...n.repository,active:!1}});for(let n of e.records())try{let e=b.default.realpathSync((0,M.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:U(e),path:e,name:x.default.basename(e)||e,active:!0},seenAt:Date.now()})}catch{}let n=[...t.entries()].filter(([,e])=>!e.repository.active).sort((e,t)=>t[1].seenAt-e[1].seenAt);for(let[e]of n.slice(12))t.delete(e);return[...t.values()].sort((e,t)=>Number(t.repository.active)-Number(e.repository.active)||t.seenAt-e.seenAt||e.repository.path.localeCompare(t.repository.path)).map(e=>e.repository)};return{list:n,resolve(e){return n().find(t=>t.id===e)?.path}}}function we(e,t,n,r,i){let a=[];for(let o of t){let t=`${k.DOOM_API_ROUTE_PREFIX}/${o.basePath}`,s;try{s=o.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${o.basePath}' did not start (${V(e)}); its routes stay unmounted`);continue}a.push(s),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await s.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${o.basePath}' failed on ${r.pathname} (${V(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function Te(e,t,n,r){e.all(`${k.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=y.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:y.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${V(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function G(e){let l=e.host??`127.0.0.1`,u=e.onNotice??(()=>{}),d=y.createWebTelemetry(),f;try{f=(0,M.findRepositoryRoot)(process.cwd())}catch{f=void 0}let p=n.createSyncGuard({...f===void 0?{cwd:process.cwd()}:{repoRoot:f},onNotice:u});await p.ensureSynced();let b;if(f!==void 0)try{b=(0,C.readSyncRegistration)(f)}catch(e){u(`synced registration is unavailable (${V(e)})`)}let S={assetsDir:xe(e.assetsDir,b,u),apiDirectory:b?.apiDirectory},k=Ce(e,u,await de.loadHubChannels(S.assetsDir,u),d),P=ue.createThreadJournals({resolve:(e,t)=>k.threadJournal(e,t),onNotice:u}),F=new Set,I=(e,t)=>{for(let n of F)t&&!n.local||n.post(e)},L=new ge.Hono,U=(0,me.createNodeWebSocket)({app:L}),G,K=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:u}),q=be(),J=ae.createBundlePublication({assetsDir:()=>S.assetsDir,stateDir:K.directory,onNotice:u}),Y;_.reapStaleTunnel(K.directory,u);let Ee=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(k.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of g.describeStranded(n.stranded))u(e);for(let e of n.migrate){let t=k.stop(e.id);t.ok||u(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},X=ie.createRemoteAccess({store:K,launchTunnel:e.remoteAccess?.launchTunnel??_.createTunnelLauncher({stateDir:K.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:u,onExit:e=>{u(`remote access: ${e}`),X.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,pe.serve)({fetch:L.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{U.injectWebSocket(n),e({port:t.port,close:async()=>await new Promise(e=>{n.closeAllConnections?.(),n.close(()=>e())})})}),r=n;r.maxConnections=64,r.maxRequestsPerSocket=100,n.once(`error`,t)}),bundleTrust:()=>J.trust(),onDeviceDropped:e=>Y?.remove(e),onNotice:u,...Ee===void 0?{}:{requestHandover:Ee},contained:(0,j.insideSandbox)(process.env),broadcastLocal:e=>I(e,!0),broadcastAll:e=>I(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});Y=oe.createLiveWebPush({stateDir:K.directory,isConnected:e=>X.isDeviceConnected(e),onNotice:u});let De=k.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&Y?.notify()}),Z=re.createRemoteGuard({loopbackPort:()=>G,tunnelPolicy:()=>X.tunnelPolicy(),authorize:e=>X.authorize((0,D.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>X.channelFor(e,t)!==void 0,stepUp:{required:e=>X.stepUpRequired(e),verify:async(e,t,n)=>{if(typeof n!=`object`||!n)return!1;let r=n.ceremonyId,i=n.response;if(typeof r!=`string`)return!1;let a=e.env?.sealedDeviceId??X.authorize((0,D.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await X.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});L.use(`*`,Z.middleware),L.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=y.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await d.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await d.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},y.readTraceContext(e.req.raw.headers))}),L.use(`*`,async(e,t)=>{if(Z.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>N?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(L,{remote:X,listenerOf:e=>Z.listenerOf(e)});let Oe=(0,O.bodyLimit)({maxSize:N,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});L.use(`*`,async(e,t)=>Z.listenerOf(e)===`local`?t():await Oe(e,t));let ke=(0,O.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Ae=y.createBrowserTelemetryRateLimit();L.post(y.WEB_TELEMETRY_ROUTE,ke,async e=>{if(!Ae())return e.json({error:`Too many telemetry batches.`},429);let t;try{t=await e.req.json()}catch{return e.json({error:`The telemetry batch must be JSON.`},400)}let n=y.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await d.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),L.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=X.authorize((0,D.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=X.channelFor(t,`http`);if(n===void 0)return e.json({error:`This device has no sealed HTTP channel.`},401);let r;try{r=await e.req.json()}catch{return e.json({error:`The sealed request envelope was malformed.`},400)}let i=n.open(r);if(!i.ok)return e.json({error:`The sealed request was refused: ${i.failure}.`},400);let a;try{a=JSON.parse(new TextDecoder().decode(i.plaintext))}catch{return e.json({error:`The sealed HTTP request was malformed.`},400)}let o=_e(a),s=o===void 0?void 0:ve(o,e),c=o===void 0?void 0:ye(o.body);if(o===void 0||s===void 0||c===void 0)return e.json({error:`The sealed HTTP request was invalid.`},400);let l=new URL(o.target,new URL(e.req.url).origin),u=await L.request(l,{method:o.method,headers:s,...o.method===`GET`||o.method===`HEAD`?{}:{body:Uint8Array.from(c).buffer}},{...e.env,sealedDeviceId:t}),d=Buffer.from(await u.arrayBuffer()),f=n.seal(new TextEncoder().encode(JSON.stringify({v:1,status:u.status,headers:Array.from(u.headers.entries()),body:d.toString(`base64`)})));return f.ok?e.json(f.envelope):e.json({error:`The sealed response failed: ${f.failure}.`},503)});let je=(0,O.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Q=e=>e.env?.sealedDeviceId;L.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Q(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:Y?.publicKey()},200,{"Cache-Control":`no-store`})),L.post(m.REMOTE_PUSH_ROUTE,je,async e=>{let t=Q(e);if(t===void 0)return e.json({error:`A sealed paired device is required.`},401);let n;try{n=await e.req.json()}catch{return e.json({error:`The Push subscription must be JSON.`},400)}return Y?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),L.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Q(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(Y?.remove(t),e.body(null,204))});let Me=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Ne=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Pe=t.createPiHubService({records:()=>k.records(),spawn:e=>k.create(e),onNotice:u}),Fe={...Pe,createSession:async()=>{throw new T.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Ie=new T.PiServer(Pe,{listeners:[Me],onError:e=>u(`protocol: ${e.message}`)}),Le=new T.PiServer(Fe,{listeners:[Ne],onError:e=>u(`protocol: ${e.message}`)});await Promise.all([Ie.start(),Le.start()]);let $=ne.createProviderAuth({runtime:e.authRuntime,onNotice:u});ee.registerAuthRoutes(L,$);let Re=W(k),ze=()=>Re.list(),Be=e=>Re.resolve(e),Ve=e=>{let t=Be(e);if(t===void 0)return;let n=(0,C.readSyncDrift)({repoRoot:t}),r;try{r=(0,C.readSyncRegistration)(t)?(0,he.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(L,{repositories:ze,models:()=>$.listModels()});let He=S.apiDirectory,Ue=we(L,He===void 0&&!process.env[A.PACKAGE_API_DIR_ENV]?[]:await(0,A.loadPackageApis)(`hub`,{apiDirectory:He,onNotice:u}),u,Be,Ve);Te(L,k,u,d),L.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:k.snapshot().length,pid:process.pid,version:R()})),L.post(s.SESSIONS_API_ROUTE,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!B(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await p.ensureSynced();let r=await k.create({cwd:t.cwd,name:n,trace:y.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),L.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`);await p.ensureSynced();let n=await k.restart(t,y.readTraceContext(e.req.raw.headers));return n.ok?e.json({sessionId:n.sessionId},202):e.json({error:n.error},n.code===`invalid_request`?404:502)}),L.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=k.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await w.SessionManager.list(n.cwd);return e.json({sessions:r.map(e=>({id:e.id,...e.name===void 0?{}:{name:e.name},firstMessage:e.firstMessage,createdAt:e.created.toISOString(),updatedAt:e.modified.toISOString(),messageCount:e.messageCount}))})}),L.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=k.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r;try{r=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!B(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await w.SessionManager.list(n.cwd)).find(e=>e.id===r.targetSessionId);if(!i)return e.json({error:`That Pi thread does not belong to this workspace.`},404);await p.ensureSynced();let a=await k.resume(t,{sessionId:i.id,name:Se(i,n.cwd)},y.readTraceContext(e.req.raw.headers));return a.ok?e.json({sessionId:a.sessionId},202):e.json({error:a.error},a.code===`invalid_request`?409:502)}),L.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=k.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),L.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=k.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await v.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let We=t=>{if(!(Z.listenerOf(t)===`local`||(0,j.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return L.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=We(e),n=await le.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),L.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=k.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await v.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),L.get(m.SESSION_SOCKET_ROUTE,U.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,D.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.channelFor(n,`session`),i=new Set,a=(0,fe.randomUUID)(),o=new Map,l,d,f,p,h=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),P.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.close(1008,`sealed session channel required`);return}let g=e=>{let n=JSON.stringify(e),i=n;if(!t){if(r===void 0)return;let e=r.seal(new TextEncoder().encode(n));if(!e.ok){m.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{m.send(i)}catch{}};f={post:g,local:t},F.add(f),n!==void 0&&(p=X.trackSocket(n,(e,t)=>m.close(e,t))),g(s.hubHello(k.channelTypes())),g({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:k.snapshot()}),l=k.onEvent(e=>{e.kind===`upsert`?g({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),h(e.sessionId),g({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&i.has(e.sessionId))&&g({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&g(s.sessionFrameEnvelope(e.sessionId,e.frame))}),d=P.onFrame(e=>{o.has(H(e.sessionId,e.threadId))&&g(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),u(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!B(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&k.channelTypes().includes(n.type)&&(i.has(c)||k.channelReceivesWithoutSubscription(n.type))){k.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=k.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of k.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=k.history(c,{...typeof n.before==`string`?{before:n.before}:{},...typeof n.limit==`number`?{limit:n.limit}:{}});if(!e)return;f?.post(e);return}if(n.type===`subscribe_thread`||n.type===`unsubscribe_thread`){if(typeof n.threadId!=`string`)return;let e=n.threadId,t=H(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&P.unsubscribe(c,e);return}if(o.has(t)||k.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,P.subscribe(c,e)));return}if(n.type===`session_command`&&B(n.frame)){if(n.frame.type===`attach`)return;k.command(c,n.frame)}},onClose(){k.disconnectChannels(a),f&&F.delete(f),f=void 0,p?.(),p=void 0,l?.(),l=void 0,d?.(),d=void 0,i.clear(),h(),u(`browser detached`)}}})),L.get(m.PROTOCOL_SOCKET_ROUTE,U.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,D.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.channelFor(n,`protocol`),i,a;return{onOpen(e,o){if(!t&&r===void 0){o.close(1008,`sealed protocol channel required`);return}n!==void 0&&(a=X.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?Me:Ne).accept({send:e=>{if(t){o.send(e);return}if(r===void 0)return;let n=e instanceof Uint8Array?e:new Uint8Array(e),i=r.seal(n);if(!i.ok){o.close(1008,`sealed protocol channel failed`);return}o.send(new TextEncoder().encode(JSON.stringify(i.envelope)))},close:()=>o.close(),get readyState(){return o.readyState}})},async onMessage(e){let n=e.data;if(typeof n==`string`)return;let a=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):(n instanceof ArrayBuffer,new Uint8Array(n));if(t){i?.onData(a);return}if(r===void 0)return;let o;try{o=JSON.parse(new TextDecoder().decode(a))}catch{return}let s=r.open(o);s.ok&&i?.onData(s.plaintext)},onClose(){a?.(),a=void 0,i?.onClose(),i=void 0},onError(e){i?.onError(e instanceof Error?e:Error(`The protocol socket failed`)),i=void 0}}})),L.get(`/manifest.webmanifest`,e=>{let t=z(x.default.join(q,`manifest.webmanifest`));return t===void 0?e.text(`The PWA manifest is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-store`,"Content-Type":`application/manifest+json; charset=utf-8`})}),L.get(`/sw.js`,e=>{let t=z(x.default.join(q,`sw.js`));return t===void 0?e.text(`The trusted service worker is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-cache`,"Content-Type":`text/javascript; charset=utf-8`,"Service-Worker-Allowed":`/`})}),L.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(q,`/${t}`),r=n===void 0?void 0:z(n);return n===void 0||r===void 0?e.text(`PWA asset not found.`,404):e.body(new Uint8Array(r),200,{"Cache-Control":`no-store`,"Content-Type":o.contentTypeFor(n),"X-Content-Type-Options":`nosniff`})}),L.get(E.BUNDLE_MANIFEST_ROUTE,e=>{let t=J.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),L.get(`/bundle-assets/*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/bundle-assets\/([1-9][0-9]*)(\/.*)$/u.exec(t),r=n===null?void 0:Number(n[1]),i=J.current();if(n===null||!Number.isSafeInteger(r)||i===void 0||i.signed.manifest.revision!==r)return e.text(`That signed bundle revision is unavailable.`,404);let a=(0,E.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:z(s);return a===void 0||s===void 0||c===void 0?e.text(`Bundle asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),L.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),L.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=e.req.header(`accept`)?.includes(`text/html`)===!0;return!t.startsWith(`/api/`)&&n?e.redirect(`/`):e.text(`Not found.`,404)}),p.watch(()=>{let e;if(f!==void 0)try{e=(0,C.readSyncRegistration)(f)}catch(e){u(`the rebuilt registration is unavailable (${V(e)}); keeping the served bundle`)}e?.webDirectory!=null&&e.webDirectory!==S.assetsDir&&(S={assetsDir:e.webDirectory,apiDirectory:e.apiDirectory},u(`serving the rebuilt cockpit bundle from ${S.assetsDir}`)),J.refresh(),I({type:s.HUB_RESYNCED_TYPE},!1)}),new Promise((t,n)=>{let r=(0,pe.serve)({fetch:L.fetch,port:e.port,hostname:l},e=>{G=e.port,U.injectWebSocket(r);let n=`http://${l}:${e.port}`;u(`cockpit on ${n}`);let i,a=async()=>{await X.close(),await new Promise(e=>{P.close(),p.close(),Ie.close(),Le.close(),De(),Y?.close(),k.close(),$.close();for(let e of Ue)e.close();for(let e of U.wss.clients)e.terminate();U.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await y.shutdownWebTelemetry(d)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{P.close(),De(),Y?.close(),k.close(),$.close(),X.close();for(let e of Ue)e.close();d.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=W,exports.packagedVersion=R,exports.serveWeb=G;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./piHubService.cjs"),n=require("./syncGuard.cjs"),r=require("./piWebSocketListener.cjs"),i=require("../types/media.cjs"),a=require("../services/fileMedia.cjs"),o=require("../services/staticAssets.cjs"),s=require("../types/hub.cjs"),c=require("../types/notification.cjs");require("../types/session.cjs");const l=require("./gitStatus.cjs"),u=require("./registryWatcher.cjs"),d=require("./serverSpawner.cjs"),f=require("./packageApiProxy.cjs"),p=require("./sessionHub.cjs"),m=require("../types/remoteAccess.cjs"),h=require("../services/remoteGuardPolicy.cjs"),g=require("../services/sessionMigration.cjs"),ee=require("./authRoutes.cjs"),te=require("./settingsRoutes.cjs"),ne=require("./providerAuth.cjs"),re=require("./remoteGuard.cjs"),ie=require("./remoteAccess.cjs"),ae=require("./bundlePublication.cjs"),oe=require("./webPush.cjs"),se=require("./remoteAccessStore.cjs"),ce=require("./remoteRoutes.cjs"),_=require("./tunnelProcess.cjs"),le=require("./directoryListing.cjs"),v=require("./sessionFiles.cjs"),ue=require("./threadJournals.cjs"),de=require("./webHubPluginLoader.cjs"),y=require("./webTelemetry.cjs"),fe=require("./webComposition.cjs");let b=require("node:fs");b=e.__toESM(b,1);let x=require("node:path");x=e.__toESM(x,1);let S=require("node:url"),C=require("@agimon-ai/doompi/services"),pe=require("node:crypto"),me=require("@hono/node-server"),he=require("@hono/node-ws"),ge=require("@earendil-works/pi-coding-agent"),w=require("@earendil-works/pi-server"),_e=require("@agimon-ai/doompi-config"),ve=require("@agimon-ai/doompi/services/syncState"),ye=require("@agimon-ai/doompi-web-security"),be=require("hono"),T=require("hono/cookie"),E=require("hono/body-limit"),D=require("@agimon-ai/doompi-extension-contracts/package-api"),xe=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Se=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),O=require("@agimon-ai/doompi/utils/repository");const k=8388608,A=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),j=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function Ce(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!A.has(t.method)||typeof t.target!=`string`||!t.target.startsWith(`/`)||t.target.startsWith(`//`)||t.target.includes(`#`)||!Array.isArray(t.headers)||!t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))||t.body!==void 0&&typeof t.body!=`string`)return;let n=new URL(t.target,`http://sealed.doompi.invalid`);if(n.pathname!==m.REMOTE_HTTP_ROUTE&&n.pathname!==m.REMOTE_CHANNEL_ROUTE)return t}function we(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();j.has(e)||e.startsWith(`sec-`)||n.append(t,r)}for(let e of[`cookie`,`host`,`origin`]){let r=t.req.header(e);r!==void 0&&n.set(e,r)}return n}catch{return}}function Te(e){if(e===void 0)return Buffer.alloc(0);if(!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(e))return;let t=Buffer.from(e,`base64`);return t.toString(`base64`)===e?t:void 0}function M(e){let t=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(b.default.existsSync(x.default.join(t,`package.json`)))return x.default.join(t,`dist`,e);let n=x.default.dirname(t);if(n===t)throw Error(`doompi-web could not locate its own package root.`);t=n}}function N(){return M(`web`)}function Ee(){return M(`pwa`)}function De(){let e=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let t=x.default.join(e,`package.json`);if(b.default.existsSync(t))try{let e=JSON.parse(b.default.readFileSync(t,`utf8`)),n=F(e)?e.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=x.default.dirname(e);if(n===e)return`unknown`;e=n}}function Oe(e,t,n){if(e!==void 0)return e;let r=process.env.DOOMPI_WEB_DIST;if(r!==void 0&&r!==``)return r;let i=t?.webDirectory;return i!=null&&b.default.existsSync(x.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):N()}function P(e){try{return b.default.statSync(e).isFile()?b.default.readFileSync(e):void 0}catch{return}}function F(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ke(e,t){let n=e.firstMessage.split(`
2
+ `,1)[0]?.trim();return(e.name?.trim()||n||x.default.basename(t)||`untitled`).slice(0,80)}function I(e){return e instanceof Error?e.message:String(e)}function Ae(e,t){return`${e}\n${t}`}function je(e,t,n,r){return p.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function L(e){return`repo-${(0,pe.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function Me(e){let t=new Map,n=()=>{for(let[e,n]of t)t.set(e,{...n,repository:{...n.repository,active:!1}});for(let n of e.records())try{let e=b.default.realpathSync((0,O.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:L(e),path:e,name:x.default.basename(e)||e,active:!0},seenAt:Date.now()})}catch{}let n=[...t.entries()].filter(([,e])=>!e.repository.active).sort((e,t)=>t[1].seenAt-e[1].seenAt);for(let[e]of n.slice(12))t.delete(e);return[...t.values()].sort((e,t)=>Number(t.repository.active)-Number(e.repository.active)||t.seenAt-e.seenAt||e.repository.path.localeCompare(t.repository.path)).map(e=>e.repository)};return{list:n,resolve(e){return n().find(t=>t.id===e)?.path}}}function Ne(e,t,n,r,i){let a=[];for(let o of t){let t=`${D.DOOM_API_ROUTE_PREFIX}/${o.basePath}`,s;try{s=o.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${o.basePath}' did not start (${I(e)}); its routes stay unmounted`);continue}a.push(s),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await s.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${o.basePath}' failed on ${r.pathname} (${I(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function Pe(e,t,n,r){e.all(`${D.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=y.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:y.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${I(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function R(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=y.createWebTelemetry(),p=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),S=e=>{try{return b.default.realpathSync((0,O.findRepositoryRoot)(e))}catch{return}},D=e=>{let t=S(e);if(t!==void 0)return t;let n=(0,_e.globalDoomConfigDirectory)();try{return(0,C.readSyncRegistration)(n)?.root}catch(e){d(`global cockpit composition is unreadable (${I(e)})`);return}},A=e.compositionDir===void 0?void 0:S(e.compositionDir);if(e.compositionDir!==void 0&&A===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let j=u.readRegistryRecords(e.registryDir,d).map(e=>D(e.cwd)).filter(e=>e!==void 0),M=[...new Set(A===void 0?j:[A])].sort((e,t)=>e.localeCompare(t)),N=new Map;for(let e of M)N.set(e,n.createSyncGuard({repoRoot:e,onNotice:d}));await Promise.all([...N.values()].map(async e=>await e.ensureSynced()));let L=async()=>await fe.resolveWebComposition({repositoryRoots:M,stateDirectory:p.directory,onNotice:d}),R=await L(),z={assetsDir:Oe(e.assetsDir,R,d),apiDirectory:R?.apiDirectory},B=async e=>{let t=D(e);if(t===void 0)return;let r=N.get(t);if(r!==void 0){await r.ensureSynced();return}let i=n.createSyncGuard({repoRoot:t,onNotice:d});try{await i.ensureSynced()}finally{i.close()}},V=je(e,d,await de.loadHubChannels(z.assetsDir,d),f),H=ue.createThreadJournals({resolve:(e,t)=>V.threadJournal(e,t),onNotice:d}),U=new Set,W=(e,t)=>{for(let n of U)t&&!n.local||n.post(e)},G=new be.Hono,K=(0,he.createNodeWebSocket)({app:G}),Fe,q=Ee(),J=ae.createBundlePublication({assetsDir:()=>z.assetsDir,stateDir:p.directory,onNotice:d}),Y;_.reapStaleTunnel(p.directory,d);let Ie=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(V.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of g.describeStranded(n.stranded))d(e);for(let e of n.migrate){let t=V.stop(e.id);t.ok||d(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},X=ie.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??_.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),X.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,me.serve)({fetch:G.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{K.injectWebSocket(n),e({port:t.port,close:async()=>await new Promise(e=>{n.closeAllConnections?.(),n.close(()=>e())})})}),r=n;r.maxConnections=64,r.maxRequestsPerSocket=100,n.once(`error`,t)}),bundleTrust:()=>J.trust(),onDeviceDropped:e=>Y?.remove(e),onNotice:d,...Ie===void 0?{}:{requestHandover:Ie},contained:(0,Se.insideSandbox)(process.env),broadcastLocal:e=>W(e,!0),broadcastAll:e=>W(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});Y=oe.createLiveWebPush({stateDir:p.directory,isConnected:e=>X.isDeviceConnected(e),onNotice:d});let Le=V.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&Y?.notify()}),Z=re.createRemoteGuard({loopbackPort:()=>Fe,tunnelPolicy:()=>X.tunnelPolicy(),authorize:e=>X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>X.channelFor(e,t)!==void 0,stepUp:{required:e=>X.stepUpRequired(e),verify:async(e,t,n)=>{if(typeof n!=`object`||!n)return!1;let r=n.ceremonyId,i=n.response;if(typeof r!=`string`)return!1;let a=e.env?.sealedDeviceId??X.authorize((0,T.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await X.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});G.use(`*`,Z.middleware),G.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=y.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await f.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await f.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},y.readTraceContext(e.req.raw.headers))}),G.use(`*`,async(e,t)=>{if(Z.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>k?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(G,{remote:X,listenerOf:e=>Z.listenerOf(e)});let Re=(0,E.bodyLimit)({maxSize:k,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});G.use(`*`,async(e,t)=>Z.listenerOf(e)===`local`?t():await Re(e,t));let ze=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Be=y.createBrowserTelemetryRateLimit();G.post(y.WEB_TELEMETRY_ROUTE,ze,async e=>{if(!Be())return e.json({error:`Too many telemetry batches.`},429);let t;try{t=await e.req.json()}catch{return e.json({error:`The telemetry batch must be JSON.`},400)}let n=y.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await f.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),G.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=X.channelFor(t,`http`);if(n===void 0)return e.json({error:`This device has no sealed HTTP channel.`},401);let r;try{r=await e.req.json()}catch{return e.json({error:`The sealed request envelope was malformed.`},400)}let i=n.open(r);if(!i.ok)return e.json({error:`The sealed request was refused: ${i.failure}.`},400);let a;try{a=JSON.parse(new TextDecoder().decode(i.plaintext))}catch{return e.json({error:`The sealed HTTP request was malformed.`},400)}let o=Ce(a),s=o===void 0?void 0:we(o,e),c=o===void 0?void 0:Te(o.body);if(o===void 0||s===void 0||c===void 0)return e.json({error:`The sealed HTTP request was invalid.`},400);let l=new URL(o.target,new URL(e.req.url).origin),u=await G.request(l,{method:o.method,headers:s,...o.method===`GET`||o.method===`HEAD`?{}:{body:Uint8Array.from(c).buffer}},{...e.env,sealedDeviceId:t}),d=Buffer.from(await u.arrayBuffer()),f=n.seal(new TextEncoder().encode(JSON.stringify({v:1,status:u.status,headers:Array.from(u.headers.entries()),body:d.toString(`base64`)})));return f.ok?e.json(f.envelope):e.json({error:`The sealed response failed: ${f.failure}.`},503)});let Ve=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Q=e=>e.env?.sealedDeviceId;G.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Q(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:Y?.publicKey()},200,{"Cache-Control":`no-store`})),G.post(m.REMOTE_PUSH_ROUTE,Ve,async e=>{let t=Q(e);if(t===void 0)return e.json({error:`A sealed paired device is required.`},401);let n;try{n=await e.req.json()}catch{return e.json({error:`The Push subscription must be JSON.`},400)}return Y?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),G.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Q(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(Y?.remove(t),e.body(null,204))});let He=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Ue=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),We=t.createPiHubService({records:()=>V.records(),spawn:e=>V.create(e),onNotice:d}),Ge={...We,createSession:async()=>{throw new w.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Ke=new w.PiServer(We,{listeners:[He],onError:e=>d(`protocol: ${e.message}`)}),qe=new w.PiServer(Ge,{listeners:[Ue],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([Ke.start(),qe.start()]);let $=ne.createProviderAuth({runtime:e.authRuntime,onNotice:d});ee.registerAuthRoutes(G,$);let Je=Me(V),Ye=()=>Je.list(),Xe=e=>Je.resolve(e),Ze=e=>{let t=Xe(e);if(t===void 0)return;let n=(0,C.readSyncDrift)({repoRoot:t}),r;try{r=(0,C.readSyncRegistration)(t)?(0,ve.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(G,{repositories:Ye,models:()=>$.listModels()});let Qe=z.apiDirectory,$e=Ne(G,Qe===void 0&&!process.env[xe.PACKAGE_API_DIR_ENV]?[]:await(0,xe.loadPackageApis)(`hub`,{apiDirectory:Qe,onNotice:d}),d,Xe,Ze);Pe(G,V,d,f),G.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:V.snapshot().length,pid:process.pid,version:De()})),G.post(s.SESSIONS_API_ROUTE,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!F(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await B(t.cwd);let r=await V.create({cwd:t.cwd,name:n,trace:y.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),G.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);n!==void 0&&await B(n.cwd);let r=await V.restart(t,y.readTraceContext(e.req.raw.headers));return r.ok?e.json({sessionId:r.sessionId},202):e.json({error:r.error},r.code===`invalid_request`?404:502)}),G.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await ge.SessionManager.list(n.cwd);return e.json({sessions:r.map(e=>({id:e.id,...e.name===void 0?{}:{name:e.name},firstMessage:e.firstMessage,createdAt:e.created.toISOString(),updatedAt:e.modified.toISOString(),messageCount:e.messageCount}))})}),G.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r;try{r=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!F(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await ge.SessionManager.list(n.cwd)).find(e=>e.id===r.targetSessionId);if(!i)return e.json({error:`That Pi thread does not belong to this workspace.`},404);await B(n.cwd);let a=await V.resume(t,{sessionId:i.id,name:ke(i,n.cwd)},y.readTraceContext(e.req.raw.headers));return a.ok?e.json({sessionId:a.sessionId},202):e.json({error:a.error},a.code===`invalid_request`?409:502)}),G.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=V.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),G.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await v.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let et=t=>{if(!(Z.listenerOf(t)===`local`||(0,Se.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};G.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=et(e),n=await le.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),G.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await v.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),G.get(m.SESSION_SOCKET_ROUTE,K.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.channelFor(n,`session`),i=new Set,a=(0,pe.randomUUID)(),o=new Map,l,u,f,p,h=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),H.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.close(1008,`sealed session channel required`);return}let g=e=>{let n=JSON.stringify(e),i=n;if(!t){if(r===void 0)return;let e=r.seal(new TextEncoder().encode(n));if(!e.ok){m.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{m.send(i)}catch{}};f={post:g,local:t},U.add(f),n!==void 0&&(p=X.trackSocket(n,(e,t)=>m.close(e,t))),g(s.hubHello(V.channelTypes())),g({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:V.snapshot()}),l=V.onEvent(e=>{e.kind===`upsert`?g({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),h(e.sessionId),g({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&i.has(e.sessionId))&&g({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&g(s.sessionFrameEnvelope(e.sessionId,e.frame))}),u=H.onFrame(e=>{o.has(Ae(e.sessionId,e.threadId))&&g(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),d(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!F(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&V.channelTypes().includes(n.type)&&(i.has(c)||V.channelReceivesWithoutSubscription(n.type))){V.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=V.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of V.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=V.history(c,{...typeof n.before==`string`?{before:n.before}:{},...typeof n.limit==`number`?{limit:n.limit}:{}});if(!e)return;f?.post(e);return}if(n.type===`subscribe_thread`||n.type===`unsubscribe_thread`){if(typeof n.threadId!=`string`)return;let e=n.threadId,t=Ae(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&H.unsubscribe(c,e);return}if(o.has(t)||V.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,H.subscribe(c,e)));return}if(n.type===`session_command`&&F(n.frame)){if(n.frame.type===`attach`)return;V.command(c,n.frame)}},onClose(){V.disconnectChannels(a),f&&U.delete(f),f=void 0,p?.(),p=void 0,l?.(),l=void 0,u?.(),u=void 0,i.clear(),h(),d(`browser detached`)}}})),G.get(m.PROTOCOL_SOCKET_ROUTE,K.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.channelFor(n,`protocol`),i,a;return{onOpen(e,o){if(!t&&r===void 0){o.close(1008,`sealed protocol channel required`);return}n!==void 0&&(a=X.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?He:Ue).accept({send:e=>{if(t){o.send(e);return}if(r===void 0)return;let n=e instanceof Uint8Array?e:new Uint8Array(e),i=r.seal(n);if(!i.ok){o.close(1008,`sealed protocol channel failed`);return}o.send(new TextEncoder().encode(JSON.stringify(i.envelope)))},close:()=>o.close(),get readyState(){return o.readyState}})},async onMessage(e){let n=e.data;if(typeof n==`string`)return;let a=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):(n instanceof ArrayBuffer,new Uint8Array(n));if(t){i?.onData(a);return}if(r===void 0)return;let o;try{o=JSON.parse(new TextDecoder().decode(a))}catch{return}let s=r.open(o);s.ok&&i?.onData(s.plaintext)},onClose(){a?.(),a=void 0,i?.onClose(),i=void 0},onError(e){i?.onError(e instanceof Error?e:Error(`The protocol socket failed`)),i=void 0}}})),G.get(`/manifest.webmanifest`,e=>{let t=P(x.default.join(q,`manifest.webmanifest`));return t===void 0?e.text(`The PWA manifest is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-store`,"Content-Type":`application/manifest+json; charset=utf-8`})}),G.get(`/sw.js`,e=>{let t=P(x.default.join(q,`sw.js`));return t===void 0?e.text(`The trusted service worker is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-cache`,"Content-Type":`text/javascript; charset=utf-8`,"Service-Worker-Allowed":`/`})}),G.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(q,`/${t}`),r=n===void 0?void 0:P(n);return n===void 0||r===void 0?e.text(`PWA asset not found.`,404):e.body(new Uint8Array(r),200,{"Cache-Control":`no-store`,"Content-Type":o.contentTypeFor(n),"X-Content-Type-Options":`nosniff`})}),G.get(ye.BUNDLE_MANIFEST_ROUTE,e=>{let t=J.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),G.get(`/bundle-assets/*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/bundle-assets\/([1-9][0-9]*)(\/.*)$/u.exec(t),r=n===null?void 0:Number(n[1]),i=J.current();if(n===null||!Number.isSafeInteger(r)||i===void 0||i.signed.manifest.revision!==r)return e.text(`That signed bundle revision is unavailable.`,404);let a=(0,ye.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:P(s);return a===void 0||s===void 0||c===void 0?e.text(`Bundle asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),G.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),G.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=e.req.header(`accept`)?.includes(`text/html`)===!0;return!t.startsWith(`/api/`)&&n?e.redirect(`/`):e.text(`Not found.`,404)});let tt=async()=>{try{let e=await L();e!==void 0&&e.webDirectory!==z.assetsDir&&(z={assetsDir:e.webDirectory,apiDirectory:e.apiDirectory},d(`serving the rebuilt cockpit bundle from ${z.assetsDir}`)),J.refresh(),W({type:s.HUB_RESYNCED_TYPE},!1)}catch(e){d(`the rebuilt cockpit composition is unavailable (${I(e)}); keeping the served bundle`)}};for(let e of N.values())e.watch(()=>void tt());return new Promise((t,n)=>{let r=(0,me.serve)({fetch:G.fetch,port:e.port,hostname:l},e=>{Fe=e.port,K.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await X.close(),await new Promise(e=>{H.close();for(let e of N.values())e.close();Ke.close(),qe.close(),Le(),Y?.close(),V.close(),$.close();for(let e of $e)e.close();for(let e of K.wss.clients)e.terminate();K.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await y.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{H.close(),Le(),Y?.close(),V.close(),$.close(),X.close();for(let e of $e)e.close();f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=Me,exports.packagedVersion=De,exports.serveWeb=R;
3
3
  //# sourceMappingURL=httpServer.cjs.map