@aoagents/ao-web 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +196 -182
- package/.next/app-path-routes-manifest.json +19 -17
- package/.next/build-manifest.json +6 -6
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +21 -21
- package/.next/react-loadable-manifest.json +5 -3
- package/.next/required-server-files.json +4 -5
- package/.next/server/app/_not-found/page.js +2 -2
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +8 -8
- package/.next/server/app/api/backlog/route.js +1 -1
- package/.next/server/app/api/backlog/route.js.nft.json +1 -1
- package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/browse-directory/route.js +1 -1
- package/.next/server/app/api/browse-directory/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/browse/route.js +1 -1
- package/.next/server/app/api/filesystem/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/issues/route.js +1 -1
- package/.next/server/app/api/issues/route.js.nft.json +1 -1
- package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/observability/route.js +1 -1
- package/.next/server/app/api/observability/route.js.nft.json +1 -1
- package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/orchestrators/route.js +1 -1
- package/.next/server/app/api/orchestrators/route.js.nft.json +1 -1
- package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +5 -1
- package/.next/server/app/api/projects/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/reload/route.js +1 -1
- package/.next/server/app/api/projects/reload/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/reload/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +1 -1
- package/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/prs/[id]/merge/route.js +1 -1
- package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -1
- package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/runtime/terminal/route.js +1 -1
- package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/message/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/[id]/send/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/patches/route.js +1 -1
- package/.next/server/app/api/sessions/patches/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/patches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/setup-labels/route.js +1 -1
- package/.next/server/app/api/setup-labels/route.js.nft.json +1 -1
- package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/spawn/route.js +1 -1
- package/.next/server/app/api/spawn/route.js.nft.json +1 -1
- package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/update/route.js +1 -0
- package/.next/server/app/api/update/route.js.nft.json +1 -0
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/verify/route.js +1 -1
- package/.next/server/app/api/verify/route.js.nft.json +1 -1
- package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/version/route.js +1 -0
- package/.next/server/app/api/version/route.js.nft.json +1 -0
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/webhooks/[...slug]/route.js +1 -1
- package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -1
- package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/apple-icon/route.js +1 -1
- package/.next/server/app/apple-icon/route.js.nft.json +1 -1
- package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -1
- package/.next/server/app/apple-icon.body +0 -0
- package/.next/server/app/dev/terminal-test/page.js +3 -3
- package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -1
- package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -1
- package/.next/server/app/dev/terminal-test.html +1 -1
- package/.next/server/app/dev/terminal-test.rsc +9 -9
- package/.next/server/app/icon/route.js +1 -1
- package/.next/server/app/icon/route.js.nft.json +1 -1
- package/.next/server/app/icon/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon-192/route.js +1 -1
- package/.next/server/app/icon-192/route.js.nft.json +1 -1
- package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon-512/route.js +1 -1
- package/.next/server/app/icon-512/route.js.nft.json +1 -1
- package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -1
- package/.next/server/app/icon.body +0 -0
- package/.next/server/app/manifest.webmanifest/route.js +2 -2
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest.body +1 -1
- package/.next/server/app/orchestrators/page.js +2 -2
- package/.next/server/app/orchestrators/page.js.nft.json +1 -1
- package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/page.js +2 -2
- package/.next/server/app/projects/[projectId]/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/sessions/[id]/page.js +2 -2
- package/.next/server/app/projects/[projectId]/sessions/[id]/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[projectId]/settings/page.js +2 -2
- package/.next/server/app/projects/[projectId]/settings/page.js.nft.json +1 -1
- package/.next/server/app/projects/[projectId]/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/prs/page.js +2 -2
- package/.next/server/app/prs/page.js.nft.json +1 -1
- package/.next/server/app/prs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/[id]/page.js +2 -2
- package/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/test-direct/page.js +2 -2
- package/.next/server/app/test-direct/page.js.nft.json +1 -1
- package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -1
- package/.next/server/app/test-direct.html +1 -1
- package/.next/server/app/test-direct.rsc +9 -9
- package/.next/server/app-paths-manifest.json +19 -17
- package/.next/server/chunks/1271.js +1 -1
- package/.next/server/chunks/1876.js +2 -2
- package/.next/server/chunks/303.js +3 -0
- package/.next/server/chunks/3714.js +1 -1
- package/.next/server/chunks/4520.js +1 -1
- package/.next/server/chunks/6013.js +884 -0
- package/.next/server/chunks/6848.js +1 -0
- package/.next/server/chunks/7167.js +1 -1
- package/.next/server/chunks/7173.js +1 -1
- package/.next/server/chunks/7227.js +604 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/9nr0fNWbZcuWTqhM2HhrH/_buildManifest.js +1 -0
- package/.next/static/chunks/1654.ac304fc9e36ec94a.js +1 -0
- package/.next/static/chunks/3764.cdef4e76dbc23af8.js +1 -0
- package/.next/static/chunks/3780-7bdc52d8370adf2f.js +1 -0
- package/.next/static/chunks/{5795-b96fd46c8c7344fc.js → 5795-a4dd81606df09bc4.js} +1 -1
- package/.next/static/chunks/6231-57dd9c1e306c7069.js +1 -0
- package/.next/static/chunks/app/_not-found/page-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/backlog/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/browse-directory/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/filesystem/browse/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/issues/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/observability/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/orchestrators/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/projects/[id]/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/projects/reload/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/projects/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/prs/[id]/merge/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/runtime/terminal/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/kill/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/message/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/remap/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/restore/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/send/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/patches/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/setup-labels/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/spawn/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/update/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/verify/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/version/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/api/webhooks/[...slug]/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/apple-icon/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/dev/terminal-test/page-9aa423dfd54c8325.js +1 -0
- package/.next/static/chunks/app/{error-da1d10c96ff5dd29.js → error-65c526052680c0dc.js} +1 -1
- package/.next/static/chunks/app/{global-error-ca06d2b1be2d4ae0.js → global-error-63dcb797b2c3ee60.js} +1 -1
- package/.next/static/chunks/app/icon/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/icon-192/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/icon-512/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/{layout-bb6db479523cb3d6.js → layout-36ab0168ddb22083.js} +1 -1
- package/.next/static/chunks/app/loading-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/manifest.webmanifest/route-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/{not-found-824d5d3c6e296eeb.js → not-found-a693bed1f9e1893f.js} +1 -1
- package/.next/static/chunks/app/orchestrators/{page-f07983413ed1a44b.js → page-376a92db51deb112.js} +1 -1
- package/.next/static/chunks/app/page-587d546e62c0796f.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/loading-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/page-bd8fc2a1decb649d.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-bd33f6ffda513080.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/settings/page-11facc471a63de50.js +1 -0
- package/.next/static/chunks/app/prs/page-f34f66ad51106080.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/{error-8de5b24e86eeae7b.js → error-df65e7b626bbb713.js} +1 -1
- package/.next/static/chunks/app/sessions/[id]/loading-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/{not-found-824d5d3c6e296eeb.js → not-found-a693bed1f9e1893f.js} +1 -1
- package/.next/static/chunks/app/sessions/[id]/page-3ea4aa79275ea449.js +1 -0
- package/.next/static/chunks/app/test-direct/page-edfc701a9300105b.js +1 -0
- package/.next/static/chunks/{main-app-690acf9d5d2050c9.js → main-app-decbc53736801215.js} +1 -1
- package/.next/static/chunks/{webpack-d2dfbd3e9262b74e.js → webpack-ecf0988dbb79e19b.js} +1 -1
- package/.next/static/css/b93232cd4a58743d.css +1 -0
- package/dist-server/direct-terminal-ws.js +12 -2
- package/dist-server/mux-websocket.js +319 -76
- package/dist-server/start-all.js +28 -6
- package/dist-server/tmux-utils.js +124 -2
- package/next.config.js +26 -0
- package/package.json +26 -12
- package/.next/server/chunks/1172.js +0 -1
- package/.next/server/chunks/3131.js +0 -3
- package/.next/server/chunks/801.js +0 -658
- package/.next/server/chunks/9223.js +0 -440
- package/.next/static/YR6Xi4DC5A7S7E2PoZuif/_buildManifest.js +0 -1
- package/.next/static/chunks/1383.c891a8ba72ee600c.js +0 -1
- package/.next/static/chunks/3764.89a5955e46eb74b4.js +0 -1
- package/.next/static/chunks/3780-52c4733ce6591b8d.js +0 -1
- package/.next/static/chunks/4465-17154f7a01abfe85.js +0 -1
- package/.next/static/chunks/app/_not-found/page-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/backlog/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/browse-directory/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/filesystem/browse/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/issues/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/observability/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/orchestrators/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/projects/[id]/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/projects/reload/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/projects/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/prs/[id]/merge/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/runtime/terminal/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/kill/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/message/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/remap/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/restore/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/send/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/patches/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/setup-labels/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/spawn/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/verify/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/api/webhooks/[...slug]/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/apple-icon/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/dev/terminal-test/page-5819e40b3d4754ef.js +0 -1
- package/.next/static/chunks/app/icon/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/icon-192/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/icon-512/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/loading-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/manifest.webmanifest/route-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/page-6aa506a579ac9949.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/loading-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/page-49eb5b990c74ca8f.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-2450704c6b66a4b4.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/settings/page-d1da671e72a7bd5e.js +0 -1
- package/.next/static/chunks/app/prs/page-2332a7180a47f28c.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/loading-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/page-b60b49ccbafe51c9.js +0 -1
- package/.next/static/chunks/app/test-direct/page-eb366dde03fab6a7.js +0 -1
- package/.next/static/css/fcafd381715071b8.css +0 -1
- /package/.next/static/{YR6Xi4DC5A7S7E2PoZuif → 9nr0fNWbZcuWTqhM2HhrH}/_ssgManifest.js +0 -0
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* every 3s, then broadcast to all subscribed clients via WebSocket.
|
|
7
7
|
*/
|
|
8
8
|
import { WebSocketServer, WebSocket } from "ws";
|
|
9
|
-
import { homedir, userInfo } from "node:os";
|
|
10
9
|
import { spawn } from "node:child_process";
|
|
11
|
-
import {
|
|
10
|
+
import { connect as netConnect } from "node:net";
|
|
11
|
+
import { findTmux, resolveTmuxSession, resolvePipePath, tmuxHasSession, validateSessionId, } from "./tmux-utils.js";
|
|
12
|
+
import { getEnvDefaults, isWindows } from "@aoagents/ao-core";
|
|
12
13
|
/**
|
|
13
14
|
* Manages polling of session patches from Next.js /api/sessions/patches.
|
|
14
15
|
* Broadcasts to all subscribed callbacks.
|
|
@@ -141,15 +142,31 @@ catch (err) {
|
|
|
141
142
|
const RING_BUFFER_MAX = 50 * 1024; // 50KB max per terminal
|
|
142
143
|
const WS_BUFFER_HIGH_WATERMARK = 64 * 1024; // 64KB
|
|
143
144
|
const MAX_REATTACH_ATTEMPTS = 3;
|
|
145
|
+
/**
|
|
146
|
+
* Grace period a freshly-attached PTY must survive before its successful
|
|
147
|
+
* attach is allowed to reset the re-attach counter. Prevents tight crash
|
|
148
|
+
* loops (e.g. attaching to a tmux session that no longer exists) from
|
|
149
|
+
* gaming the MAX_REATTACH_ATTEMPTS cap by resetting the counter to 0
|
|
150
|
+
* between every failed attempt.
|
|
151
|
+
*
|
|
152
|
+
* 5 s is comfortably longer than the ~40 ms a doomed `tmux attach-session`
|
|
153
|
+
* takes to exit, while still being short enough that a healthy PTY which
|
|
154
|
+
* crashes hours later gets a fresh retry budget.
|
|
155
|
+
*/
|
|
156
|
+
const REATTACH_RESET_GRACE_MS = 5_000;
|
|
144
157
|
/**
|
|
145
158
|
* TerminalManager manages PTY processes independently of WebSocket connections.
|
|
146
159
|
* A single manager instance is shared across all mux connections.
|
|
147
160
|
*/
|
|
148
|
-
class TerminalManager {
|
|
161
|
+
export class TerminalManager {
|
|
149
162
|
terminals = new Map();
|
|
150
163
|
TMUX;
|
|
151
164
|
constructor(tmuxPath) {
|
|
152
|
-
|
|
165
|
+
const resolved = tmuxPath ?? findTmux();
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
throw new Error("tmux not available on this platform");
|
|
168
|
+
}
|
|
169
|
+
this.TMUX = resolved;
|
|
153
170
|
}
|
|
154
171
|
terminalKey(id, projectId) {
|
|
155
172
|
return projectId ? `${projectId}:${id}` : id;
|
|
@@ -189,33 +206,37 @@ class TerminalManager {
|
|
|
189
206
|
if (terminal.pty) {
|
|
190
207
|
return tmuxSessionId;
|
|
191
208
|
}
|
|
209
|
+
// tmux 3.4 only honours the `=` exact-match prefix on has-session and
|
|
210
|
+
// attach-session; set-option silently ignores it, so we use the bare id
|
|
211
|
+
// here. The `=`-prefixed form is built below for attach-session.
|
|
192
212
|
// Enable mouse mode
|
|
193
|
-
const
|
|
194
|
-
const mouseProc = spawn(this.TMUX, ["set-option", "-t", exactTmuxTarget, "mouse", "on"]);
|
|
213
|
+
const mouseProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "mouse", "on"]);
|
|
195
214
|
mouseProc.on("error", (err) => {
|
|
196
215
|
console.error(`[MuxServer] Failed to set mouse mode for ${tmuxSessionId}:`, err.message);
|
|
197
216
|
});
|
|
198
217
|
// Hide the status bar
|
|
199
|
-
const statusProc = spawn(this.TMUX, ["set-option", "-t",
|
|
218
|
+
const statusProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "status", "off"]);
|
|
200
219
|
statusProc.on("error", (err) => {
|
|
201
220
|
console.error(`[MuxServer] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
|
|
202
221
|
});
|
|
203
222
|
// Build environment
|
|
204
|
-
const
|
|
205
|
-
const
|
|
223
|
+
const platformDefaults = getEnvDefaults();
|
|
224
|
+
const homeDir = platformDefaults.HOME;
|
|
206
225
|
const env = {
|
|
207
|
-
HOME:
|
|
208
|
-
SHELL:
|
|
209
|
-
USER:
|
|
210
|
-
PATH: process.env.PATH ||
|
|
226
|
+
HOME: platformDefaults.HOME,
|
|
227
|
+
SHELL: platformDefaults.SHELL,
|
|
228
|
+
USER: platformDefaults.USER,
|
|
229
|
+
PATH: process.env.PATH || platformDefaults.PATH,
|
|
211
230
|
TERM: "xterm-256color",
|
|
212
231
|
LANG: process.env.LANG || "en_US.UTF-8",
|
|
213
|
-
TMPDIR:
|
|
232
|
+
TMPDIR: platformDefaults.TMPDIR,
|
|
214
233
|
};
|
|
215
234
|
if (!ptySpawn) {
|
|
216
235
|
throw new Error("node-pty not available");
|
|
217
236
|
}
|
|
218
|
-
// Spawn PTY
|
|
237
|
+
// Spawn PTY — use `=`-prefixed exact-match target so we never attach to
|
|
238
|
+
// a session whose name happens to be a prefix of the requested id.
|
|
239
|
+
const exactTmuxTarget = `=${tmuxSessionId}`;
|
|
219
240
|
const pty = ptySpawn(this.TMUX, ["attach-session", "-t", exactTmuxTarget], {
|
|
220
241
|
name: "xterm-256color",
|
|
221
242
|
cols: 80,
|
|
@@ -224,6 +245,24 @@ class TerminalManager {
|
|
|
224
245
|
env,
|
|
225
246
|
});
|
|
226
247
|
terminal.pty = pty;
|
|
248
|
+
// Schedule a grace-period reset of the re-attach counter. We only
|
|
249
|
+
// consider an attach "really successful" if the PTY survives long
|
|
250
|
+
// enough to suggest the underlying tmux session is actually usable.
|
|
251
|
+
// The closure-captured `pty` reference is compared with terminal.pty
|
|
252
|
+
// so a stale timer cannot reset the counter for a PTY that has
|
|
253
|
+
// already exited or been replaced by re-attach. Any previously-
|
|
254
|
+
// scheduled timer (from a now-replaced PTY) is cleared so we don't
|
|
255
|
+
// keep its closure references reachable until the timer fires.
|
|
256
|
+
if (terminal.resetTimer) {
|
|
257
|
+
clearTimeout(terminal.resetTimer);
|
|
258
|
+
}
|
|
259
|
+
terminal.resetTimer = setTimeout(() => {
|
|
260
|
+
terminal.resetTimer = undefined;
|
|
261
|
+
if (terminal.pty === pty) {
|
|
262
|
+
terminal.reattachAttempts = 0;
|
|
263
|
+
}
|
|
264
|
+
}, REATTACH_RESET_GRACE_MS);
|
|
265
|
+
terminal.resetTimer.unref();
|
|
227
266
|
// Wire up data events
|
|
228
267
|
pty.onData((data) => {
|
|
229
268
|
// Push to all subscribers — isolate each callback so a throw in one
|
|
@@ -246,18 +285,49 @@ class TerminalManager {
|
|
|
246
285
|
}
|
|
247
286
|
});
|
|
248
287
|
// Handle PTY exit
|
|
249
|
-
|
|
288
|
+
//
|
|
289
|
+
// Async: the has-session probe shells out via promisified execFile and
|
|
290
|
+
// must be awaited. node-pty fires onExit on the main thread; a sync
|
|
291
|
+
// probe would freeze the entire web server (every WebSocket, HTTP
|
|
292
|
+
// request, in-flight terminal) for up to the subprocess timeout when
|
|
293
|
+
// tmux is slow to respond.
|
|
294
|
+
pty.onExit(async ({ exitCode }) => {
|
|
250
295
|
console.log(`[MuxServer] PTY exited for ${id} with code ${exitCode}`);
|
|
251
296
|
terminal.pty = null;
|
|
297
|
+
// Skip the re-attach loop entirely when the underlying tmux session is
|
|
298
|
+
// gone (e.g. user pressed Ctrl-C in the pane and the launch command
|
|
299
|
+
// exited, taking the only window with it). Without this guard we
|
|
300
|
+
// burn three doomed attach-session spawns and emit a noisy
|
|
301
|
+
// "Max re-attach attempts reached" log line for what is actually a
|
|
302
|
+
// clean user-initiated termination — see issue #1756. The
|
|
303
|
+
// MAX_REATTACH_ATTEMPTS bound from #1640 still covers tmux server
|
|
304
|
+
// hiccups where the session does still exist.
|
|
305
|
+
if (terminal.subscribers.size > 0 &&
|
|
306
|
+
!(await tmuxHasSession(this.TMUX, tmuxSessionId))) {
|
|
307
|
+
console.log(`[MuxServer] tmux session ${tmuxSessionId} is gone, not re-attaching`);
|
|
308
|
+
if (terminal.resetTimer) {
|
|
309
|
+
clearTimeout(terminal.resetTimer);
|
|
310
|
+
terminal.resetTimer = undefined;
|
|
311
|
+
}
|
|
312
|
+
for (const cb of terminal.exitCallbacks) {
|
|
313
|
+
cb(exitCode);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
252
317
|
// Re-attach if subscribers are still present, up to MAX_REATTACH_ATTEMPTS.
|
|
253
318
|
// The cap prevents an unbounded respawn loop when the PTY crashes immediately
|
|
254
319
|
// after every attach (e.g. resource exhaustion or a broken tmux session).
|
|
320
|
+
// The counter is reset by a delayed timer in open() once the new PTY has
|
|
321
|
+
// survived REATTACH_RESET_GRACE_MS — see the comment on that constant.
|
|
322
|
+
// Resetting here would defeat the cap: when ao stop kills the tmux session
|
|
323
|
+
// out from under a still-subscribed dashboard, attach-session exits ~40 ms
|
|
324
|
+
// after spawn and the loop runs at ~80 spawns/sec, exhausting the system
|
|
325
|
+
// PTY pool in seconds (issue #1639).
|
|
255
326
|
if (terminal.subscribers.size > 0 && terminal.reattachAttempts < MAX_REATTACH_ATTEMPTS) {
|
|
256
327
|
terminal.reattachAttempts += 1;
|
|
257
328
|
console.log(`[MuxServer] Re-attaching to ${id} (attempt ${terminal.reattachAttempts}/${MAX_REATTACH_ATTEMPTS})`);
|
|
258
329
|
try {
|
|
259
330
|
this.open(id, projectId, tmuxSessionId);
|
|
260
|
-
terminal.reattachAttempts = 0; // reset on successful attach
|
|
261
331
|
return; // re-attached — don't notify exit
|
|
262
332
|
}
|
|
263
333
|
catch (err) {
|
|
@@ -317,6 +387,10 @@ class TerminalManager {
|
|
|
317
387
|
terminal.exitCallbacks.delete(onExit);
|
|
318
388
|
// Kill PTY and clean up when the last subscriber leaves
|
|
319
389
|
if (terminal.subscribers.size === 0) {
|
|
390
|
+
if (terminal.resetTimer) {
|
|
391
|
+
clearTimeout(terminal.resetTimer);
|
|
392
|
+
terminal.resetTimer = undefined;
|
|
393
|
+
}
|
|
320
394
|
if (terminal.pty) {
|
|
321
395
|
terminal.pty.kill();
|
|
322
396
|
terminal.pty = null;
|
|
@@ -335,22 +409,163 @@ class TerminalManager {
|
|
|
335
409
|
return terminal.buffer.join("");
|
|
336
410
|
}
|
|
337
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Handle a Windows terminal message by relaying through named pipes.
|
|
414
|
+
* Extracted from the WebSocket connection handler for testability.
|
|
415
|
+
*/
|
|
416
|
+
export function handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, deps) {
|
|
417
|
+
const WS_OPEN = 1; // WebSocket.OPEN
|
|
418
|
+
const { id, type, projectId } = msg;
|
|
419
|
+
// MuxProvider keys subscribers under `${projectId}:${id}` when projectId is
|
|
420
|
+
// provided, so every outbound terminal message must echo projectId back —
|
|
421
|
+
// otherwise the client routes by id alone and the subscriber bucket
|
|
422
|
+
// mismatches, leaving the xterm pane blank on /projects/[id]/sessions/[id].
|
|
423
|
+
const echo = projectId ? { projectId } : {};
|
|
424
|
+
// Project-scoped pipe-map key: matches the Unix `subscriptionKey` shape so
|
|
425
|
+
// two projects sharing a sessionId on the same mux connection don't collide
|
|
426
|
+
// on the same socket/buffer entry.
|
|
427
|
+
const pipeKey = projectId ? `${projectId}:${id}` : id;
|
|
428
|
+
// The Unix path validates inside TerminalManager.open(). The Windows pipe
|
|
429
|
+
// relay bypasses TerminalManager entirely, so validate here too — `id`
|
|
430
|
+
// becomes a map key and is constructed into a pipe path downstream.
|
|
431
|
+
if (!validateSessionId(id)) {
|
|
432
|
+
if (ws.readyState === WS_OPEN) {
|
|
433
|
+
ws.send(JSON.stringify({
|
|
434
|
+
ch: "terminal",
|
|
435
|
+
id,
|
|
436
|
+
type: "error",
|
|
437
|
+
message: "invalid session id",
|
|
438
|
+
...echo,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (type === "open") {
|
|
444
|
+
if (winPipes.has(pipeKey)) {
|
|
445
|
+
ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const pipePath = deps.resolvePipePath(id, projectId);
|
|
449
|
+
if (!pipePath) {
|
|
450
|
+
throw new Error(`No PTY host pipe found for session ${id}`);
|
|
451
|
+
}
|
|
452
|
+
const pipeSocket = deps.connect(pipePath);
|
|
453
|
+
winPipes.set(pipeKey, pipeSocket);
|
|
454
|
+
winPipeBuffers.set(pipeKey, Buffer.alloc(0));
|
|
455
|
+
pipeSocket.on("error", (err) => {
|
|
456
|
+
winPipes.delete(pipeKey);
|
|
457
|
+
winPipeBuffers.delete(pipeKey);
|
|
458
|
+
pipeSocket.destroy();
|
|
459
|
+
if (ws.readyState === WS_OPEN) {
|
|
460
|
+
ws.send(JSON.stringify({
|
|
461
|
+
ch: "terminal",
|
|
462
|
+
id,
|
|
463
|
+
type: "error",
|
|
464
|
+
message: `PTY host not available: ${err.message}`,
|
|
465
|
+
...echo,
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
pipeSocket.on("connect", () => {
|
|
470
|
+
if (ws.readyState === WS_OPEN) {
|
|
471
|
+
ws.send(JSON.stringify({ ch: "terminal", id, type: "opened", ...echo }));
|
|
472
|
+
}
|
|
473
|
+
pipeSocket.on("data", (chunk) => {
|
|
474
|
+
const existing = winPipeBuffers.get(pipeKey) ?? Buffer.alloc(0);
|
|
475
|
+
let buf = Buffer.concat([existing, chunk]);
|
|
476
|
+
winPipeBuffers.set(pipeKey, buf);
|
|
477
|
+
while (buf.length >= 5) {
|
|
478
|
+
const msgType = buf.readUInt8(0);
|
|
479
|
+
const length = buf.readUInt32BE(1);
|
|
480
|
+
if (buf.length < 5 + length)
|
|
481
|
+
break;
|
|
482
|
+
const payload = buf.subarray(5, 5 + length);
|
|
483
|
+
buf = buf.subarray(5 + length);
|
|
484
|
+
winPipeBuffers.set(pipeKey, buf);
|
|
485
|
+
if (msgType === 0x01 && ws.readyState === WS_OPEN) {
|
|
486
|
+
ws.send(JSON.stringify({
|
|
487
|
+
ch: "terminal",
|
|
488
|
+
id,
|
|
489
|
+
type: "data",
|
|
490
|
+
data: payload.toString("utf-8"),
|
|
491
|
+
...echo,
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
if (msgType === 0x07) {
|
|
495
|
+
try {
|
|
496
|
+
const status = JSON.parse(payload.toString("utf-8"));
|
|
497
|
+
if (!status.alive && ws.readyState === WS_OPEN) {
|
|
498
|
+
ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
/* ignore parse errors */
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
pipeSocket.on("close", () => {
|
|
508
|
+
winPipes.delete(pipeKey);
|
|
509
|
+
winPipeBuffers.delete(pipeKey);
|
|
510
|
+
if (ws.readyState === WS_OPEN) {
|
|
511
|
+
ws.send(JSON.stringify({ ch: "terminal", id, type: "exited", code: 0, ...echo }));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (type === "data" && msg.data !== undefined) {
|
|
518
|
+
const pipeSocket = winPipes.get(pipeKey);
|
|
519
|
+
if (pipeSocket) {
|
|
520
|
+
const inputBuf = Buffer.from(msg.data, "utf-8");
|
|
521
|
+
const header = Buffer.alloc(5);
|
|
522
|
+
header.writeUInt8(0x02, 0);
|
|
523
|
+
header.writeUInt32BE(inputBuf.length, 1);
|
|
524
|
+
pipeSocket.write(Buffer.concat([header, inputBuf]));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (type === "resize" && msg.cols !== undefined && msg.rows !== undefined) {
|
|
528
|
+
const pipeSocket = winPipes.get(pipeKey);
|
|
529
|
+
if (pipeSocket) {
|
|
530
|
+
const resizePayload = Buffer.from(JSON.stringify({ cols: msg.cols, rows: msg.rows }));
|
|
531
|
+
const header = Buffer.alloc(5);
|
|
532
|
+
header.writeUInt8(0x03, 0);
|
|
533
|
+
header.writeUInt32BE(resizePayload.length, 1);
|
|
534
|
+
pipeSocket.write(Buffer.concat([header, resizePayload]));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else if (type === "close") {
|
|
538
|
+
const pipeSocket = winPipes.get(pipeKey);
|
|
539
|
+
if (pipeSocket) {
|
|
540
|
+
pipeSocket.end();
|
|
541
|
+
winPipes.delete(pipeKey);
|
|
542
|
+
winPipeBuffers.delete(pipeKey);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
338
546
|
/**
|
|
339
547
|
* Create a mux WebSocket server (noServer mode).
|
|
340
548
|
* Returns the WebSocketServer instance for manual upgrade routing.
|
|
341
549
|
*/
|
|
342
550
|
export function createMuxWebSocket(tmuxPath) {
|
|
343
|
-
|
|
551
|
+
// On Windows, we use named pipe relay instead of node-pty/tmux.
|
|
552
|
+
// Allow the server to be created without ptySpawn on Windows.
|
|
553
|
+
if (!ptySpawn && !isWindows()) {
|
|
344
554
|
console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled");
|
|
345
555
|
return null;
|
|
346
556
|
}
|
|
347
|
-
|
|
557
|
+
// On Windows, terminal I/O goes through named pipe relay — no TerminalManager needed.
|
|
558
|
+
const terminalManager = ptySpawn && !isWindows() ? new TerminalManager(tmuxPath ?? undefined) : null;
|
|
348
559
|
const nextPort = process.env.PORT || "3000";
|
|
349
560
|
const broadcaster = new SessionBroadcaster(nextPort);
|
|
350
561
|
const wss = new WebSocketServer({ noServer: true });
|
|
351
562
|
wss.on("connection", (ws) => {
|
|
352
563
|
console.log("[MuxServer] New mux connection");
|
|
353
564
|
const subscriptions = new Map();
|
|
565
|
+
// Windows: named pipe sockets keyed by session ID
|
|
566
|
+
const winPipes = new Map();
|
|
567
|
+
// Windows: framing buffers keyed by session ID
|
|
568
|
+
const winPipeBuffers = new Map();
|
|
354
569
|
let sessionUnsubscribe = null;
|
|
355
570
|
let missedPongs = 0;
|
|
356
571
|
const MAX_MISSED_PONGS = 3;
|
|
@@ -390,71 +605,93 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
390
605
|
const subscriptionKey = projectId ? `${projectId}:${id}` : id;
|
|
391
606
|
try {
|
|
392
607
|
if (type === "open") {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
data,
|
|
425
|
-
...(projectId && { projectId }),
|
|
426
|
-
};
|
|
427
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
428
|
-
ws.send(JSON.stringify(dataMsg));
|
|
429
|
-
}
|
|
430
|
-
}, (exitCode) => {
|
|
431
|
-
const exitedMsg = {
|
|
432
|
-
ch: "terminal",
|
|
433
|
-
id,
|
|
434
|
-
type: "exited",
|
|
435
|
-
code: exitCode,
|
|
436
|
-
...(projectId && { projectId }),
|
|
437
|
-
};
|
|
438
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
439
|
-
ws.send(JSON.stringify(exitedMsg));
|
|
608
|
+
if (isWindows()) {
|
|
609
|
+
handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// --- Unix: tmux path with project scoping ---
|
|
613
|
+
if (!terminalManager)
|
|
614
|
+
throw new Error("Terminal manager not available");
|
|
615
|
+
terminalManager.open(id, projectId, "tmuxName" in msg ? msg.tmuxName : undefined);
|
|
616
|
+
// Send opened confirmation (idempotent — safe to send on re-open)
|
|
617
|
+
const openedMsg = {
|
|
618
|
+
ch: "terminal",
|
|
619
|
+
id,
|
|
620
|
+
type: "opened",
|
|
621
|
+
...(projectId && { projectId }),
|
|
622
|
+
};
|
|
623
|
+
ws.send(JSON.stringify(openedMsg));
|
|
624
|
+
// Subscribe and send history buffer only for new subscribers.
|
|
625
|
+
// Skipping the buffer on re-open prevents duplicate output when
|
|
626
|
+
// MuxProvider re-sends open for all terminals on reconnect.
|
|
627
|
+
if (!subscriptions.has(subscriptionKey)) {
|
|
628
|
+
// Send buffered history to catch up the new subscriber
|
|
629
|
+
const buffer = terminalManager.getBuffer(id, projectId);
|
|
630
|
+
if (buffer) {
|
|
631
|
+
const bufferMsg = {
|
|
632
|
+
ch: "terminal",
|
|
633
|
+
id,
|
|
634
|
+
type: "data",
|
|
635
|
+
data: buffer,
|
|
636
|
+
...(projectId && { projectId }),
|
|
637
|
+
};
|
|
638
|
+
ws.send(JSON.stringify(bufferMsg));
|
|
440
639
|
}
|
|
441
|
-
|
|
442
|
-
|
|
640
|
+
const unsub = terminalManager.subscribe(id, projectId, (data) => {
|
|
641
|
+
const dataMsg = {
|
|
642
|
+
ch: "terminal",
|
|
643
|
+
id,
|
|
644
|
+
type: "data",
|
|
645
|
+
data,
|
|
646
|
+
...(projectId && { projectId }),
|
|
647
|
+
};
|
|
648
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
649
|
+
ws.send(JSON.stringify(dataMsg));
|
|
650
|
+
}
|
|
651
|
+
}, (exitCode) => {
|
|
652
|
+
const exitedMsg = {
|
|
653
|
+
ch: "terminal",
|
|
654
|
+
id,
|
|
655
|
+
type: "exited",
|
|
656
|
+
code: exitCode,
|
|
657
|
+
...(projectId && { projectId }),
|
|
658
|
+
};
|
|
659
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
660
|
+
ws.send(JSON.stringify(exitedMsg));
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
subscriptions.set(subscriptionKey, unsub);
|
|
664
|
+
}
|
|
443
665
|
}
|
|
444
666
|
}
|
|
445
667
|
else if (type === "data" && "data" in msg) {
|
|
446
|
-
|
|
668
|
+
if (isWindows()) {
|
|
669
|
+
handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
terminalManager?.write(id, msg.data, projectId);
|
|
673
|
+
}
|
|
447
674
|
}
|
|
448
675
|
else if (type === "resize" && "cols" in msg && "rows" in msg) {
|
|
449
|
-
|
|
676
|
+
if (isWindows()) {
|
|
677
|
+
handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
terminalManager?.resize(id, msg.cols, msg.rows, projectId);
|
|
681
|
+
}
|
|
450
682
|
}
|
|
451
683
|
else if (type === "close") {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
684
|
+
if (isWindows()) {
|
|
685
|
+
handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
// Unsubscribe this client only — TerminalManager is shared across
|
|
689
|
+
// all mux connections so we must not kill the PTY here.
|
|
690
|
+
const unsub = subscriptions.get(subscriptionKey);
|
|
691
|
+
if (unsub) {
|
|
692
|
+
unsub();
|
|
693
|
+
subscriptions.delete(subscriptionKey);
|
|
694
|
+
}
|
|
458
695
|
}
|
|
459
696
|
}
|
|
460
697
|
}
|
|
@@ -515,6 +752,12 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
515
752
|
unsub();
|
|
516
753
|
}
|
|
517
754
|
subscriptions.clear();
|
|
755
|
+
// Windows: close all open pipe sockets
|
|
756
|
+
for (const pipeSocket of winPipes.values()) {
|
|
757
|
+
pipeSocket.destroy();
|
|
758
|
+
}
|
|
759
|
+
winPipes.clear();
|
|
760
|
+
winPipeBuffers.clear();
|
|
518
761
|
});
|
|
519
762
|
// In the ws library, "error" is always followed by "close", so the close
|
|
520
763
|
// handler below handles all cleanup. Log the error here and nothing more.
|
package/dist-server/start-all.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolve, dirname } from "node:path";
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
|
+
import { killProcessTree } from "@aoagents/ao-core";
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = dirname(__filename);
|
|
13
14
|
// Resolve paths relative to the package root (one level up from dist-server/)
|
|
@@ -25,6 +26,7 @@ function spawnProcess(label, command, args, opts) {
|
|
|
25
26
|
cwd: pkgRoot,
|
|
26
27
|
stdio: ["ignore", "pipe", "pipe"],
|
|
27
28
|
env: process.env,
|
|
29
|
+
detached: process.platform !== "win32",
|
|
28
30
|
});
|
|
29
31
|
child.stdout?.on("data", (data) => {
|
|
30
32
|
for (const line of data.toString().split("\n").filter(Boolean)) {
|
|
@@ -60,10 +62,14 @@ function spawnProcess(label, command, args, opts) {
|
|
|
60
62
|
* Tries the local .bin shim first (fast), then falls back to require.resolve (hoisted deps).
|
|
61
63
|
*/
|
|
62
64
|
function resolveNextBin() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// On Windows, .bin/next is a POSIX shell shim that spawn() cannot execute.
|
|
66
|
+
// Skip it and go straight to the JS entry point.
|
|
67
|
+
if (process.platform !== "win32") {
|
|
68
|
+
const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
|
|
69
|
+
if (existsSync(localBin))
|
|
70
|
+
return localBin;
|
|
71
|
+
}
|
|
72
|
+
// Resolve the actual Next.js CLI JS entry point
|
|
67
73
|
const require = createRequire(resolve(pkgRoot, "package.json"));
|
|
68
74
|
try {
|
|
69
75
|
const nextPkg = require.resolve("next/package.json");
|
|
@@ -76,7 +82,15 @@ function resolveNextBin() {
|
|
|
76
82
|
}
|
|
77
83
|
// Start Next.js production server
|
|
78
84
|
const port = process.env["PORT"] || "3000";
|
|
79
|
-
|
|
85
|
+
const nextBin = resolveNextBin();
|
|
86
|
+
if (process.platform === "win32" && nextBin !== "next") {
|
|
87
|
+
// On Windows, run the JS entry point via the current node binary.
|
|
88
|
+
// spawn() can't execute .js files directly on Windows.
|
|
89
|
+
spawnProcess("next", process.execPath, [nextBin, "start", "-p", port]);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
spawnProcess("next", nextBin, ["start", "-p", port]);
|
|
93
|
+
}
|
|
80
94
|
// Start direct terminal WebSocket server (auto-restart on crash)
|
|
81
95
|
spawnProcess("direct-terminal", "node", [resolve(__dirname, "direct-terminal-ws.js")], { restart: true });
|
|
82
96
|
// Graceful shutdown — send SIGTERM to children and wait for them to exit
|
|
@@ -104,7 +118,15 @@ function cleanup() {
|
|
|
104
118
|
process.exit(0);
|
|
105
119
|
}
|
|
106
120
|
});
|
|
107
|
-
child.
|
|
121
|
+
const pid = child.pid;
|
|
122
|
+
if (pid) {
|
|
123
|
+
void killProcessTree(pid, "SIGTERM").catch(() => {
|
|
124
|
+
child.kill("SIGTERM");
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
child.kill("SIGTERM");
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
}
|
|
110
132
|
process.on("SIGINT", cleanup);
|