@aoagents/ao-web 0.6.0 → 0.8.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 +211 -197
- package/.next/app-path-routes-manifest.json +18 -16
- package/.next/build-manifest.json +6 -6
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +22 -22
- 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 +18 -16
- package/.next/server/chunks/1271.js +1 -1
- package/.next/server/chunks/1876.js +2 -2
- 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/chunks/9291.js +3 -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/9ARsRA8WlFgsAl-7NPrHh/_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-a6f86436c4a06eeb.js +1 -0
- package/.next/static/chunks/app/{error-684a1c5596fa1e14.js → error-d4f7c3a8dd722e8f.js} +1 -1
- package/.next/static/chunks/app/{global-error-1a79bacfbd9b1ba4.js → global-error-cb3a4148c223622a.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-e4c02836878bea2d.js +1 -0
- 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-982c28ba7f6492be.js +1 -0
- package/.next/static/chunks/app/orchestrators/{page-e3a2c53b57dd8391.js → page-11a10c8b0bea320d.js} +1 -1
- package/.next/static/chunks/app/page-31df8f5f0d079eba.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/loading-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/{page-19a6d4cc9951a9e4.js → page-f076731a5810b255.js} +1 -1
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-7e46ee5c902c28aa.js +1 -0
- package/.next/static/chunks/app/projects/[projectId]/settings/page-40047db90c406c9e.js +1 -0
- package/.next/static/chunks/app/prs/page-5015d39fb7984d26.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/{error-62e9972b39d9cd16.js → error-147fb962bda03d23.js} +1 -1
- package/.next/static/chunks/app/sessions/[id]/loading-8b5044bdc951ae98.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/not-found-982c28ba7f6492be.js +1 -0
- package/.next/static/chunks/app/sessions/[id]/page-0a74680de719ca58.js +1 -0
- package/.next/static/chunks/app/test-direct/{page-ee0944bcd355194e.js → page-cbca94d9a5985aea.js} +1 -1
- package/.next/static/chunks/main-app-113c48dd1c080c52.js +1 -0
- package/.next/static/chunks/{webpack-83d2d8248a30259c.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 +270 -70
- 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/6811.js +0 -3
- package/.next/server/chunks/801.js +0 -658
- package/.next/server/chunks/9223.js +0 -440
- package/.next/static/chunks/1383.8f5f7d4606d356cc.js +0 -1
- package/.next/static/chunks/3764.88619fb0d047cae8.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-59decd1aad4e02ad.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/layout-0cda720d75f111b8.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/not-found-315f4e5c106b425d.js +0 -1
- package/.next/static/chunks/app/page-e2e589ea11a0780a.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/loading-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/sessions/[id]/page-75c536c9755754f7.js +0 -1
- package/.next/static/chunks/app/projects/[projectId]/settings/page-f8c323b91978efff.js +0 -1
- package/.next/static/chunks/app/prs/page-08f17d7dc341b6f1.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/loading-3b8a01e726e988c8.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/not-found-315f4e5c106b425d.js +0 -1
- package/.next/static/chunks/app/sessions/[id]/page-d0b08722dec5a04a.js +0 -1
- package/.next/static/chunks/main-app-b95f197c38e3b0a3.js +0 -1
- package/.next/static/css/cf9226160e230bf4.css +0 -1
- package/.next/static/h0RXs0prE87a8wlbEXQKM/_buildManifest.js +0 -1
- /package/.next/static/{h0RXs0prE87a8wlbEXQKM → 9ARsRA8WlFgsAl-7NPrHh}/_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.
|
|
@@ -161,7 +162,11 @@ export class TerminalManager {
|
|
|
161
162
|
terminals = new Map();
|
|
162
163
|
TMUX;
|
|
163
164
|
constructor(tmuxPath) {
|
|
164
|
-
|
|
165
|
+
const resolved = tmuxPath ?? findTmux();
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
throw new Error("tmux not available on this platform");
|
|
168
|
+
}
|
|
169
|
+
this.TMUX = resolved;
|
|
165
170
|
}
|
|
166
171
|
terminalKey(id, projectId) {
|
|
167
172
|
return projectId ? `${projectId}:${id}` : id;
|
|
@@ -215,16 +220,16 @@ export class TerminalManager {
|
|
|
215
220
|
console.error(`[MuxServer] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
|
|
216
221
|
});
|
|
217
222
|
// Build environment
|
|
218
|
-
const
|
|
219
|
-
const
|
|
223
|
+
const platformDefaults = getEnvDefaults();
|
|
224
|
+
const homeDir = platformDefaults.HOME;
|
|
220
225
|
const env = {
|
|
221
|
-
HOME:
|
|
222
|
-
SHELL:
|
|
223
|
-
USER:
|
|
224
|
-
PATH: process.env.PATH ||
|
|
226
|
+
HOME: platformDefaults.HOME,
|
|
227
|
+
SHELL: platformDefaults.SHELL,
|
|
228
|
+
USER: platformDefaults.USER,
|
|
229
|
+
PATH: process.env.PATH || platformDefaults.PATH,
|
|
225
230
|
TERM: "xterm-256color",
|
|
226
231
|
LANG: process.env.LANG || "en_US.UTF-8",
|
|
227
|
-
TMPDIR:
|
|
232
|
+
TMPDIR: platformDefaults.TMPDIR,
|
|
228
233
|
};
|
|
229
234
|
if (!ptySpawn) {
|
|
230
235
|
throw new Error("node-pty not available");
|
|
@@ -280,9 +285,35 @@ export class TerminalManager {
|
|
|
280
285
|
}
|
|
281
286
|
});
|
|
282
287
|
// Handle PTY exit
|
|
283
|
-
|
|
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 }) => {
|
|
284
295
|
console.log(`[MuxServer] PTY exited for ${id} with code ${exitCode}`);
|
|
285
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
|
+
}
|
|
286
317
|
// Re-attach if subscribers are still present, up to MAX_REATTACH_ATTEMPTS.
|
|
287
318
|
// The cap prevents an unbounded respawn loop when the PTY crashes immediately
|
|
288
319
|
// after every attach (e.g. resource exhaustion or a broken tmux session).
|
|
@@ -378,22 +409,163 @@ export class TerminalManager {
|
|
|
378
409
|
return terminal.buffer.join("");
|
|
379
410
|
}
|
|
380
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
|
+
}
|
|
381
546
|
/**
|
|
382
547
|
* Create a mux WebSocket server (noServer mode).
|
|
383
548
|
* Returns the WebSocketServer instance for manual upgrade routing.
|
|
384
549
|
*/
|
|
385
550
|
export function createMuxWebSocket(tmuxPath) {
|
|
386
|
-
|
|
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()) {
|
|
387
554
|
console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled");
|
|
388
555
|
return null;
|
|
389
556
|
}
|
|
390
|
-
|
|
557
|
+
// On Windows, terminal I/O goes through named pipe relay — no TerminalManager needed.
|
|
558
|
+
const terminalManager = ptySpawn && !isWindows() ? new TerminalManager(tmuxPath ?? undefined) : null;
|
|
391
559
|
const nextPort = process.env.PORT || "3000";
|
|
392
560
|
const broadcaster = new SessionBroadcaster(nextPort);
|
|
393
561
|
const wss = new WebSocketServer({ noServer: true });
|
|
394
562
|
wss.on("connection", (ws) => {
|
|
395
563
|
console.log("[MuxServer] New mux connection");
|
|
396
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();
|
|
397
569
|
let sessionUnsubscribe = null;
|
|
398
570
|
let missedPongs = 0;
|
|
399
571
|
const MAX_MISSED_PONGS = 3;
|
|
@@ -433,71 +605,93 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
433
605
|
const subscriptionKey = projectId ? `${projectId}:${id}` : id;
|
|
434
606
|
try {
|
|
435
607
|
if (type === "open") {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
data,
|
|
468
|
-
...(projectId && { projectId }),
|
|
469
|
-
};
|
|
470
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
471
|
-
ws.send(JSON.stringify(dataMsg));
|
|
472
|
-
}
|
|
473
|
-
}, (exitCode) => {
|
|
474
|
-
const exitedMsg = {
|
|
475
|
-
ch: "terminal",
|
|
476
|
-
id,
|
|
477
|
-
type: "exited",
|
|
478
|
-
code: exitCode,
|
|
479
|
-
...(projectId && { projectId }),
|
|
480
|
-
};
|
|
481
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
482
|
-
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));
|
|
483
639
|
}
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
}
|
|
486
665
|
}
|
|
487
666
|
}
|
|
488
667
|
else if (type === "data" && "data" in msg) {
|
|
489
|
-
|
|
668
|
+
if (isWindows()) {
|
|
669
|
+
handleWindowsPipeMessage(msg, ws, winPipes, winPipeBuffers, { connect: netConnect, resolvePipePath });
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
terminalManager?.write(id, msg.data, projectId);
|
|
673
|
+
}
|
|
490
674
|
}
|
|
491
675
|
else if (type === "resize" && "cols" in msg && "rows" in msg) {
|
|
492
|
-
|
|
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
|
+
}
|
|
493
682
|
}
|
|
494
683
|
else if (type === "close") {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
501
695
|
}
|
|
502
696
|
}
|
|
503
697
|
}
|
|
@@ -558,6 +752,12 @@ export function createMuxWebSocket(tmuxPath) {
|
|
|
558
752
|
unsub();
|
|
559
753
|
}
|
|
560
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();
|
|
561
761
|
});
|
|
562
762
|
// In the ws library, "error" is always followed by "close", so the close
|
|
563
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);
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Extracted from direct-terminal-ws.ts and terminal-websocket.ts
|
|
5
5
|
* so the logic can be properly unit tested.
|
|
6
6
|
*/
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { readdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
8
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { isWindows } from "@aoagents/ao-core";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
11
14
|
/** Session ID validation regex — alphanumeric, hyphens, underscores only */
|
|
12
15
|
export const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
13
16
|
/** Hash prefix pattern — 12-char lowercase hex, as generated by generateConfigHash */
|
|
@@ -90,6 +93,8 @@ export function validateSessionId(sessionId) {
|
|
|
90
93
|
* @param execFn - Injectable execFileSync for testing. Defaults to child_process.execFileSync.
|
|
91
94
|
*/
|
|
92
95
|
export function findTmux(execFn = execFileSync) {
|
|
96
|
+
if (isWindows())
|
|
97
|
+
return null;
|
|
93
98
|
const candidates = [
|
|
94
99
|
"/opt/homebrew/bin/tmux", // macOS ARM (Homebrew)
|
|
95
100
|
"/usr/local/bin/tmux", // macOS Intel (Homebrew)
|
|
@@ -106,6 +111,35 @@ export function findTmux(execFn = execFileSync) {
|
|
|
106
111
|
}
|
|
107
112
|
return "tmux"; // Fall back to bare name
|
|
108
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Check whether a tmux session with the given name exists.
|
|
116
|
+
*
|
|
117
|
+
* Uses `=` exact-match prefix so the lookup never falls back to tmux's
|
|
118
|
+
* default prefix matching (where "ao-1" would match "ao-15"). The caller
|
|
119
|
+
* must already have the canonical tmux session name (typically the value
|
|
120
|
+
* returned by `resolveTmuxSession`).
|
|
121
|
+
*
|
|
122
|
+
* Async: this runs from inside node-pty's `onExit` callback on every agent
|
|
123
|
+
* exit, and the WebSocket server is single-threaded. A synchronous
|
|
124
|
+
* `execFileSync` here would block the event loop — and every WebSocket
|
|
125
|
+
* connection, HTTP request, and in-flight terminal — for up to the 5 s
|
|
126
|
+
* subprocess timeout when tmux is slow to respond. Use the promisified
|
|
127
|
+
* `execFile` form instead.
|
|
128
|
+
*
|
|
129
|
+
* @returns true if the session exists, false otherwise (including tmux
|
|
130
|
+
* not running, no sessions, or any unexpected error)
|
|
131
|
+
*/
|
|
132
|
+
export async function tmuxHasSession(tmuxPath, tmuxSessionName, execFn = execFileAsync) {
|
|
133
|
+
if (!tmuxPath)
|
|
134
|
+
return false;
|
|
135
|
+
try {
|
|
136
|
+
await execFn(tmuxPath, ["has-session", "-t", `=${tmuxSessionName}`], { timeout: 5000 });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
109
143
|
/**
|
|
110
144
|
* Resolve a user-facing session ID to its actual tmux session name.
|
|
111
145
|
*
|
|
@@ -132,6 +166,8 @@ export function findTmux(execFn = execFileSync) {
|
|
|
132
166
|
* @returns The actual tmux session name, or null if not found
|
|
133
167
|
*/
|
|
134
168
|
export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync, fs = defaultFs, projectId) {
|
|
169
|
+
if (!tmuxPath)
|
|
170
|
+
return null;
|
|
135
171
|
// Try exact match first using = prefix for exact matching (e.g., "ao-orchestrator")
|
|
136
172
|
// Without =, tmux uses prefix matching: "ao-1" would match "ao-15"
|
|
137
173
|
try {
|
|
@@ -177,3 +213,89 @@ export function resolveTmuxSession(sessionId, tmuxPath, execFn = execFileSync, f
|
|
|
177
213
|
}
|
|
178
214
|
return null;
|
|
179
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve a user-facing session ID to its Windows named pipe path.
|
|
218
|
+
*
|
|
219
|
+
* V2 layout (current): JSON metadata at
|
|
220
|
+
* `~/.agent-orchestrator/projects/{projectId}/sessions/{sessionId}.json`
|
|
221
|
+
* with `runtimeHandle.data.pipePath` as a top-level field.
|
|
222
|
+
*
|
|
223
|
+
* V1 layout (legacy fallback): line-delimited key=value at
|
|
224
|
+
* `~/.agent-orchestrator/{storageKey}/sessions/{sessionId}` where
|
|
225
|
+
* storageKey is bare 12-hex or `{hash}-{projectName}`. Kept so users
|
|
226
|
+
* who haven't run `ao migrate-storage` still see live sessions.
|
|
227
|
+
*
|
|
228
|
+
* When `projectId` is provided, only that project's metadata file is read.
|
|
229
|
+
* Without it (legacy callers), walks all projects and returns the first
|
|
230
|
+
* matching pipePath — which can collide when two projects share a sessionId.
|
|
231
|
+
*
|
|
232
|
+
* @returns Full pipe path (e.g., "\\\\.\\pipe\\ao-pty-win1-orchestrator"), or null
|
|
233
|
+
*/
|
|
234
|
+
export function resolvePipePath(sessionId, projectId, fs = defaultFs) {
|
|
235
|
+
if (!isWindows())
|
|
236
|
+
return null;
|
|
237
|
+
const readFile = fs.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
238
|
+
const aoBase = join(fs.homedir(), ".agent-orchestrator");
|
|
239
|
+
const readPipeFromV2 = (project) => {
|
|
240
|
+
const sessionFile = join(aoBase, "projects", project, "sessions", `${sessionId}.json`);
|
|
241
|
+
if (!fs.exists(sessionFile))
|
|
242
|
+
return null;
|
|
243
|
+
try {
|
|
244
|
+
const meta = JSON.parse(readFile(sessionFile));
|
|
245
|
+
const pipePath = meta.runtimeHandle?.data?.pipePath;
|
|
246
|
+
return typeof pipePath === "string" && pipePath.length > 0 ? pipePath : null;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
// V2: prefer the caller's projectId when provided; otherwise walk all projects
|
|
253
|
+
const projectsDir = join(aoBase, "projects");
|
|
254
|
+
if (projectId) {
|
|
255
|
+
const pipe = readPipeFromV2(projectId);
|
|
256
|
+
if (pipe)
|
|
257
|
+
return pipe;
|
|
258
|
+
}
|
|
259
|
+
else if (fs.exists(projectsDir)) {
|
|
260
|
+
let projects;
|
|
261
|
+
try {
|
|
262
|
+
projects = fs.readdir(projectsDir);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
projects = [];
|
|
266
|
+
}
|
|
267
|
+
for (const project of projects) {
|
|
268
|
+
const pipe = readPipeFromV2(project);
|
|
269
|
+
if (pipe)
|
|
270
|
+
return pipe;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// V1 fallback: line-delimited key=value under {storageKey}/sessions/{sessionId}
|
|
274
|
+
for (const storageKey of findStorageKeysForSession(sessionId, {
|
|
275
|
+
readdir: fs.readdir,
|
|
276
|
+
exists: fs.exists,
|
|
277
|
+
homedir: fs.homedir,
|
|
278
|
+
})) {
|
|
279
|
+
const sessionFile = join(aoBase, storageKey, "sessions", sessionId);
|
|
280
|
+
let content;
|
|
281
|
+
try {
|
|
282
|
+
content = readFile(sessionFile);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const match = content.match(/^runtimeHandle=(.+)$/m);
|
|
288
|
+
if (!match)
|
|
289
|
+
continue;
|
|
290
|
+
try {
|
|
291
|
+
const handle = JSON.parse(match[1]);
|
|
292
|
+
const pipePath = handle.data?.pipePath;
|
|
293
|
+
if (pipePath && pipePath.length > 0)
|
|
294
|
+
return pipePath;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|