@agimon-ai/doompi-web 0.0.1-alpha.16 → 0.0.1-alpha.18

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 (124) hide show
  1. package/dist/bin/serve.cjs +1 -1
  2. package/dist/bin/serve.cjs.map +1 -1
  3. package/dist/bin/serve.mjs +1 -1
  4. package/dist/bin/serve.mjs.map +1 -1
  5. package/dist/src/adapters/computerUseIpc.cjs +2 -0
  6. package/dist/src/adapters/computerUseIpc.cjs.map +1 -0
  7. package/dist/src/adapters/computerUseIpc.mjs +2 -0
  8. package/dist/src/adapters/computerUseIpc.mjs.map +1 -0
  9. package/dist/src/adapters/httpServer.cjs +2 -2
  10. package/dist/src/adapters/httpServer.cjs.map +1 -1
  11. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  12. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  13. package/dist/src/adapters/httpServer.mjs +2 -2
  14. package/dist/src/adapters/httpServer.mjs.map +1 -1
  15. package/dist/src/adapters/packageApiProxy.cjs +1 -1
  16. package/dist/src/adapters/packageApiProxy.cjs.map +1 -1
  17. package/dist/src/adapters/packageApiProxy.mjs +1 -1
  18. package/dist/src/adapters/packageApiProxy.mjs.map +1 -1
  19. package/dist/src/adapters/piHubService.cjs +1 -1
  20. package/dist/src/adapters/piHubService.cjs.map +1 -1
  21. package/dist/src/adapters/piHubService.mjs +1 -1
  22. package/dist/src/adapters/piHubService.mjs.map +1 -1
  23. package/dist/src/adapters/piWebSocketListener.cjs +1 -1
  24. package/dist/src/adapters/piWebSocketListener.cjs.map +1 -1
  25. package/dist/src/adapters/piWebSocketListener.mjs +1 -1
  26. package/dist/src/adapters/piWebSocketListener.mjs.map +1 -1
  27. package/dist/src/adapters/remoteGuard.cjs +1 -1
  28. package/dist/src/adapters/remoteGuard.cjs.map +1 -1
  29. package/dist/src/adapters/remoteGuard.mjs +1 -1
  30. package/dist/src/adapters/remoteGuard.mjs.map +1 -1
  31. package/dist/src/adapters/sessionFiles.cjs +2 -2
  32. package/dist/src/adapters/sessionFiles.cjs.map +1 -1
  33. package/dist/src/adapters/sessionFiles.mjs +2 -2
  34. package/dist/src/adapters/sessionFiles.mjs.map +1 -1
  35. package/dist/src/adapters/sessionHub.cjs +1 -1
  36. package/dist/src/adapters/sessionHub.cjs.map +1 -1
  37. package/dist/src/adapters/sessionHub.d.cts +1 -1
  38. package/dist/src/adapters/sessionHub.d.mts +1 -1
  39. package/dist/src/adapters/sessionHub.mjs +1 -1
  40. package/dist/src/adapters/sessionHub.mjs.map +1 -1
  41. package/dist/src/adapters/syncGuard.cjs +1 -1
  42. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  43. package/dist/src/adapters/syncGuard.mjs +1 -1
  44. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  45. package/dist/src/services/registryStore.cjs +1 -1
  46. package/dist/src/services/registryStore.cjs.map +1 -1
  47. package/dist/src/services/registryStore.mjs +1 -1
  48. package/dist/src/services/registryStore.mjs.map +1 -1
  49. package/dist/src/services/webauthnPolicy.cjs +1 -1
  50. package/dist/src/services/webauthnPolicy.cjs.map +1 -1
  51. package/dist/src/services/webauthnPolicy.mjs +1 -1
  52. package/dist/src/services/webauthnPolicy.mjs.map +1 -1
  53. package/dist/src/types/bridge.d.cts +3 -0
  54. package/dist/src/types/bridge.d.cts.map +1 -1
  55. package/dist/src/types/bridge.d.mts +3 -0
  56. package/dist/src/types/bridge.d.mts.map +1 -1
  57. package/dist/src/types/media.cjs +1 -1
  58. package/dist/src/types/media.cjs.map +1 -1
  59. package/dist/src/types/media.mjs +1 -1
  60. package/dist/src/types/media.mjs.map +1 -1
  61. package/dist/web/assets/CodeEditorView-Vcq41fvN.js +3 -0
  62. package/dist/web/assets/CodeEditorView-Vcq41fvN.js.map +1 -0
  63. package/dist/web/assets/{dist-DOndDXyJ.js → dist-0Xx-ctg3.js} +2 -2
  64. package/dist/web/assets/{dist-DOndDXyJ.js.map → dist-0Xx-ctg3.js.map} +1 -1
  65. package/dist/web/assets/{dist-K0Kxol7m.js → dist-9y0keW18.js} +2 -2
  66. package/dist/web/assets/{dist-K0Kxol7m.js.map → dist-9y0keW18.js.map} +1 -1
  67. package/dist/web/assets/{dist-DGGMgE1d.js → dist-CwttV9P8.js} +2 -2
  68. package/dist/web/assets/{dist-DGGMgE1d.js.map → dist-CwttV9P8.js.map} +1 -1
  69. package/dist/web/assets/{dist-gdc1AOyh.js → dist-DDX_XVB8.js} +2 -2
  70. package/dist/web/assets/{dist-gdc1AOyh.js.map → dist-DDX_XVB8.js.map} +1 -1
  71. package/dist/web/assets/{dist-BjOHzEAd.js → dist-DHeBCSod.js} +2 -2
  72. package/dist/web/assets/{dist-BjOHzEAd.js.map → dist-DHeBCSod.js.map} +1 -1
  73. package/dist/web/assets/{dist-scMsNAmD.js → dist-Gi0bJbX1.js} +2 -2
  74. package/dist/web/assets/{dist-scMsNAmD.js.map → dist-Gi0bJbX1.js.map} +1 -1
  75. package/dist/web/assets/{dist-CkiChlyh.js → dist-PTC7K_TU.js} +2 -2
  76. package/dist/web/assets/{dist-CkiChlyh.js.map → dist-PTC7K_TU.js.map} +1 -1
  77. package/dist/web/assets/{dist-CeI3DoxX.js → dist-VjxpG4yr.js} +2 -2
  78. package/dist/web/assets/{dist-CeI3DoxX.js.map → dist-VjxpG4yr.js.map} +1 -1
  79. package/dist/web/assets/{dist-DmNx_rOj.js → dist-fXu9lXcM.js} +2 -2
  80. package/dist/web/assets/{dist-DmNx_rOj.js.map → dist-fXu9lXcM.js.map} +1 -1
  81. package/dist/web/assets/index-CN4JqlUs.js +198 -0
  82. package/dist/web/assets/index-CN4JqlUs.js.map +1 -0
  83. package/dist/web/assets/index-UaAZtWjx.css +2 -0
  84. package/dist/web/assets/pdf.worker-K4tV72Oz.js +49 -0
  85. package/dist/web/assets/pdf.worker-K4tV72Oz.js.map +1 -0
  86. package/dist/web/assets/webmcpSimulator-VMDmMkau.js +2 -0
  87. package/dist/web/assets/webmcpSimulator-VMDmMkau.js.map +1 -0
  88. package/dist/web/index.html +2 -2
  89. package/package.json +15 -14
  90. package/src/adapters/computerUseIpc.ts +140 -0
  91. package/src/adapters/httpServer.ts +94 -14
  92. package/src/adapters/packageApiProxy.ts +23 -3
  93. package/src/adapters/piHubService.ts +138 -96
  94. package/src/adapters/piWebSocketListener.ts +7 -27
  95. package/src/adapters/remoteGuard.ts +20 -7
  96. package/src/adapters/sessionFiles.ts +107 -2
  97. package/src/adapters/sessionHub.ts +57 -3
  98. package/src/adapters/syncGuard.ts +16 -3
  99. package/src/bin/serve.ts +12 -0
  100. package/src/services/registryStore.ts +3 -0
  101. package/src/services/webauthnPolicy.ts +2 -0
  102. package/src/types/bridge.ts +3 -0
  103. package/src/types/media.ts +2 -0
  104. package/src/web/app/Providers.tsx +10 -1
  105. package/src/web/app/protocolRuntime.ts +93 -53
  106. package/src/web/features/activity/ActivityDock.tsx +25 -5
  107. package/src/web/features/activity/DockTabs.tsx +16 -18
  108. package/src/web/features/session/Composer.tsx +37 -4
  109. package/src/web/lib/composerSubmissions.ts +24 -0
  110. package/src/web/lib/composition.ts +21 -1
  111. package/src/web/lib/modelContextAdapter.ts +72 -0
  112. package/src/web/lib/pluginRegistry.ts +24 -0
  113. package/src/web/lib/pluginSlotProps.ts +3 -0
  114. package/src/web/lib/protocolTimeline.ts +16 -1
  115. package/src/web/lib/webmcpSimulator.ts +136 -0
  116. package/src/web/routes/CockpitPage.tsx +8 -5
  117. package/src/web/stores/composerStore.ts +190 -6
  118. package/src/web/stores/uiStore.ts +4 -8
  119. package/src/web/stores/usePluginSlotProps.ts +10 -2
  120. package/dist/web/assets/CodeEditorView-x5dSJyH5.js +0 -3
  121. package/dist/web/assets/CodeEditorView-x5dSJyH5.js.map +0 -1
  122. package/dist/web/assets/index-6BGXtAmW.js +0 -143
  123. package/dist/web/assets/index-6BGXtAmW.js.map +0 -1
  124. package/dist/web/assets/index-DxH-he8Q.css +0 -2
@@ -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/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});
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/computerUseIpc.cjs"),s=require("../src/adapters/cockpitHandoff.cjs"),c=require("../src/adapters/doomInitialization.cjs"),l=require("../src/adapters/hubProbe.cjs"),u=require("../src/adapters/sessionRelaunch.cjs");let d=require("node:os");d=e.__toESM(d,1);function f(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function p(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.hubAnswers(t,n))return!0;return!1}async function m(){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 c.ensureDoomInitialized({homeDirectory:d.default.homedir(),onNotice:f});let s=`http://${e.host}:${String(e.port)}`,u=e.stateDir??n.defaultRemoteStateDir(),m=a.createCockpitContainer({stateDir:u,onNotice:f}),g=o.createComputerUseIpcBinding(process,f);await m.reapStale(),i.isLoopbackHost(e.host)||(f(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),f(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let _=await l.probeHub(e.host,e.port);if(_!==void 0){let t=r.packagedVersion(),n=_.version??`an older build`;if(_.version===t){f(`cockpit already running at ${s}`);return}if(_.sessions>0){f(`WARNING: ${n} is already running at ${s} with ${String(_.sessions)} live session(s).`),f(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(_.pid===void 0||!i.isLoopbackHost(e.host)){f(`WARNING: ${n} is already running at ${s} and cannot be replaced automatically.`),f(`WARNING: stop it and run again to serve ${t}.`);return}if(f(`replacing ${n} at ${s} with ${t}`),!await p(_.pid,e.host,e.port)){f(`WARNING: the hub at ${s} did not stop; serve ${t} on another port with --port.`);return}}let v=async()=>{},y=async()=>{g??=o.createComputerUseIpcBinding(process,f);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:d.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:u,cloudflaredPath:e.cloudflaredPath,...g===void 0?{}:{computerUse:g},onNotice:f,onHandover:t=>{h({detachComputerUse:()=>{g?.close?.(),g=void 0},serving:n,restart:y,container:m,handover:t,port:e.port,host:e.host,adopt:e=>{v=e}})}});return v=async()=>{g?.close?.(),g=void 0,await n.close()},n};try{await y()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await l.hubAnswers(e.host,e.port)){f(`cockpit already running at ${s}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let b=()=>{v().then(()=>{process.exit(0)})};process.on(`SIGINT`,b),process.on(`SIGTERM`,b)}async function h(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:f});if(!t.ok){f(`the cockpit could not move into a container: ${t.error}`),f(`serving from the host again`),await e.restart(),await u.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:f});return}e.detachComputerUse(),e.adopt(async()=>{await t.container.stop()}),await s.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:f}),await u.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:f}),f(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}m().catch(e=>{f(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","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
+ {"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","createComputerUseIpcBinding","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 { createComputerUseIpcBinding } from '../adapters/computerUseIpc.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 let computerUse = createComputerUseIpcBinding(process, 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 computerUse ??= createComputerUseIpcBinding(process, notice);\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 ...(computerUse === undefined ? {} : { computerUse }),\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 detachComputerUse: () => {\n computerUse?.close?.();\n computerUse = undefined;\n },\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 computerUse?.close?.();\n computerUse = undefined;\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 detachComputerUse: () => void;\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.detachComputerUse();\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":";4jBAoBA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAQA,eAAe,EAAe,EAAa,EAAc,EAAgC,CACvF,GAAI,CACF,QAAQ,KAAK,EAAK,SAAS,CAC7B,MAAQ,CAER,CACA,IAAM,EAAW,KAAK,IAAI,EAAI,IAC9B,KAAO,KAAK,IAAI,EAAI,GAElB,GADA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,GAAY,CAAC,EAC5D,CAAE,MAAMA,EAAAA,WAAW,EAAM,CAAI,EAAI,MAAO,GAE9C,MAAO,EACT,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAUC,EAAAA,kBAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACvD,GAAI,EAAQ,KAAM,CAChB,QAAQ,OAAO,MAAMC,EAAAA,UAAU,CAAC,EAChC,MACF,CACA,GAAI,EAAQ,QAAS,CACnB,QAAQ,OAAO,MAAM,GAAGC,EAAAA,gBAAgB,EAAE,GAAG,EAC7C,MACF,CACA,MAAMC,EAAAA,sBAAsB,CAAE,cAAeC,EAAAA,QAAG,QAAQ,EAAG,SAAU,CAAO,CAAC,EAC7E,IAAM,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IACnD,EAAW,EAAQ,UAAYC,EAAAA,sBAAsB,EACrD,EAAYC,EAAAA,uBAAuB,CAAE,WAAU,SAAU,CAAO,CAAC,EACnE,EAAcC,EAAAA,4BAA4B,QAAS,CAAM,EAI7D,MAAM,EAAU,UAAU,EAKrBC,EAAAA,eAAe,EAAQ,IAAI,IAC9B,EAAO,mBAAmB,EAAQ,KAAK,+DAA+D,EACtG,EAAO,qFAAqF,GAQ9F,IAAM,EAAU,MAAMC,EAAAA,SAAS,EAAQ,KAAM,EAAQ,IAAI,EACzD,GAAI,IAAY,IAAA,GAAW,CACzB,IAAM,EAAOP,EAAAA,gBAAgB,EACvB,EAAS,EAAQ,SAAW,iBAClC,GAAI,EAAQ,UAAY,EAAM,CAC5B,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,GAAI,EAAQ,SAAW,EAAG,CACxB,EAAO,YAAY,EAAO,yBAAyB,EAAI,QAAQ,OAAO,EAAQ,QAAQ,EAAE,kBAAkB,EAC1G,EAAO,2CAA2C,EAAK,sCAAsC,EAC7F,MACF,CACA,GAAI,EAAQ,MAAQ,IAAA,IAAa,CAACM,EAAAA,eAAe,EAAQ,IAAI,EAAG,CAC9D,EAAO,YAAY,EAAO,yBAAyB,EAAI,uCAAuC,EAC9F,EAAO,2CAA2C,EAAK,EAAE,EACzD,MACF,CAEA,GADA,EAAO,aAAa,EAAO,MAAM,EAAI,QAAQ,GAAM,EAC/C,CAAE,MAAM,EAAe,EAAQ,IAAK,EAAQ,KAAM,EAAQ,IAAI,EAAI,CACpE,EAAO,uBAAuB,EAAI,uBAAuB,EAAK,8BAA8B,EAC5F,MACF,CACF,CASA,IAAI,EAAgC,SAAY,CAAC,EAE3C,EAAQ,SAAgC,CAC5C,IAAgBD,EAAAA,4BAA4B,QAAS,CAAM,EAC3D,IAAM,EAAS,MAAMG,EAAAA,SAAS,CAC5B,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,eAAgB,EAAQ,UACxB,YAAaC,EAAAA,mBAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAIC,EAAAA,kBACtB,QAASR,EAAAA,QAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,eAAgB,EAChB,gBAAiB,EAAQ,gBACzB,GAAI,IAAgB,IAAA,GAAY,CAAC,EAAI,CAAE,aAAY,EACnD,SAAU,EAGV,WAAa,GAAa,CACxB,EAAc,CACZ,sBAAyB,CACvB,GAAa,QAAQ,EACrB,EAAc,IAAA,EAChB,EACA,QAAS,EACT,QAAS,EACT,YACA,WACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,MAAQ,GAAS,CACf,EAAW,CACb,CACF,CAAC,CACH,CACF,CAAC,EAMD,MALA,GAAW,SAAY,CACrB,GAAa,QAAQ,EACrB,EAAc,IAAA,GACd,MAAM,EAAO,MAAM,CACrB,EACO,CACT,EAEA,GAAI,CACF,MAAM,EAAM,CACd,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAML,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAc,CAAC,CAAC,SAAW,CACzB,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAWA,eAAe,EAAS,EASN,CAChB,MAAM,EAAM,QAAQ,MAAM,EAC1B,IAAM,EAAU,MAAM,EAAM,UAAU,MAAM,CAC1C,WAAY,EAAM,SAAS,SAAS,QAAQ,WAAW,IAAK,IAAe,CAAE,KAAM,CAAU,EAAE,EAC/F,KAAM,EAAM,KACZ,WAAY,CACd,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,EAAO,gDAAgD,EAAQ,OAAO,EACtE,EAAO,6BAA6B,EACpC,MAAM,EAAM,QAAQ,EACpB,MAAMc,EAAAA,iBAAiB,CACrB,KAAM,EAAM,KACZ,KAAM,EAAM,KACZ,SAAU,EAAM,SAAS,SACzB,SAAU,CACZ,CAAC,EACD,MACF,CACA,EAAM,kBAAkB,EACxB,EAAM,MAAM,SAAY,CACtB,MAAM,EAAQ,UAAU,KAAK,CAC/B,CAAC,EAGD,MAAMC,EAAAA,oBAAoB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EACnG,MAAMD,EAAAA,iBAAiB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EAGhG,EAAO,oDAAoD,EAC3D,MAAM,EAAQ,UAAU,UAAU,CACpC,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
@@ -1,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,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{};
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{createComputerUseIpcBinding as l}from"../src/adapters/computerUseIpc.mjs";import{handOffRemoteAccess as u}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as d}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as f,probeHub as p}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as m}from"../src/adapters/sessionRelaunch.mjs";import h from"node:os";function g(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function _(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 f(t,n))return!0;return!1}async function v(){let u=o(process.argv.slice(2));if(u.help){process.stdout.write(s());return}if(u.version){process.stdout.write(`${r()}\n`);return}await d({homeDirectory:h.homedir(),onNotice:g});let m=`http://${u.host}:${String(u.port)}`,v=u.stateDir??n(),b=c({stateDir:v,onNotice:g}),x=l(process,g);await b.reapStale(),a(u.host)||(g(`WARNING: --host ${u.host} is not loopback, so the cockpit is reachable without pairing.`),g(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let S=await p(u.host,u.port);if(S!==void 0){let e=r(),t=S.version??`an older build`;if(S.version===e){g(`cockpit already running at ${m}`);return}if(S.sessions>0){g(`WARNING: ${t} is already running at ${m} with ${String(S.sessions)} live session(s).`),g(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(S.pid===void 0||!a(u.host)){g(`WARNING: ${t} is already running at ${m} and cannot be replaced automatically.`),g(`WARNING: stop it and run again to serve ${e}.`);return}if(g(`replacing ${t} at ${m} with ${e}`),!await _(S.pid,u.host,u.port)){g(`WARNING: the hub at ${m} did not stop; serve ${e} on another port with --port.`);return}}let C=async()=>{},w=async()=>{x??=l(process,g);let n=await i({port:u.port,host:u.host,assetsDir:u.assetsDir,compositionDir:u.directory,registryDir:t({flagValue:u.registryDir,envValue:process.env[e],homeDir:h.homedir()}),spawnCommand:u.spawnCommand,remoteStateDir:v,cloudflaredPath:u.cloudflaredPath,...x===void 0?{}:{computerUse:x},onNotice:g,onHandover:e=>{y({detachComputerUse:()=>{x?.close?.(),x=void 0},serving:n,restart:w,container:b,handover:e,port:u.port,host:u.host,adopt:e=>{C=e}})}});return C=async()=>{x?.close?.(),x=void 0,await n.close()},n};try{await w()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await f(u.host,u.port)){g(`cockpit already running at ${m}`);return}throw Error(`Port ${String(u.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let T=()=>{C().then(()=>{process.exit(0)})};process.on(`SIGINT`,T),process.on(`SIGTERM`,T)}async function y(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:g});if(!t.ok){g(`the cockpit could not move into a container: ${t.error}`),g(`serving from the host again`),await e.restart(),await m({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:g});return}e.detachComputerUse(),e.adopt(async()=>{await t.container.stop()}),await u({port:e.port,settings:e.handover.settings,onNotice:g}),await m({port:e.port,sessions:e.handover.sessions,onNotice:g}),g(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}v().catch(e=>{g(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 { 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
+ {"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 { createComputerUseIpcBinding } from '../adapters/computerUseIpc.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 let computerUse = createComputerUseIpcBinding(process, 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 computerUse ??= createComputerUseIpcBinding(process, notice);\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 ...(computerUse === undefined ? {} : { computerUse }),\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 detachComputerUse: () => {\n computerUse?.close?.();\n computerUse = undefined;\n },\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 computerUse?.close?.();\n computerUse = undefined;\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 detachComputerUse: () => void;\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.detachComputerUse();\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
+ let e=require("node:crypto");const t=8388608;function n(e){try{return Buffer.byteLength(JSON.stringify(e))}catch{return 1/0}}function r(e){if(n(e)>t||typeof e!=`object`||!e||Array.isArray(e))return!1;let r=e;return r.type===`doompi:computer-use:response`&&r.version===1&&typeof r.requestId==`string`&&typeof r.ok==`boolean`}function i(i=process,a=()=>void 0){if(i.connected!==!0||typeof i.send!=`function`)return;let o=new Map,s=!1,c=e=>{s||i.connected!==!0||typeof i.send!=`function`||i.send({type:`doompi:computer-use:cancel`,version:1,requestId:e})},l=(e,t)=>{let n=o.get(e);n!==void 0&&(o.delete(e),clearTimeout(n.timer),n.signal!==void 0&&n.abort!==void 0&&n.signal.removeEventListener(`abort`,n.abort),t.ok?n.resolve(t.value):n.reject(t.error))},u=e=>{r(e)&&(e.ok?l(e.requestId,{ok:!0,value:e.result}):l(e.requestId,{ok:!1,error:Error(e.error??`Desktop computer use failed.`)}))},d=()=>{if(!s){s=!0,i.connected===!0&&typeof i.send==`function`&&i.send({type:`doompi:computer-use:close`,version:1}),i.off(`message`,u),i.off(`disconnect`,f);for(let e of o.keys())l(e,{ok:!1,error:Error(`DoomPi Desktop disconnected.`)})}},f=()=>{a(`DoomPi Desktop computer-use capability disconnected`),d()};return i.on(`message`,u),i.on(`disconnect`,f),{available:!0,request:async(r,a)=>{if(s||i.connected!==!0||typeof i.send!=`function`)throw Error(`DoomPi Desktop computer-use capability is unavailable.`);if(o.size>=128)throw Error(`Too many pending computer-use requests.`);let u=(0,e.randomUUID)(),d={type:`doompi:computer-use:request`,version:1,requestId:u,sessionId:r.sessionId,operation:a.operation,...a.payload===void 0?{}:{payload:a.payload}};if(n(d)>t)throw Error(`The computer-use request is too large.`);return await new Promise((e,t)=>{let n=setTimeout(()=>{c(u),l(u,{ok:!1,error:Error(`The Desktop computer-use request timed out.`)})},3e4),r=a.signal===void 0?void 0:()=>{c(u),l(u,{ok:!1,error:Error(`The computer-use request was cancelled.`)})};if(o.set(u,{resolve:e,reject:t,timer:n,signal:a.signal,abort:r}),r!==void 0&&a.signal?.addEventListener(`abort`,r,{once:!0}),a.signal?.aborted===!0){r?.();return}i.send?.(d,e=>{e!==null&&l(u,{ok:!1,error:e})})})},close:d}}exports.createComputerUseIpcBinding=i;
2
+ //# sourceMappingURL=computerUseIpc.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computerUseIpc.cjs","names":["randomUUID"],"sources":["../../../src/adapters/computerUseIpc.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { ComputerUseHostBinding, ComputerUseHostRequest, HubSessionScope } from '@agimon-ai/doompi-web-contracts';\n\nconst REQUEST_TYPE = 'doompi:computer-use:request';\nconst RESPONSE_TYPE = 'doompi:computer-use:response';\nconst CANCEL_TYPE = 'doompi:computer-use:cancel';\nconst VERSION = 1;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst MAX_MESSAGE_BYTES = 8 * 1024 * 1024;\nconst MAX_PENDING_REQUESTS = 128;\n\nexport type IpcProcess = Pick<NodeJS.Process, 'connected' | 'send' | 'on' | 'off'>;\n\ninterface PendingRequest {\n readonly resolve: (value: unknown) => void;\n readonly reject: (error: Error) => void;\n readonly timer: ReturnType<typeof setTimeout>;\n readonly signal?: AbortSignal;\n readonly abort?: () => void;\n}\n\nfunction messageBytes(value: unknown): number {\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return Number.POSITIVE_INFINITY;\n }\n}\n\nfunction isResponse(value: unknown): value is {\n type: typeof RESPONSE_TYPE;\n version: typeof VERSION;\n requestId: string;\n ok: boolean;\n result?: unknown;\n error?: string;\n} {\n if (messageBytes(value) > MAX_MESSAGE_BYTES) return false;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const candidate = value as Record<string, unknown>;\n return (\n candidate.type === RESPONSE_TYPE &&\n candidate.version === VERSION &&\n typeof candidate.requestId === 'string' &&\n typeof candidate.ok === 'boolean'\n );\n}\n\nexport function createComputerUseIpcBinding(\n child: IpcProcess = process,\n onNotice: (message: string) => void = () => undefined,\n): ComputerUseHostBinding | undefined {\n if (child.connected !== true || typeof child.send !== 'function') return undefined;\n\n const pending = new Map<string, PendingRequest>();\n let closed = false;\n\n const cancelRequest = (requestId: string): void => {\n if (closed || child.connected !== true || typeof child.send !== 'function') return;\n child.send({ type: CANCEL_TYPE, version: VERSION, requestId });\n };\n const settle = (requestId: string, result: { ok: true; value: unknown } | { ok: false; error: Error }): void => {\n const held = pending.get(requestId);\n if (held === undefined) return;\n pending.delete(requestId);\n clearTimeout(held.timer);\n if (held.signal !== undefined && held.abort !== undefined) held.signal.removeEventListener('abort', held.abort);\n if (result.ok) held.resolve(result.value);\n else held.reject(result.error);\n };\n\n const onMessage = (value: unknown): void => {\n if (!isResponse(value)) return;\n if (value.ok) settle(value.requestId, { ok: true, value: value.result });\n else settle(value.requestId, { ok: false, error: new Error(value.error ?? 'Desktop computer use failed.') });\n };\n\n const close = (): void => {\n if (closed) return;\n closed = true;\n if (child.connected === true && typeof child.send === 'function') {\n child.send({ type: 'doompi:computer-use:close', version: VERSION });\n }\n child.off('message', onMessage);\n child.off('disconnect', onDisconnect);\n for (const requestId of pending.keys()) {\n settle(requestId, { ok: false, error: new Error('DoomPi Desktop disconnected.') });\n }\n };\n\n const onDisconnect = (): void => {\n onNotice('DoomPi Desktop computer-use capability disconnected');\n close();\n };\n\n child.on('message', onMessage);\n child.on('disconnect', onDisconnect);\n\n const request = async (scope: HubSessionScope, input: ComputerUseHostRequest): Promise<unknown> => {\n if (closed || child.connected !== true || typeof child.send !== 'function') {\n throw new Error('DoomPi Desktop computer-use capability is unavailable.');\n }\n if (pending.size >= MAX_PENDING_REQUESTS) throw new Error('Too many pending computer-use requests.');\n const requestId = randomUUID();\n const message = {\n type: REQUEST_TYPE,\n version: VERSION,\n requestId,\n sessionId: scope.sessionId,\n operation: input.operation,\n ...(input.payload === undefined ? {} : { payload: input.payload }),\n };\n if (messageBytes(message) > MAX_MESSAGE_BYTES) throw new Error('The computer-use request is too large.');\n\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n cancelRequest(requestId);\n settle(requestId, { ok: false, error: new Error('The Desktop computer-use request timed out.') });\n }, REQUEST_TIMEOUT_MS);\n const abort =\n input.signal === undefined\n ? undefined\n : (): void => {\n cancelRequest(requestId);\n settle(requestId, { ok: false, error: new Error('The computer-use request was cancelled.') });\n };\n pending.set(requestId, { resolve, reject, timer, signal: input.signal, abort });\n if (abort !== undefined) input.signal?.addEventListener('abort', abort, { once: true });\n if (input.signal?.aborted === true) {\n abort?.();\n return;\n }\n child.send?.(message, (error) => {\n if (error !== null) settle(requestId, { ok: false, error });\n });\n });\n };\n\n return { available: true, request, close };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{randomUUID as e}from"node:crypto";const t=8388608;function n(e){try{return Buffer.byteLength(JSON.stringify(e))}catch{return 1/0}}function r(e){if(n(e)>t||typeof e!=`object`||!e||Array.isArray(e))return!1;let r=e;return r.type===`doompi:computer-use:response`&&r.version===1&&typeof r.requestId==`string`&&typeof r.ok==`boolean`}function i(i=process,a=()=>void 0){if(i.connected!==!0||typeof i.send!=`function`)return;let o=new Map,s=!1,c=e=>{s||i.connected!==!0||typeof i.send!=`function`||i.send({type:`doompi:computer-use:cancel`,version:1,requestId:e})},l=(e,t)=>{let n=o.get(e);n!==void 0&&(o.delete(e),clearTimeout(n.timer),n.signal!==void 0&&n.abort!==void 0&&n.signal.removeEventListener(`abort`,n.abort),t.ok?n.resolve(t.value):n.reject(t.error))},u=e=>{r(e)&&(e.ok?l(e.requestId,{ok:!0,value:e.result}):l(e.requestId,{ok:!1,error:Error(e.error??`Desktop computer use failed.`)}))},d=()=>{if(!s){s=!0,i.connected===!0&&typeof i.send==`function`&&i.send({type:`doompi:computer-use:close`,version:1}),i.off(`message`,u),i.off(`disconnect`,f);for(let e of o.keys())l(e,{ok:!1,error:Error(`DoomPi Desktop disconnected.`)})}},f=()=>{a(`DoomPi Desktop computer-use capability disconnected`),d()};return i.on(`message`,u),i.on(`disconnect`,f),{available:!0,request:async(r,a)=>{if(s||i.connected!==!0||typeof i.send!=`function`)throw Error(`DoomPi Desktop computer-use capability is unavailable.`);if(o.size>=128)throw Error(`Too many pending computer-use requests.`);let u=e(),d={type:`doompi:computer-use:request`,version:1,requestId:u,sessionId:r.sessionId,operation:a.operation,...a.payload===void 0?{}:{payload:a.payload}};if(n(d)>t)throw Error(`The computer-use request is too large.`);return await new Promise((e,t)=>{let n=setTimeout(()=>{c(u),l(u,{ok:!1,error:Error(`The Desktop computer-use request timed out.`)})},3e4),r=a.signal===void 0?void 0:()=>{c(u),l(u,{ok:!1,error:Error(`The computer-use request was cancelled.`)})};if(o.set(u,{resolve:e,reject:t,timer:n,signal:a.signal,abort:r}),r!==void 0&&a.signal?.addEventListener(`abort`,r,{once:!0}),a.signal?.aborted===!0){r?.();return}i.send?.(d,e=>{e!==null&&l(u,{ok:!1,error:e})})})},close:d}}export{i as createComputerUseIpcBinding};
2
+ //# sourceMappingURL=computerUseIpc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computerUseIpc.mjs","names":[],"sources":["../../../src/adapters/computerUseIpc.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { ComputerUseHostBinding, ComputerUseHostRequest, HubSessionScope } from '@agimon-ai/doompi-web-contracts';\n\nconst REQUEST_TYPE = 'doompi:computer-use:request';\nconst RESPONSE_TYPE = 'doompi:computer-use:response';\nconst CANCEL_TYPE = 'doompi:computer-use:cancel';\nconst VERSION = 1;\nconst REQUEST_TIMEOUT_MS = 30_000;\nconst MAX_MESSAGE_BYTES = 8 * 1024 * 1024;\nconst MAX_PENDING_REQUESTS = 128;\n\nexport type IpcProcess = Pick<NodeJS.Process, 'connected' | 'send' | 'on' | 'off'>;\n\ninterface PendingRequest {\n readonly resolve: (value: unknown) => void;\n readonly reject: (error: Error) => void;\n readonly timer: ReturnType<typeof setTimeout>;\n readonly signal?: AbortSignal;\n readonly abort?: () => void;\n}\n\nfunction messageBytes(value: unknown): number {\n try {\n return Buffer.byteLength(JSON.stringify(value));\n } catch {\n return Number.POSITIVE_INFINITY;\n }\n}\n\nfunction isResponse(value: unknown): value is {\n type: typeof RESPONSE_TYPE;\n version: typeof VERSION;\n requestId: string;\n ok: boolean;\n result?: unknown;\n error?: string;\n} {\n if (messageBytes(value) > MAX_MESSAGE_BYTES) return false;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const candidate = value as Record<string, unknown>;\n return (\n candidate.type === RESPONSE_TYPE &&\n candidate.version === VERSION &&\n typeof candidate.requestId === 'string' &&\n typeof candidate.ok === 'boolean'\n );\n}\n\nexport function createComputerUseIpcBinding(\n child: IpcProcess = process,\n onNotice: (message: string) => void = () => undefined,\n): ComputerUseHostBinding | undefined {\n if (child.connected !== true || typeof child.send !== 'function') return undefined;\n\n const pending = new Map<string, PendingRequest>();\n let closed = false;\n\n const cancelRequest = (requestId: string): void => {\n if (closed || child.connected !== true || typeof child.send !== 'function') return;\n child.send({ type: CANCEL_TYPE, version: VERSION, requestId });\n };\n const settle = (requestId: string, result: { ok: true; value: unknown } | { ok: false; error: Error }): void => {\n const held = pending.get(requestId);\n if (held === undefined) return;\n pending.delete(requestId);\n clearTimeout(held.timer);\n if (held.signal !== undefined && held.abort !== undefined) held.signal.removeEventListener('abort', held.abort);\n if (result.ok) held.resolve(result.value);\n else held.reject(result.error);\n };\n\n const onMessage = (value: unknown): void => {\n if (!isResponse(value)) return;\n if (value.ok) settle(value.requestId, { ok: true, value: value.result });\n else settle(value.requestId, { ok: false, error: new Error(value.error ?? 'Desktop computer use failed.') });\n };\n\n const close = (): void => {\n if (closed) return;\n closed = true;\n if (child.connected === true && typeof child.send === 'function') {\n child.send({ type: 'doompi:computer-use:close', version: VERSION });\n }\n child.off('message', onMessage);\n child.off('disconnect', onDisconnect);\n for (const requestId of pending.keys()) {\n settle(requestId, { ok: false, error: new Error('DoomPi Desktop disconnected.') });\n }\n };\n\n const onDisconnect = (): void => {\n onNotice('DoomPi Desktop computer-use capability disconnected');\n close();\n };\n\n child.on('message', onMessage);\n child.on('disconnect', onDisconnect);\n\n const request = async (scope: HubSessionScope, input: ComputerUseHostRequest): Promise<unknown> => {\n if (closed || child.connected !== true || typeof child.send !== 'function') {\n throw new Error('DoomPi Desktop computer-use capability is unavailable.');\n }\n if (pending.size >= MAX_PENDING_REQUESTS) throw new Error('Too many pending computer-use requests.');\n const requestId = randomUUID();\n const message = {\n type: REQUEST_TYPE,\n version: VERSION,\n requestId,\n sessionId: scope.sessionId,\n operation: input.operation,\n ...(input.payload === undefined ? {} : { payload: input.payload }),\n };\n if (messageBytes(message) > MAX_MESSAGE_BYTES) throw new Error('The computer-use request is too large.');\n\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n cancelRequest(requestId);\n settle(requestId, { ok: false, error: new Error('The Desktop computer-use request timed out.') });\n }, REQUEST_TIMEOUT_MS);\n const abort =\n input.signal === undefined\n ? undefined\n : (): void => {\n cancelRequest(requestId);\n settle(requestId, { ok: false, error: new Error('The computer-use request was cancelled.') });\n };\n pending.set(requestId, { resolve, reject, timer, signal: input.signal, abort });\n if (abort !== undefined) input.signal?.addEventListener('abort', abort, { once: true });\n if (input.signal?.aborted === true) {\n abort?.();\n return;\n }\n child.send?.(message, (error) => {\n if (error !== null) settle(requestId, { ok: false, error });\n });\n });\n };\n\n return { available: true, request, close };\n}\n"],"mappings":"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"}
@@ -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"),le=require("./tunnelProcess.cjs"),ue=require("./directoryListing.cjs"),de=require("./sessionFiles.cjs"),fe=require("./threadJournals.cjs"),pe=require("./webHubPluginLoader.cjs"),_=require("./webTelemetry.cjs"),me=require("./sessionWebComposition.cjs");let v=require("node:fs");v=e.__toESM(v,1);let y=require("node:path");y=e.__toESM(y,1);let b=require("node:url"),x=require("@agimon-ai/doompi/services"),he=require("node:crypto"),S=require("node:os");S=e.__toESM(S,1);let ge=require("@hono/node-server"),_e=require("@hono/node-ws"),ve=require("@earendil-works/pi-coding-agent"),C=require("@earendil-works/pi-server"),ye=require("@agimon-ai/doompi-config"),be=require("@agimon-ai/doompi/services/syncState"),w=require("@agimon-ai/doompi-web-security"),xe=require("hono"),T=require("hono/cookie"),E=require("hono/body-limit"),D=require("@agimon-ai/doompi-extension-contracts/package-api"),O=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Se=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),k=require("@agimon-ai/doompi/utils/repository");const A=`DOOMPI_WEB_PACKAGE_ROOT`,Ce=`/api/web-plugins/`,we=8388608,j=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),M=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function Te(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!j.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 Ee(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();M.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 De(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 N(e){let t=process.env[A];if(t!==void 0&&t!==``)return y.default.join(t,`dist`,e);let n=y.default.dirname((0,b.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(v.default.existsSync(y.default.join(n,`package.json`)))return y.default.join(n,`dist`,e);let t=y.default.dirname(n);if(t===n)throw Error(`doompi-web could not locate its own package root.`);n=t}}function P(){return N(`web`)}function Oe(){return N(`pwa`)}function ke(){let e=process.env[A],t=e!==void 0&&e!==``?e:y.default.dirname((0,b.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let e=y.default.join(t,`package.json`);if(v.default.existsSync(e))try{let t=JSON.parse(v.default.readFileSync(e,`utf8`)),n=I(t)?t.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=y.default.dirname(t);if(n===t)return`unknown`;t=n}}function Ae(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&&v.default.existsSync(y.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):P()}function F(e){try{return v.default.statSync(e).isFile()?v.default.readFileSync(e):void 0}catch{return}}function I(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function je(e,t){let n=e.firstMessage.split(`
2
- `,1)[0]?.trim();return(e.name?.trim()||n||y.default.basename(t)||`untitled`).slice(0,80)}function L(e){return e instanceof Error?e.message:String(e)}function Me(e,t){return`${e}\n${t}`}function Ne(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,...n,telemetry:r,onNotice:t})}function R(e){return`repo-${(0,he.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function Pe(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=v.default.realpathSync((0,k.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:R(e),path:e,name:y.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 Fe(e,t,n,r,i){let a=[],o=new Map,s=e=>{for(let t of e){if(o.has(t.basePath))continue;let e;try{e=t.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${t.basePath}' did not start (${L(e)}); its routes stay unmounted`);continue}o.set(t.basePath,e),a.push(e)}};return s(t),e.all(`${D.DOOM_API_ROUTE_PREFIX}/:basePath/*`,async(e,t)=>{if(e.req.query(`session`)!==void 0)return t();let r=e.req.param(`basePath`),i=o.get(r);if(i===void 0)return t();let a=`${D.DOOM_API_ROUTE_PREFIX}/${r}`,s=new URL(e.req.url);s.pathname=s.pathname.slice(a.length)||`/`;try{return await i.fetch(new Request(s,e.req.raw))}catch(t){return n(`hub API '${r}' failed on ${s.pathname} (${L(t)})`),e.json({error:`The '${r}' API failed.`},500)}}),{handlers:a,add:s}}function Ie(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=_.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:_.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${L(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function z(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=_.createWebTelemetry(),p=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),b=e=>{try{return v.default.realpathSync((0,k.findRepositoryRoot)(e))}catch{return}},D=e=>{let t=S.default.homedir(),n=(0,k.resolveDoomConfigurationRoot)(e,t);if(n===(0,ye.globalDoomConfigDirectory)(t))return n;try{return v.default.realpathSync(n)}catch{return y.default.resolve(n)}},A=e.compositionDir===void 0?void 0:b(e.compositionDir);if(e.compositionDir!==void 0&&A===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let j=(0,ye.globalDoomConfigDirectory)(S.default.homedir()),M=n.createSyncGuard({repoRoot:j,onNotice:d}),N=async e=>{},P=async e=>{if(e===j)await M.ensureSynced();else{let t=n.createSyncGuard({repoRoot:e,onNotice:d});try{await t.ensureSynced()}finally{t.close()}}await N(e)},R=async e=>{try{await P(e)}catch(t){d(`WARNING: startup sync failed for ${e}; continuing to serve the cockpit (${L(t)})`)}};await R(j);let z=async e=>await P(D(e)),Le=[...u.readRegistryRecords(e.registryDir,d).map(e=>D(e.cwd)),...A===void 0?[]:[D(A)]];await Promise.all([...new Set(Le)].map(R));let Re={assetsDir:Ae(e.assetsDir,void 0,d),apiDirectory:void 0},B=ae.createPluginBundlePublication(p.directory,d),V=Ne(e,d,{loadChannels:async e=>{let t=D(e.cwd);await P(t);let n=(0,x.readSyncRegistration)(t);return n?.webDirectory===null||n?.webDirectory===void 0?[]:await pe.loadHubChannels(n.webDirectory,d)},webComposition:(e,t)=>{let n=me.resolveSessionWebArtifacts(D(e.cwd));if(n===void 0)return;let r=B.publish(n.id,n.pluginsDir);if(r===void 0)return;let i=r.signed.manifest.revision,a=`${Ce}${n.id}/${String(i)}`;return{id:n.id,revision:i,manifestUrl:`${a}/manifest`,rawAssetBaseUrl:`${a}/assets`,verifiedAssetBaseUrl:`/verified-plugins/${n.id}/${String(i)}`,entryPath:n.entryPath,stylePaths:n.stylePaths,channels:[...t]}}},f),H=fe.createThreadJournals({resolve:(e,t)=>V.threadJournal(e,t),onNotice:d}),U=new Set,ze=(e,t)=>{for(let n of U)t&&!n.local||n.post(e)},W=new xe.Hono,G=(0,_e.createNodeWebSocket)({app:W}),Be,K=Oe(),q=ae.createBundlePublication({assetsDir:()=>Re.assetsDir,stateDir:p.directory,onNotice:d}),J;le.reapStaleTunnel(p.directory,d);let Ve=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})},Y=ie.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??le.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),Y.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,ge.serve)({fetch:W.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{G.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:()=>q.trust(),onDeviceDropped:e=>J?.remove(e),onNotice:d,...Ve===void 0?{}:{requestHandover:Ve},contained:(0,Se.insideSandbox)(process.env),broadcastLocal:e=>ze(e,!0),broadcastAll:e=>ze(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});J=oe.createLiveWebPush({stateDir:p.directory,isConnected:e=>Y.isDeviceConnected(e),onNotice:d});let He=V.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&J?.notify()}),X=re.createRemoteGuard({loopbackPort:()=>Be,tunnelPolicy:()=>Y.tunnelPolicy(),authorize:e=>Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>Y.channelFor(e,t)!==void 0,stepUp:{required:e=>Y.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??Y.authorize((0,T.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await Y.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});W.use(`*`,X.middleware),W.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=_.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))})},_.readTraceContext(e.req.raw.headers))}),W.use(`*`,async(e,t)=>{if(X.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>we?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(W,{remote:Y,listenerOf:e=>X.listenerOf(e)});let Ue=(0,E.bodyLimit)({maxSize:we,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});W.use(`*`,async(e,t)=>X.listenerOf(e)===`local`?t():await Ue(e,t));let We=(0,E.bodyLimit)({maxSize:32768,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Ge=_.createBrowserTelemetryRateLimit();W.post(_.WEB_TELEMETRY_ROUTE,We,async e=>{if(!Ge())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=_.parseBrowserTelemetryBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n){if(_.isBrowserErrorEvent(e)){await f.recordError(_.BROWSER_ERROR_EVENT,_.createBrowserError(e),{"browser.source":e.source,...e.session_id===void 0?{}:{"session.id":e.session_id},"failure.kind":`browser`},{includeException:!0});continue}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)}),W.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=Y.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=Y.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=Te(a),s=o===void 0?void 0:Ee(o,e),c=o===void 0?void 0:De(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 W.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 Ke=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Z=e=>e.env?.sealedDeviceId;W.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Z(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:J?.publicKey()},200,{"Cache-Control":`no-store`})),W.post(m.REMOTE_PUSH_ROUTE,Ke,async e=>{let t=Z(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 J?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),W.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Z(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(J?.remove(t),e.body(null,204))});let qe=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Je=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Ye=t.createPiHubService({records:()=>V.records(),spawn:e=>V.create(e),onNotice:d}),Xe={...Ye,createSession:async()=>{throw new C.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Ze=new C.PiServer(Ye,{listeners:[qe],onError:e=>d(`protocol: ${e.message}`)}),Qe=new C.PiServer(Xe,{listeners:[Je],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([Ze.start(),Qe.start()]);let Q=ne.createProviderAuth({runtime:e.authRuntime,onNotice:d});ee.registerAuthRoutes(W,Q);let $e=Pe(V),et=()=>$e.list(),tt=e=>$e.resolve(e),nt=e=>{let t=tt(e);if(t===void 0)return;let n=(0,x.readSyncDrift)({repoRoot:t}),r;try{r=(0,x.readSyncRegistration)(t)?(0,be.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(W,{repositories:et,models:()=>Q.listModels()});let rt=Re.apiDirectory,$=Fe(W,rt===void 0&&!process.env[O.PACKAGE_API_DIR_ENV]?[]:await(0,O.loadPackageApis)(`hub`,{apiDirectory:rt,onNotice:d}),d,tt,nt);N=async e=>{let t=(0,x.readSyncRegistration)(e);if(t===void 0)return;let n=await(0,O.loadPackageApis)(`hub`,{apiDirectory:t.apiDirectory,env:{},onNotice:d});$.add(n)},await Promise.all([...new Set([j,...Le])].map(N)),Ie(W,V,d,f),W.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:2,sessions:V.snapshot().length,pid:process.pid,version:ke()})),W.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(!I(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n;try{n=v.default.statSync(t.cwd)}catch{return e.json({error:`No such directory: ${t.cwd}`},400)}if(!y.default.isAbsolute(t.cwd)||!n.isDirectory())return e.json({error:`The working directory must be an absolute path to a directory, received "${t.cwd}".`},400);let r=typeof t.name==`string`&&t.name!==``?t.name:void 0;await z(t.cwd);let i=await V.create({cwd:t.cwd,name:r,trace:_.readTraceContext(e.req.raw.headers)});return i.ok?e.json({sessionId:i.sessionId},201):e.json({error:i.error},i.code===`invalid_request`?400:502)}),W.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 z(n.cwd);let r=await V.restart(t,_.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)}),W.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 ve.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}))})}),W.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(!I(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await ve.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 z(n.cwd);let a=await V.resume(t,{sessionId:i.id,name:je(i,n.cwd)},_.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)}),W.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)}),W.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 de.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let it=t=>{if(!(X.listenerOf(t)===`local`||(0,Se.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return W.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=it(e),n=await ue.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),W.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 de.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))}),W.get(m.SESSION_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.channelFor(n,`session`),i=new Set,a=(0,he.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=Y.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(Me(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(!I(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(c,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=Me(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`&&I(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`)}}})),W.get(m.PROTOCOL_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.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=Y.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?qe:Je).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}}})),W.get(`/manifest.webmanifest`,e=>{let t=F(y.default.join(K,`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`})}),W.get(`/sw.js`,e=>{let t=F(y.default.join(K,`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":`/`})}),W.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(K,`/${t}`),r=n===void 0?void 0:F(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`})}),W.get(`${Ce}*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/api\/web-plugins\/([a-f0-9]{64})\/([1-9][0-9]*)\/(manifest|assets(\/.*))$/u.exec(t),r=n===null?void 0:Number(n[2]),i=n===null||!Number.isSafeInteger(r)?void 0:B.get(n[1]??``,r);if(n===null||i===void 0)return e.text(`That signed plugin composition is unavailable.`,404);if(n[3]===`manifest`)return e.json(i.signed,200,{"Cache-Control":`no-store`});let a=(0,w.assetFor)(i.signed.manifest,n[4]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(s);return a===void 0||s===void 0||c===void 0?e.text(`Plugin composition 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`})}),W.get(w.BUNDLE_MANIFEST_ROUTE,e=>{let t=q.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),W.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=q.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,w.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(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`})}),W.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),W.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)}),M.watch(()=>{for(let e of V.records())D(e.cwd)===j&&V.reloadChannels(e.id)}),new Promise((t,n)=>{let r=(0,ge.serve)({fetch:W.fetch,port:e.port,hostname:l},e=>{Be=e.port,G.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await Y.close(),await new Promise(e=>{H.close(),M.close(),Ze.close(),Qe.close(),He(),J?.close(),V.close(),Q.close();for(let e of $.handlers)e.close();B.close();for(let e of G.wss.clients)e.terminate();G.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await _.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{M.close(),H.close(),He(),J?.close(),V.close(),Q.close(),Y.close();for(let e of $.handlers)e.close();B.close(),f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=Pe,exports.mountHubApis=Fe,exports.packagedVersion=ke,exports.serveWeb=z;
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"),le=require("./tunnelProcess.cjs"),ue=require("./directoryListing.cjs"),_=require("./sessionFiles.cjs"),de=require("./threadJournals.cjs"),fe=require("./webHubPluginLoader.cjs"),v=require("./webTelemetry.cjs"),pe=require("./sessionWebComposition.cjs");let y=require("node:fs");y=e.__toESM(y,1);let b=require("node:path");b=e.__toESM(b,1);let x=require("node:url"),S=require("@agimon-ai/doompi/services"),me=require("node:crypto"),C=require("node:os");C=e.__toESM(C,1);let he=require("@hono/node-server"),ge=require("@hono/node-ws"),_e=require("@earendil-works/pi-coding-agent"),ve=require("@earendil-works/pi-server"),ye=require("@agimon-ai/doompi-config"),be=require("@agimon-ai/doompi/services/syncState"),xe=require("@agimon-ai/doompi-extension-contracts/session-protocol"),w=require("@agimon-ai/doompi-web-security"),Se=require("hono"),T=require("hono/cookie"),E=require("hono/body-limit"),D=require("@agimon-ai/doompi-extension-contracts/package-api"),O=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Ce=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),k=require("@agimon-ai/doompi/utils/repository");const A=`DOOMPI_WEB_PACKAGE_ROOT`,we=`/api/web-plugins/`,Te=8388608,j=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),M=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function Ee(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!j.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 De(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();M.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 Oe(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 N(e){let t=process.env[A];if(t!==void 0&&t!==``)return b.default.join(t,`dist`,e);let n=b.default.dirname((0,x.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(y.default.existsSync(b.default.join(n,`package.json`)))return b.default.join(n,`dist`,e);let t=b.default.dirname(n);if(t===n)throw Error(`doompi-web could not locate its own package root.`);n=t}}function P(){return N(`web`)}function ke(){return N(`pwa`)}function Ae(){let e=process.env[A],t=e!==void 0&&e!==``?e:b.default.dirname((0,x.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let e=b.default.join(t,`package.json`);if(y.default.existsSync(e))try{let t=JSON.parse(y.default.readFileSync(e,`utf8`)),n=I(t)?t.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=b.default.dirname(t);if(n===t)return`unknown`;t=n}}function je(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&&y.default.existsSync(b.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):P()}function F(e){try{return y.default.statSync(e).isFile()?y.default.readFileSync(e):void 0}catch{return}}function I(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Me(e,t){let n=/^bytes=(\d*)-(\d*)$/.exec(e);if(n===null||t===0)return;let[,r=``,i=``]=n;if(r===``&&i===``)return;if(r===``){let e=Number(i);return!Number.isSafeInteger(e)||e<=0?void 0:{start:Math.max(0,t-e),end:t-1}}let a=Number(r),o=i===``?t-1:Number(i);if(!(!Number.isSafeInteger(a)||!Number.isSafeInteger(o)||a>=t||o<a))return{start:a,end:Math.min(o,t-1)}}function Ne(e,t){let n=e.firstMessage.split(`
2
+ `,1)[0]?.trim();return(e.name?.trim()||n||b.default.basename(t)||`untitled`).slice(0,80)}function L(e){return e instanceof Error?e.message:String(e)}function Pe(e,t){return`${e}\n${t}`}function Fe(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,...n,...e.computerUse===void 0?{}:{computerUse:e.computerUse},telemetry:r,onNotice:t})}function R(e){return`repo-${(0,me.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function Ie(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=y.default.realpathSync((0,k.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:R(e),path:e,name:b.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 Le(e,t,n,r,i){let a=[],o=new Map,s=e=>{for(let t of e){if(o.has(t.basePath))continue;let e;try{e=t.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${t.basePath}' did not start (${L(e)}); its routes stay unmounted`);continue}o.set(t.basePath,e),a.push(e)}};return s(t),e.all(`${D.DOOM_API_ROUTE_PREFIX}/:basePath/*`,async(e,t)=>{if(e.req.query(`session`)!==void 0)return t();let r=e.req.param(`basePath`),i=o.get(r);if(i===void 0)return t();let a=`${D.DOOM_API_ROUTE_PREFIX}/${r}`,s=new URL(e.req.url);s.pathname=s.pathname.slice(a.length)||`/`;try{return await i.fetch(new Request(s,e.req.raw))}catch(t){return n(`hub API '${r}' failed on ${s.pathname} (${L(t)})`),e.json({error:`The '${r}' API failed.`},500)}}),{handlers:a,add:s}}function Re(e,t,n,r,i){e.all(`${D.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let a=e.req.query(s.API_SESSION_QUERY_PARAM);if(a===void 0)return e.notFound();let o=t.snapshot().find(e=>e.id===a);if(o===void 0)return e.json({error:`No session ${a}.`},404);if(o.apiSocketPath===void 0)return e.json({error:`Session ${a} serves no package API.`},404);let c=new URL(e.req.url);c.searchParams.delete(s.API_SESSION_QUERY_PARAM);let l=v.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:o.apiSocketPath,path:`${c.pathname}${c.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,caller:i(e),trace:v.forwardedTraceContext(t,l)}),l)}catch(t){return n(`session ${a} API is unreachable (${L(t)})`),e.json({error:`Session ${a} is not answering.`},502)}})}async function z(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=v.createWebTelemetry(),p=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),x=e=>{try{return y.default.realpathSync((0,k.findRepositoryRoot)(e))}catch{return}},D=e=>{let t=C.default.homedir(),n=(0,k.resolveDoomConfigurationRoot)(e,t);if(n===(0,ye.globalDoomConfigDirectory)(t))return n;try{return y.default.realpathSync(n)}catch{return b.default.resolve(n)}},A=e.compositionDir===void 0?void 0:x(e.compositionDir);if(e.compositionDir!==void 0&&A===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let j=(0,ye.globalDoomConfigDirectory)(C.default.homedir()),M=n.createSyncGuard({repoRoot:j,onNotice:d}),N=async e=>{},P=async e=>{if(e===j)await M.ensureSynced();else{let t=n.createSyncGuard({repoRoot:e,onNotice:d});try{await t.ensureSynced()}finally{t.close()}}await N(e)},R=async e=>{try{await P(e)}catch(t){d(`WARNING: startup sync failed for ${e}; continuing to serve the cockpit (${L(t)})`)}};await R(j);let z=async e=>await P(D(e)),ze=[...u.readRegistryRecords(e.registryDir,d).map(e=>D(e.cwd)),...A===void 0?[]:[D(A)]];await Promise.all([...new Set(ze)].map(R));let Be={assetsDir:je(e.assetsDir,void 0,d),apiDirectory:void 0},B=ae.createPluginBundlePublication(p.directory,d),V=Fe(e,d,{loadChannels:async e=>{let t=D(e.cwd);await P(t);let n=(0,S.readSyncRegistration)(t);return n?.webDirectory===null||n?.webDirectory===void 0?[]:await fe.loadHubChannels(n.webDirectory,d)},webComposition:(e,t)=>{let n=pe.resolveSessionWebArtifacts(D(e.cwd));if(n===void 0)return;let r=B.publish(n.id,n.pluginsDir);if(r===void 0)return;let i=r.signed.manifest.revision,a=`${we}${n.id}/${String(i)}`;return{id:n.id,revision:i,manifestUrl:`${a}/manifest`,rawAssetBaseUrl:`${a}/assets`,verifiedAssetBaseUrl:`/verified-plugins/${n.id}/${String(i)}`,entryPath:n.entryPath,stylePaths:n.stylePaths,channels:[...t]}}},f),H=de.createThreadJournals({resolve:(e,t)=>V.threadJournal(e,t),onNotice:d}),U=new Set,Ve=(e,t)=>{for(let n of U)t&&!n.local||n.post(e)},W=new Se.Hono,G=(0,ge.createNodeWebSocket)({app:W}),He,K=ke(),q=ae.createBundlePublication({assetsDir:()=>Be.assetsDir,stateDir:p.directory,onNotice:d}),J;le.reapStaleTunnel(p.directory,d);let Ue=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})},Y=ie.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??le.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),Y.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,he.serve)({fetch:W.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{G.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:()=>q.trust(),onDeviceDropped:e=>J?.remove(e),onNotice:d,...Ue===void 0?{}:{requestHandover:Ue},contained:(0,Ce.insideSandbox)(process.env),broadcastLocal:e=>Ve(e,!0),broadcastAll:e=>Ve(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});J=oe.createLiveWebPush({stateDir:p.directory,isConnected:e=>Y.isDeviceConnected(e),onNotice:d});let We=V.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&J?.notify()}),X=re.createRemoteGuard({loopbackPort:()=>He,tunnelPolicy:()=>Y.tunnelPolicy(),authorize:e=>Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>Y.channelFor(e,t)!==void 0,stepUp:{required:e=>Y.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??Y.authorize((0,T.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await Y.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});W.use(`*`,X.middleware),W.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=v.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))})},v.readTraceContext(e.req.raw.headers))}),W.use(`*`,async(e,t)=>{if(X.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>Te?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(W,{remote:Y,listenerOf:e=>X.listenerOf(e)});let Ge=(0,E.bodyLimit)({maxSize:Te,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});W.use(`*`,async(e,t)=>X.listenerOf(e)===`local`?t():await Ge(e,t));let Ke=(0,E.bodyLimit)({maxSize:32768,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),qe=v.createBrowserTelemetryRateLimit();W.post(v.WEB_TELEMETRY_ROUTE,Ke,async e=>{if(!qe())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=v.parseBrowserTelemetryBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n){if(v.isBrowserErrorEvent(e)){await f.recordError(v.BROWSER_ERROR_EVENT,v.createBrowserError(e),{"browser.source":e.source,...e.session_id===void 0?{}:{"session.id":e.session_id},"failure.kind":`browser`},{includeException:!0});continue}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)}),W.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=Y.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=Y.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=Ee(a),s=o===void 0?void 0:De(o,e),c=o===void 0?void 0:Oe(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 W.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,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Z=e=>e.env?.sealedDeviceId;W.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Z(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:J?.publicKey()},200,{"Cache-Control":`no-store`})),W.post(m.REMOTE_PUSH_ROUTE,Je,async e=>{let t=Z(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 J?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),W.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Z(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(J?.remove(t),e.body(null,204))});let Ye=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Xe=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Ze=t.createPiHubService({records:()=>V.records(),spawn:e=>V.create(e),onNotice:d}),Qe=new ve.Server(Ze,{serverId:xe.DOOM_COCKPIT_SERVER_ID,listeners:[Ye],onError:e=>d(`protocol: ${e.message}`)}),$e=new ve.Server(Ze,{serverId:xe.DOOM_COCKPIT_SERVER_ID,listeners:[Xe],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([Qe.start(),$e.start()]);let Q=ne.createProviderAuth({runtime:e.authRuntime,onNotice:d});ee.registerAuthRoutes(W,Q);let et=Ie(V),tt=()=>et.list(),nt=e=>et.resolve(e),rt=e=>{let t=nt(e);if(t===void 0)return;let n=(0,S.readSyncDrift)({repoRoot:t}),r;try{r=(0,S.readSyncRegistration)(t)?(0,be.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(W,{repositories:tt,models:()=>Q.listModels()});let it=Be.apiDirectory,$=Le(W,it===void 0&&!process.env[O.PACKAGE_API_DIR_ENV]?[]:await(0,O.loadPackageApis)(`hub`,{apiDirectory:it,onNotice:d}),d,nt,rt);N=async e=>{let t=(0,S.readSyncRegistration)(e);if(t===void 0)return;let n=await(0,O.loadPackageApis)(`hub`,{apiDirectory:t.apiDirectory,env:{},onNotice:d});$.add(n)},await Promise.all([...new Set([j,...ze])].map(N)),Re(W,V,d,f,e=>X.callerOf(e)),W.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:2,sessions:V.snapshot().length,pid:process.pid,version:Ae()})),W.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(!I(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n;try{n=y.default.statSync(t.cwd)}catch{return e.json({error:`No such directory: ${t.cwd}`},400)}if(!b.default.isAbsolute(t.cwd)||!n.isDirectory())return e.json({error:`The working directory must be an absolute path to a directory, received "${t.cwd}".`},400);let r=typeof t.name==`string`&&t.name!==``?t.name:void 0;await z(t.cwd);let i=await V.create({cwd:t.cwd,name:r,trace:v.readTraceContext(e.req.raw.headers)});return i.ok?e.json({sessionId:i.sessionId},201):e.json({error:i.error},i.code===`invalid_request`?400:502)}),W.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 z(n.cwd);let r=await V.restart(t,v.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)}),W.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 _e.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}))})}),W.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(!I(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await _e.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 z(n.cwd);let a=await V.resume(t,{sessionId:i.id,name:Ne(i,n.cwd)},v.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)}),W.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)}),W.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 _.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let at=t=>{if(!(X.listenerOf(t)===`local`||(0,Ce.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};W.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=at(e),n=await ue.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),W.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 _.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);if(o.status===`forbidden`)return e.json({error:`The path leaves the session directory.`},403);if(o.status===`not-found`)return e.json({error:`No such file.`},404);if(o.status===`too-large`)return e.json({error:`The file is too large to preview.`},413);let s={...a.sessionFileHeaders(r),[i.SESSION_FILE_SHA256_HEADER]:o.sha256,"Accept-Ranges":`bytes`},c=e.req.header(`range`);if(c===void 0)return e.body(new Uint8Array(o.body),200,s);let l=Me(c,o.body.byteLength);if(l===void 0)return e.body(null,416,{...s,"Content-Range":`bytes */${String(o.body.byteLength)}`});let u=o.body.subarray(l.start,l.end+1);return e.body(new Uint8Array(u),206,{...s,"Content-Range":`bytes ${String(l.start)}-${String(l.end)}/${String(o.body.byteLength)}`,"Content-Length":String(u.byteLength)})});let ot=(0,E.bodyLimit)({maxSize:i.MAX_SESSION_FILE_BYTES,onError:e=>e.json({error:`The file is too large to save.`},413)});return W.put(i.SESSION_FILE_ROUTE,ot,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);if(V.planSaveState(t)!==`allowed`)return e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423);let r=e.req.header(i.SESSION_FILE_EXPECTED_SHA256_HEADER);if(r===void 0||!/^[a-f0-9]{64}$/.test(r))return e.json({error:`A lowercase SHA-256 ${i.SESSION_FILE_EXPECTED_SHA256_HEADER} header is required.`},400);let a=Buffer.from(await e.req.arrayBuffer()),o=await _.writeSessionFile(n.cwd,e.req.query(`path`)??``,a,r,i.MAX_SESSION_FILE_BYTES,()=>V.planSaveState(t)===`allowed`);return o.status===`ok`?e.body(null,204,{[i.SESSION_FILE_SHA256_HEADER]:o.sha256}):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 save.`},413):o.status===`locked`?e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423):e.json({error:`The file changed after it was read.`},409)}),W.get(m.SESSION_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.channelFor(n,`session`),i=new Set,a=(0,me.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=Y.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(Pe(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(!I(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(c,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=Pe(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`&&I(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`)}}})),W.get(m.PROTOCOL_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.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=Y.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?Ye:Xe).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}}})),W.get(`/manifest.webmanifest`,e=>{let t=F(b.default.join(K,`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`})}),W.get(`/sw.js`,e=>{let t=F(b.default.join(K,`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":`/`})}),W.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(K,`/${t}`),r=n===void 0?void 0:F(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`})}),W.get(`${we}*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/api\/web-plugins\/([a-f0-9]{64})\/([1-9][0-9]*)\/(manifest|assets(\/.*))$/u.exec(t),r=n===null?void 0:Number(n[2]),i=n===null||!Number.isSafeInteger(r)?void 0:B.get(n[1]??``,r);if(n===null||i===void 0)return e.text(`That signed plugin composition is unavailable.`,404);if(n[3]===`manifest`)return e.json(i.signed,200,{"Cache-Control":`no-store`});let a=(0,w.assetFor)(i.signed.manifest,n[4]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(s);return a===void 0||s===void 0||c===void 0?e.text(`Plugin composition 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`})}),W.get(w.BUNDLE_MANIFEST_ROUTE,e=>{let t=q.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),W.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=q.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,w.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(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`})}),W.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),W.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)}),M.watch(()=>{for(let e of V.records())D(e.cwd)===j&&V.reloadChannels(e.id)}),new Promise((t,n)=>{let r=(0,he.serve)({fetch:W.fetch,port:e.port,hostname:l},e=>{He=e.port,G.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await Y.close(),await new Promise(e=>{H.close(),M.close(),Qe.close(),$e.close(),We(),J?.close(),V.close(),Q.close();for(let e of $.handlers)e.close();B.close();for(let e of G.wss.clients)e.terminate();G.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await v.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{M.close(),H.close(),We(),J?.close(),V.close(),Q.close(),Y.close();for(let e of $.handlers)e.close();B.close(),f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=Ie,exports.mountHubApis=Le,exports.packagedVersion=Ae,exports.serveWeb=z;
3
3
  //# sourceMappingURL=httpServer.cjs.map