parasor 0.1.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/LICENSE +21 -0
- package/README.md +208 -0
- package/THIRD-PARTY-NOTICES.md +138 -0
- package/bin/parasor.mjs +3 -0
- package/node_modules/@parasor/shared/dist/client.d.ts +8 -0
- package/node_modules/@parasor/shared/dist/client.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/client.js +2 -0
- package/node_modules/@parasor/shared/dist/client.js.map +1 -0
- package/node_modules/@parasor/shared/dist/drops.d.ts +24 -0
- package/node_modules/@parasor/shared/dist/drops.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/drops.js +2 -0
- package/node_modules/@parasor/shared/dist/drops.js.map +1 -0
- package/node_modules/@parasor/shared/dist/file-uploads.d.ts +56 -0
- package/node_modules/@parasor/shared/dist/file-uploads.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/file-uploads.js +13 -0
- package/node_modules/@parasor/shared/dist/file-uploads.js.map +1 -0
- package/node_modules/@parasor/shared/dist/ide-commands.d.ts +8 -0
- package/node_modules/@parasor/shared/dist/ide-commands.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/ide-commands.js +59 -0
- package/node_modules/@parasor/shared/dist/ide-commands.js.map +1 -0
- package/node_modules/@parasor/shared/dist/pane-commands.d.ts +7 -0
- package/node_modules/@parasor/shared/dist/pane-commands.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/pane-commands.js +47 -0
- package/node_modules/@parasor/shared/dist/pane-commands.js.map +1 -0
- package/node_modules/@parasor/shared/dist/pane-model.d.ts +63 -0
- package/node_modules/@parasor/shared/dist/pane-model.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/pane-model.js +89 -0
- package/node_modules/@parasor/shared/dist/pane-model.js.map +1 -0
- package/node_modules/@parasor/shared/dist/panes.d.ts +33 -0
- package/node_modules/@parasor/shared/dist/panes.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/panes.js +2 -0
- package/node_modules/@parasor/shared/dist/panes.js.map +1 -0
- package/node_modules/@parasor/shared/dist/runtime.d.ts +180 -0
- package/node_modules/@parasor/shared/dist/runtime.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/runtime.js +2 -0
- package/node_modules/@parasor/shared/dist/runtime.js.map +1 -0
- package/node_modules/@parasor/shared/dist/state.d.ts +192 -0
- package/node_modules/@parasor/shared/dist/state.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/state.js +10 -0
- package/node_modules/@parasor/shared/dist/state.js.map +1 -0
- package/node_modules/@parasor/shared/dist/terminal.d.ts +191 -0
- package/node_modules/@parasor/shared/dist/terminal.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/terminal.js +174 -0
- package/node_modules/@parasor/shared/dist/terminal.js.map +1 -0
- package/node_modules/@parasor/shared/dist/types.d.ts +13 -0
- package/node_modules/@parasor/shared/dist/types.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/types.js +13 -0
- package/node_modules/@parasor/shared/dist/types.js.map +1 -0
- package/node_modules/@parasor/shared/dist/worktree-local-files.d.ts +14 -0
- package/node_modules/@parasor/shared/dist/worktree-local-files.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/worktree-local-files.js +41 -0
- package/node_modules/@parasor/shared/dist/worktree-local-files.js.map +1 -0
- package/node_modules/@parasor/shared/dist/ws-events.d.ts +152 -0
- package/node_modules/@parasor/shared/dist/ws-events.d.ts.map +1 -0
- package/node_modules/@parasor/shared/dist/ws-events.js +2 -0
- package/node_modules/@parasor/shared/dist/ws-events.js.map +1 -0
- package/node_modules/@parasor/shared/package.json +13 -0
- package/package.json +61 -0
- package/server/agent-detector/agent-state-store.d.ts +23 -0
- package/server/agent-detector/agent-state-store.d.ts.map +1 -0
- package/server/agent-detector/agent-state-store.js +110 -0
- package/server/agent-detector/agent-state-store.js.map +1 -0
- package/server/agent-detector/detector.d.ts +80 -0
- package/server/agent-detector/detector.d.ts.map +1 -0
- package/server/agent-detector/detector.js +219 -0
- package/server/agent-detector/detector.js.map +1 -0
- package/server/agent-detector/event-map.d.ts +25 -0
- package/server/agent-detector/event-map.d.ts.map +1 -0
- package/server/agent-detector/event-map.js +136 -0
- package/server/agent-detector/event-map.js.map +1 -0
- package/server/agent-detector/manual-agent-tracker.d.ts +19 -0
- package/server/agent-detector/manual-agent-tracker.d.ts.map +1 -0
- package/server/agent-detector/manual-agent-tracker.js +123 -0
- package/server/agent-detector/manual-agent-tracker.js.map +1 -0
- package/server/agent-detector/output-eligibility.d.ts +3 -0
- package/server/agent-detector/output-eligibility.d.ts.map +1 -0
- package/server/agent-detector/output-eligibility.js +39 -0
- package/server/agent-detector/output-eligibility.js.map +1 -0
- package/server/application/files/errors.d.ts +28 -0
- package/server/application/files/errors.d.ts.map +1 -0
- package/server/application/files/errors.js +55 -0
- package/server/application/files/errors.js.map +1 -0
- package/server/application/files/local-filesystem.d.ts +36 -0
- package/server/application/files/local-filesystem.d.ts.map +1 -0
- package/server/application/files/local-filesystem.js +171 -0
- package/server/application/files/local-filesystem.js.map +1 -0
- package/server/application/files/project-file-queries.d.ts +16 -0
- package/server/application/files/project-file-queries.d.ts.map +1 -0
- package/server/application/files/project-file-queries.js +126 -0
- package/server/application/files/project-file-queries.js.map +1 -0
- package/server/application/integrations/errors.d.ts +16 -0
- package/server/application/integrations/errors.d.ts.map +1 -0
- package/server/application/integrations/errors.js +31 -0
- package/server/application/integrations/errors.js.map +1 -0
- package/server/application/integrations/hook-notify.d.ts +32 -0
- package/server/application/integrations/hook-notify.d.ts.map +1 -0
- package/server/application/integrations/hook-notify.js +118 -0
- package/server/application/integrations/hook-notify.js.map +1 -0
- package/server/application/integrations/open-url.d.ts +11 -0
- package/server/application/integrations/open-url.d.ts.map +1 -0
- package/server/application/integrations/open-url.js +23 -0
- package/server/application/integrations/open-url.js.map +1 -0
- package/server/application/ports.d.ts +11 -0
- package/server/application/ports.d.ts.map +1 -0
- package/server/application/ports.js +2 -0
- package/server/application/ports.js.map +1 -0
- package/server/application/workspace/errors.d.ts +21 -0
- package/server/application/workspace/errors.d.ts.map +1 -0
- package/server/application/workspace/errors.js +21 -0
- package/server/application/workspace/errors.js.map +1 -0
- package/server/application/workspace/pane-commands.d.ts +47 -0
- package/server/application/workspace/pane-commands.d.ts.map +1 -0
- package/server/application/workspace/pane-commands.js +193 -0
- package/server/application/workspace/pane-commands.js.map +1 -0
- package/server/application/workspace/project-commands.d.ts +34 -0
- package/server/application/workspace/project-commands.d.ts.map +1 -0
- package/server/application/workspace/project-commands.js +81 -0
- package/server/application/workspace/project-commands.js.map +1 -0
- package/server/application/workspace/project-queries.d.ts +21 -0
- package/server/application/workspace/project-queries.d.ts.map +1 -0
- package/server/application/workspace/project-queries.js +216 -0
- package/server/application/workspace/project-queries.js.map +1 -0
- package/server/application/workspace/session-commands.d.ts +24 -0
- package/server/application/workspace/session-commands.d.ts.map +1 -0
- package/server/application/workspace/session-commands.js +123 -0
- package/server/application/workspace/session-commands.js.map +1 -0
- package/server/application/workspace/session-queries.d.ts +14 -0
- package/server/application/workspace/session-queries.d.ts.map +1 -0
- package/server/application/workspace/session-queries.js +47 -0
- package/server/application/workspace/session-queries.js.map +1 -0
- package/server/application/workspace/worktree-commands.d.ts +98 -0
- package/server/application/workspace/worktree-commands.d.ts.map +1 -0
- package/server/application/workspace/worktree-commands.js +323 -0
- package/server/application/workspace/worktree-commands.js.map +1 -0
- package/server/application/workspace/worktree-local-files.d.ts +11 -0
- package/server/application/workspace/worktree-local-files.d.ts.map +1 -0
- package/server/application/workspace/worktree-local-files.js +223 -0
- package/server/application/workspace/worktree-local-files.js.map +1 -0
- package/server/application/workspace/worktree-reconcile.d.ts +21 -0
- package/server/application/workspace/worktree-reconcile.d.ts.map +1 -0
- package/server/application/workspace/worktree-reconcile.js +49 -0
- package/server/application/workspace/worktree-reconcile.js.map +1 -0
- package/server/auth/origin.d.ts +17 -0
- package/server/auth/origin.d.ts.map +1 -0
- package/server/auth/origin.js +93 -0
- package/server/auth/origin.js.map +1 -0
- package/server/auth/pairing-token.d.ts +28 -0
- package/server/auth/pairing-token.d.ts.map +1 -0
- package/server/auth/pairing-token.js +46 -0
- package/server/auth/pairing-token.js.map +1 -0
- package/server/auth/token-exchange.d.ts +11 -0
- package/server/auth/token-exchange.d.ts.map +1 -0
- package/server/auth/token-exchange.js +27 -0
- package/server/auth/token-exchange.js.map +1 -0
- package/server/auth/token.d.ts +18 -0
- package/server/auth/token.d.ts.map +1 -0
- package/server/auth/token.js +115 -0
- package/server/auth/token.js.map +1 -0
- package/server/bootstrap/create-app-server.d.ts +49 -0
- package/server/bootstrap/create-app-server.d.ts.map +1 -0
- package/server/bootstrap/create-app-server.js +154 -0
- package/server/bootstrap/create-app-server.js.map +1 -0
- package/server/bootstrap/project-runtime.d.ts +29 -0
- package/server/bootstrap/project-runtime.d.ts.map +1 -0
- package/server/bootstrap/project-runtime.js +253 -0
- package/server/bootstrap/project-runtime.js.map +1 -0
- package/server/bootstrap/pty-env.d.ts +3 -0
- package/server/bootstrap/pty-env.d.ts.map +1 -0
- package/server/bootstrap/pty-env.js +15 -0
- package/server/bootstrap/pty-env.js.map +1 -0
- package/server/bootstrap/reconcile-state.d.ts +16 -0
- package/server/bootstrap/reconcile-state.d.ts.map +1 -0
- package/server/bootstrap/reconcile-state.js +73 -0
- package/server/bootstrap/reconcile-state.js.map +1 -0
- package/server/bootstrap/runtime-loops.d.ts +51 -0
- package/server/bootstrap/runtime-loops.d.ts.map +1 -0
- package/server/bootstrap/runtime-loops.js +189 -0
- package/server/bootstrap/runtime-loops.js.map +1 -0
- package/server/bootstrap/runtime-port.d.ts +4 -0
- package/server/bootstrap/runtime-port.d.ts.map +1 -0
- package/server/bootstrap/runtime-port.js +52 -0
- package/server/bootstrap/runtime-port.js.map +1 -0
- package/server/bootstrap/safety-gate.d.ts +22 -0
- package/server/bootstrap/safety-gate.d.ts.map +1 -0
- package/server/bootstrap/safety-gate.js +43 -0
- package/server/bootstrap/safety-gate.js.map +1 -0
- package/server/bootstrap/shutdown-marker.d.ts +35 -0
- package/server/bootstrap/shutdown-marker.d.ts.map +1 -0
- package/server/bootstrap/shutdown-marker.js +100 -0
- package/server/bootstrap/shutdown-marker.js.map +1 -0
- package/server/bootstrap/shutdown-runtime.d.ts +62 -0
- package/server/bootstrap/shutdown-runtime.d.ts.map +1 -0
- package/server/bootstrap/shutdown-runtime.js +66 -0
- package/server/bootstrap/shutdown-runtime.js.map +1 -0
- package/server/bootstrap/startup-banner.d.ts +34 -0
- package/server/bootstrap/startup-banner.d.ts.map +1 -0
- package/server/bootstrap/startup-banner.js +114 -0
- package/server/bootstrap/startup-banner.js.map +1 -0
- package/server/bootstrap/wire-runtime.d.ts +42 -0
- package/server/bootstrap/wire-runtime.d.ts.map +1 -0
- package/server/bootstrap/wire-runtime.js +205 -0
- package/server/bootstrap/wire-runtime.js.map +1 -0
- package/server/cli/help.d.ts +3 -0
- package/server/cli/help.d.ts.map +1 -0
- package/server/cli/help.js +39 -0
- package/server/cli/help.js.map +1 -0
- package/server/cli/hook-client.d.ts +12 -0
- package/server/cli/hook-client.d.ts.map +1 -0
- package/server/cli/hook-client.js +81 -0
- package/server/cli/hook-client.js.map +1 -0
- package/server/cli/hook.d.ts +3 -0
- package/server/cli/hook.d.ts.map +1 -0
- package/server/cli/hook.js +152 -0
- package/server/cli/hook.js.map +1 -0
- package/server/cli/main.d.ts +2 -0
- package/server/cli/main.d.ts.map +1 -0
- package/server/cli/main.js +133 -0
- package/server/cli/main.js.map +1 -0
- package/server/cli/notify.d.ts +19 -0
- package/server/cli/notify.d.ts.map +1 -0
- package/server/cli/notify.js +77 -0
- package/server/cli/notify.js.map +1 -0
- package/server/cli/open.d.ts +2 -0
- package/server/cli/open.d.ts.map +1 -0
- package/server/cli/open.js +47 -0
- package/server/cli/open.js.map +1 -0
- package/server/cli/probe-daemon-version.d.ts +46 -0
- package/server/cli/probe-daemon-version.d.ts.map +1 -0
- package/server/cli/probe-daemon-version.js +167 -0
- package/server/cli/probe-daemon-version.js.map +1 -0
- package/server/cli/pty-host.d.ts +36 -0
- package/server/cli/pty-host.d.ts.map +1 -0
- package/server/cli/pty-host.js +390 -0
- package/server/cli/pty-host.js.map +1 -0
- package/server/cli/qr.d.ts +2 -0
- package/server/cli/qr.d.ts.map +1 -0
- package/server/cli/qr.js +70 -0
- package/server/cli/qr.js.map +1 -0
- package/server/cli/restart-confirm.d.ts +33 -0
- package/server/cli/restart-confirm.d.ts.map +1 -0
- package/server/cli/restart-confirm.js +107 -0
- package/server/cli/restart-confirm.js.map +1 -0
- package/server/cli/restart.d.ts +17 -0
- package/server/cli/restart.d.ts.map +1 -0
- package/server/cli/restart.js +145 -0
- package/server/cli/restart.js.map +1 -0
- package/server/cli/service-darwin.d.ts +70 -0
- package/server/cli/service-darwin.d.ts.map +1 -0
- package/server/cli/service-darwin.js +562 -0
- package/server/cli/service-darwin.js.map +1 -0
- package/server/cli/service-linux.d.ts +81 -0
- package/server/cli/service-linux.d.ts.map +1 -0
- package/server/cli/service-linux.js +412 -0
- package/server/cli/service-linux.js.map +1 -0
- package/server/cli/service.d.ts +33 -0
- package/server/cli/service.d.ts.map +1 -0
- package/server/cli/service.js +133 -0
- package/server/cli/service.js.map +1 -0
- package/server/cli/shim-installer.d.ts +35 -0
- package/server/cli/shim-installer.d.ts.map +1 -0
- package/server/cli/shim-installer.js +662 -0
- package/server/cli/shim-installer.js.map +1 -0
- package/server/cli/shim-open.d.ts +2 -0
- package/server/cli/shim-open.d.ts.map +1 -0
- package/server/cli/shim-open.js +128 -0
- package/server/cli/shim-open.js.map +1 -0
- package/server/cli/shutdown-deps.d.ts +41 -0
- package/server/cli/shutdown-deps.d.ts.map +1 -0
- package/server/cli/shutdown-deps.js +158 -0
- package/server/cli/shutdown-deps.js.map +1 -0
- package/server/cli/stop.d.ts +11 -0
- package/server/cli/stop.d.ts.map +1 -0
- package/server/cli/stop.js +123 -0
- package/server/cli/stop.js.map +1 -0
- package/server/cli/unknown-command.d.ts +8 -0
- package/server/cli/unknown-command.d.ts.map +1 -0
- package/server/cli/unknown-command.js +14 -0
- package/server/cli/unknown-command.js.map +1 -0
- package/server/debug/agent-status-recorder.d.ts +57 -0
- package/server/debug/agent-status-recorder.d.ts.map +1 -0
- package/server/debug/agent-status-recorder.js +209 -0
- package/server/debug/agent-status-recorder.js.map +1 -0
- package/server/debug/terminal-trace-recorder.d.ts +41 -0
- package/server/debug/terminal-trace-recorder.d.ts.map +1 -0
- package/server/debug/terminal-trace-recorder.js +112 -0
- package/server/debug/terminal-trace-recorder.js.map +1 -0
- package/server/fonts/catalog.d.ts +35 -0
- package/server/fonts/catalog.d.ts.map +1 -0
- package/server/fonts/catalog.js +76 -0
- package/server/fonts/catalog.js.map +1 -0
- package/server/fonts/installer.d.ts +45 -0
- package/server/fonts/installer.d.ts.map +1 -0
- package/server/fonts/installer.js +201 -0
- package/server/fonts/installer.js.map +1 -0
- package/server/fonts/routes.d.ts +4 -0
- package/server/fonts/routes.d.ts.map +1 -0
- package/server/fonts/routes.js +91 -0
- package/server/fonts/routes.js.map +1 -0
- package/server/fs/drops.d.ts +50 -0
- package/server/fs/drops.d.ts.map +1 -0
- package/server/fs/drops.js +137 -0
- package/server/fs/drops.js.map +1 -0
- package/server/fs/file-uploads.d.ts +57 -0
- package/server/fs/file-uploads.d.ts.map +1 -0
- package/server/fs/file-uploads.js +214 -0
- package/server/fs/file-uploads.js.map +1 -0
- package/server/fs/file-watcher.d.ts +22 -0
- package/server/fs/file-watcher.d.ts.map +1 -0
- package/server/fs/file-watcher.js +105 -0
- package/server/fs/file-watcher.js.map +1 -0
- package/server/fs/git-watcher.d.ts +76 -0
- package/server/fs/git-watcher.d.ts.map +1 -0
- package/server/fs/git-watcher.js +356 -0
- package/server/fs/git-watcher.js.map +1 -0
- package/server/fs/media.d.ts +35 -0
- package/server/fs/media.d.ts.map +1 -0
- package/server/fs/media.js +271 -0
- package/server/fs/media.js.map +1 -0
- package/server/fs/service.d.ts +97 -0
- package/server/fs/service.d.ts.map +1 -0
- package/server/fs/service.js +306 -0
- package/server/fs/service.js.map +1 -0
- package/server/fs/upload-staging.d.ts +76 -0
- package/server/fs/upload-staging.d.ts.map +1 -0
- package/server/fs/upload-staging.js +283 -0
- package/server/fs/upload-staging.js.map +1 -0
- package/server/fs/watcher-lifecycle.d.ts +24 -0
- package/server/fs/watcher-lifecycle.d.ts.map +1 -0
- package/server/fs/watcher-lifecycle.js +105 -0
- package/server/fs/watcher-lifecycle.js.map +1 -0
- package/server/index.d.ts +17 -0
- package/server/index.d.ts.map +1 -0
- package/server/index.js +510 -0
- package/server/index.js.map +1 -0
- package/server/ipc/socket-server.d.ts +28 -0
- package/server/ipc/socket-server.d.ts.map +1 -0
- package/server/ipc/socket-server.js +206 -0
- package/server/ipc/socket-server.js.map +1 -0
- package/server/lib/git-exec.d.ts +38 -0
- package/server/lib/git-exec.d.ts.map +1 -0
- package/server/lib/git-exec.js +96 -0
- package/server/lib/git-exec.js.map +1 -0
- package/server/lib/open-in-ide.d.ts +22 -0
- package/server/lib/open-in-ide.d.ts.map +1 -0
- package/server/lib/open-in-ide.js +90 -0
- package/server/lib/open-in-ide.js.map +1 -0
- package/server/lib/open-in-os.d.ts +20 -0
- package/server/lib/open-in-os.d.ts.map +1 -0
- package/server/lib/open-in-os.js +51 -0
- package/server/lib/open-in-os.js.map +1 -0
- package/server/lib/path.d.ts +2 -0
- package/server/lib/path.d.ts.map +1 -0
- package/server/lib/path.js +10 -0
- package/server/lib/path.js.map +1 -0
- package/server/lib/promise-mutex.d.ts +7 -0
- package/server/lib/promise-mutex.d.ts.map +1 -0
- package/server/lib/promise-mutex.js +26 -0
- package/server/lib/promise-mutex.js.map +1 -0
- package/server/lib/sd-notify.d.ts +8 -0
- package/server/lib/sd-notify.d.ts.map +1 -0
- package/server/lib/sd-notify.js +27 -0
- package/server/lib/sd-notify.js.map +1 -0
- package/server/net/local-machine.d.ts +7 -0
- package/server/net/local-machine.d.ts.map +1 -0
- package/server/net/local-machine.js +79 -0
- package/server/net/local-machine.js.map +1 -0
- package/server/net/reachable-host.d.ts +12 -0
- package/server/net/reachable-host.d.ts.map +1 -0
- package/server/net/reachable-host.js +25 -0
- package/server/net/reachable-host.js.map +1 -0
- package/server/network/endpoints.d.ts +14 -0
- package/server/network/endpoints.d.ts.map +1 -0
- package/server/network/endpoints.js +104 -0
- package/server/network/endpoints.js.map +1 -0
- package/server/network/qr.d.ts +16 -0
- package/server/network/qr.d.ts.map +1 -0
- package/server/network/qr.js +89 -0
- package/server/network/qr.js.map +1 -0
- package/server/port-forwarder/forwarder.d.ts +47 -0
- package/server/port-forwarder/forwarder.d.ts.map +1 -0
- package/server/port-forwarder/forwarder.js +159 -0
- package/server/port-forwarder/forwarder.js.map +1 -0
- package/server/port-scanner/scanner.d.ts +24 -0
- package/server/port-scanner/scanner.d.ts.map +1 -0
- package/server/port-scanner/scanner.js +193 -0
- package/server/port-scanner/scanner.js.map +1 -0
- package/server/pty/connection-lifecycle.d.ts +50 -0
- package/server/pty/connection-lifecycle.d.ts.map +1 -0
- package/server/pty/connection-lifecycle.js +113 -0
- package/server/pty/connection-lifecycle.js.map +1 -0
- package/server/pty/daemon-connect.d.ts +52 -0
- package/server/pty/daemon-connect.d.ts.map +1 -0
- package/server/pty/daemon-connect.js +62 -0
- package/server/pty/daemon-connect.js.map +1 -0
- package/server/pty/handshake-timeout-race.d.ts +22 -0
- package/server/pty/handshake-timeout-race.d.ts.map +1 -0
- package/server/pty/handshake-timeout-race.js +23 -0
- package/server/pty/handshake-timeout-race.js.map +1 -0
- package/server/pty/headless-replay-snapshot.d.ts +28 -0
- package/server/pty/headless-replay-snapshot.d.ts.map +1 -0
- package/server/pty/headless-replay-snapshot.js +285 -0
- package/server/pty/headless-replay-snapshot.js.map +1 -0
- package/server/pty/headless-terminal-state-cache.d.ts +31 -0
- package/server/pty/headless-terminal-state-cache.d.ts.map +1 -0
- package/server/pty/headless-terminal-state-cache.js +119 -0
- package/server/pty/headless-terminal-state-cache.js.map +1 -0
- package/server/pty/hello-ack-validator.d.ts +15 -0
- package/server/pty/hello-ack-validator.d.ts.map +1 -0
- package/server/pty/hello-ack-validator.js +30 -0
- package/server/pty/hello-ack-validator.js.map +1 -0
- package/server/pty/host-daemon/bootstrap.d.ts +100 -0
- package/server/pty/host-daemon/bootstrap.d.ts.map +1 -0
- package/server/pty/host-daemon/bootstrap.js +611 -0
- package/server/pty/host-daemon/bootstrap.js.map +1 -0
- package/server/pty/host-daemon/daemon.d.ts +111 -0
- package/server/pty/host-daemon/daemon.d.ts.map +1 -0
- package/server/pty/host-daemon/daemon.js +648 -0
- package/server/pty/host-daemon/daemon.js.map +1 -0
- package/server/pty/host-daemon/entry.d.ts +3 -0
- package/server/pty/host-daemon/entry.d.ts.map +1 -0
- package/server/pty/host-daemon/entry.js +72 -0
- package/server/pty/host-daemon/entry.js.map +1 -0
- package/server/pty/host-daemon/lockfile.d.ts +12 -0
- package/server/pty/host-daemon/lockfile.d.ts.map +1 -0
- package/server/pty/host-daemon/lockfile.js +120 -0
- package/server/pty/host-daemon/lockfile.js.map +1 -0
- package/server/pty/host-daemon/mode-marker.d.ts +77 -0
- package/server/pty/host-daemon/mode-marker.d.ts.map +1 -0
- package/server/pty/host-daemon/mode-marker.js +228 -0
- package/server/pty/host-daemon/mode-marker.js.map +1 -0
- package/server/pty/host-daemon/orphan-cleanup.d.ts +42 -0
- package/server/pty/host-daemon/orphan-cleanup.d.ts.map +1 -0
- package/server/pty/host-daemon/orphan-cleanup.js +84 -0
- package/server/pty/host-daemon/orphan-cleanup.js.map +1 -0
- package/server/pty/host-daemon/paths.d.ts +10 -0
- package/server/pty/host-daemon/paths.d.ts.map +1 -0
- package/server/pty/host-daemon/paths.js +56 -0
- package/server/pty/host-daemon/paths.js.map +1 -0
- package/server/pty/host-daemon/server-connection.d.ts +44 -0
- package/server/pty/host-daemon/server-connection.d.ts.map +1 -0
- package/server/pty/host-daemon/server-connection.js +100 -0
- package/server/pty/host-daemon/server-connection.js.map +1 -0
- package/server/pty/host-daemon/service-detection.d.ts +24 -0
- package/server/pty/host-daemon/service-detection.d.ts.map +1 -0
- package/server/pty/host-daemon/service-detection.js +16 -0
- package/server/pty/host-daemon/service-detection.js.map +1 -0
- package/server/pty/host-daemon/socket-ready.d.ts +22 -0
- package/server/pty/host-daemon/socket-ready.d.ts.map +1 -0
- package/server/pty/host-daemon/socket-ready.js +33 -0
- package/server/pty/host-daemon/socket-ready.js.map +1 -0
- package/server/pty/host-daemon/spawn-daemon.d.ts +45 -0
- package/server/pty/host-daemon/spawn-daemon.d.ts.map +1 -0
- package/server/pty/host-daemon/spawn-daemon.js +155 -0
- package/server/pty/host-daemon/spawn-daemon.js.map +1 -0
- package/server/pty/host-daemon/terminate-daemon.d.ts +16 -0
- package/server/pty/host-daemon/terminate-daemon.d.ts.map +1 -0
- package/server/pty/host-daemon/terminate-daemon.js +128 -0
- package/server/pty/host-daemon/terminate-daemon.js.map +1 -0
- package/server/pty/host-protocol/frames.d.ts +110 -0
- package/server/pty/host-protocol/frames.d.ts.map +1 -0
- package/server/pty/host-protocol/frames.js +255 -0
- package/server/pty/host-protocol/frames.js.map +1 -0
- package/server/pty/host-protocol/messages.d.ts +140 -0
- package/server/pty/host-protocol/messages.d.ts.map +1 -0
- package/server/pty/host-protocol/messages.js +117 -0
- package/server/pty/host-protocol/messages.js.map +1 -0
- package/server/pty/host.d.ts +273 -0
- package/server/pty/host.d.ts.map +1 -0
- package/server/pty/host.js +184 -0
- package/server/pty/host.js.map +1 -0
- package/server/pty/in-process-host.d.ts +224 -0
- package/server/pty/in-process-host.d.ts.map +1 -0
- package/server/pty/in-process-host.js +1183 -0
- package/server/pty/in-process-host.js.map +1 -0
- package/server/pty/osc7-lifecycle.d.ts +8 -0
- package/server/pty/osc7-lifecycle.d.ts.map +1 -0
- package/server/pty/osc7-lifecycle.js +22 -0
- package/server/pty/osc7-lifecycle.js.map +1 -0
- package/server/pty/osc7-parser.d.ts +23 -0
- package/server/pty/osc7-parser.d.ts.map +1 -0
- package/server/pty/osc7-parser.js +91 -0
- package/server/pty/osc7-parser.js.map +1 -0
- package/server/pty/remote-host.d.ts +188 -0
- package/server/pty/remote-host.d.ts.map +1 -0
- package/server/pty/remote-host.js +810 -0
- package/server/pty/remote-host.js.map +1 -0
- package/server/pty/request-correlator.d.ts +59 -0
- package/server/pty/request-correlator.d.ts.map +1 -0
- package/server/pty/request-correlator.js +75 -0
- package/server/pty/request-correlator.js.map +1 -0
- package/server/pty/scrollback-log.d.ts +130 -0
- package/server/pty/scrollback-log.d.ts.map +1 -0
- package/server/pty/scrollback-log.js +344 -0
- package/server/pty/scrollback-log.js.map +1 -0
- package/server/pty/scrollback-sanitize.d.ts +2 -0
- package/server/pty/scrollback-sanitize.d.ts.map +1 -0
- package/server/pty/scrollback-sanitize.js +54 -0
- package/server/pty/scrollback-sanitize.js.map +1 -0
- package/server/pty/session-mirror.d.ts +67 -0
- package/server/pty/session-mirror.d.ts.map +1 -0
- package/server/pty/session-mirror.js +112 -0
- package/server/pty/session-mirror.js.map +1 -0
- package/server/pty/session-policy.d.ts +85 -0
- package/server/pty/session-policy.d.ts.map +1 -0
- package/server/pty/session-policy.js +186 -0
- package/server/pty/session-policy.js.map +1 -0
- package/server/pty/version-mismatch-recovery.d.ts +71 -0
- package/server/pty/version-mismatch-recovery.d.ts.map +1 -0
- package/server/pty/version-mismatch-recovery.js +63 -0
- package/server/pty/version-mismatch-recovery.js.map +1 -0
- package/server/routes/debug-agent-status.d.ts +5 -0
- package/server/routes/debug-agent-status.d.ts.map +1 -0
- package/server/routes/debug-agent-status.js +22 -0
- package/server/routes/debug-agent-status.js.map +1 -0
- package/server/routes/debug-diagnostics.d.ts +8 -0
- package/server/routes/debug-diagnostics.d.ts.map +1 -0
- package/server/routes/debug-diagnostics.js +34 -0
- package/server/routes/debug-diagnostics.js.map +1 -0
- package/server/routes/debug-terminal-trace.d.ts +16 -0
- package/server/routes/debug-terminal-trace.d.ts.map +1 -0
- package/server/routes/debug-terminal-trace.js +545 -0
- package/server/routes/debug-terminal-trace.js.map +1 -0
- package/server/routes/drops.d.ts +13 -0
- package/server/routes/drops.d.ts.map +1 -0
- package/server/routes/drops.js +126 -0
- package/server/routes/drops.js.map +1 -0
- package/server/routes/file-uploads.d.ts +9 -0
- package/server/routes/file-uploads.d.ts.map +1 -0
- package/server/routes/file-uploads.js +119 -0
- package/server/routes/file-uploads.js.map +1 -0
- package/server/routes/files.d.ts +9 -0
- package/server/routes/files.d.ts.map +1 -0
- package/server/routes/files.js +441 -0
- package/server/routes/files.js.map +1 -0
- package/server/routes/filesystem.d.ts +6 -0
- package/server/routes/filesystem.d.ts.map +1 -0
- package/server/routes/filesystem.js +76 -0
- package/server/routes/filesystem.js.map +1 -0
- package/server/routes/git.d.ts +38 -0
- package/server/routes/git.d.ts.map +1 -0
- package/server/routes/git.js +585 -0
- package/server/routes/git.js.map +1 -0
- package/server/routes/healthz.d.ts +8 -0
- package/server/routes/healthz.d.ts.map +1 -0
- package/server/routes/healthz.js +24 -0
- package/server/routes/healthz.js.map +1 -0
- package/server/routes/hook.d.ts +14 -0
- package/server/routes/hook.d.ts.map +1 -0
- package/server/routes/hook.js +86 -0
- package/server/routes/hook.js.map +1 -0
- package/server/routes/ide-commands.d.ts +9 -0
- package/server/routes/ide-commands.d.ts.map +1 -0
- package/server/routes/ide-commands.js +23 -0
- package/server/routes/ide-commands.js.map +1 -0
- package/server/routes/lib/resolve-worktree.d.ts +30 -0
- package/server/routes/lib/resolve-worktree.d.ts.map +1 -0
- package/server/routes/lib/resolve-worktree.js +35 -0
- package/server/routes/lib/resolve-worktree.js.map +1 -0
- package/server/routes/open.d.ts +4 -0
- package/server/routes/open.d.ts.map +1 -0
- package/server/routes/open.js +21 -0
- package/server/routes/open.js.map +1 -0
- package/server/routes/pane-commands.d.ts +9 -0
- package/server/routes/pane-commands.d.ts.map +1 -0
- package/server/routes/pane-commands.js +23 -0
- package/server/routes/pane-commands.js.map +1 -0
- package/server/routes/projects.d.ts +15 -0
- package/server/routes/projects.d.ts.map +1 -0
- package/server/routes/projects.js +363 -0
- package/server/routes/projects.js.map +1 -0
- package/server/routes/server-notices.d.ts +4 -0
- package/server/routes/server-notices.d.ts.map +1 -0
- package/server/routes/server-notices.js +29 -0
- package/server/routes/server-notices.js.map +1 -0
- package/server/routes/service-config.d.ts +11 -0
- package/server/routes/service-config.d.ts.map +1 -0
- package/server/routes/service-config.js +64 -0
- package/server/routes/service-config.js.map +1 -0
- package/server/routes/sessions.d.ts +7 -0
- package/server/routes/sessions.d.ts.map +1 -0
- package/server/routes/sessions.js +218 -0
- package/server/routes/sessions.js.map +1 -0
- package/server/service/caffeinate.d.ts +33 -0
- package/server/service/caffeinate.d.ts.map +1 -0
- package/server/service/caffeinate.js +72 -0
- package/server/service/caffeinate.js.map +1 -0
- package/server/state/app-state.d.ts +167 -0
- package/server/state/app-state.d.ts.map +1 -0
- package/server/state/app-state.js +335 -0
- package/server/state/app-state.js.map +1 -0
- package/server/state/project-manager.d.ts +30 -0
- package/server/state/project-manager.d.ts.map +1 -0
- package/server/state/project-manager.js +128 -0
- package/server/state/project-manager.js.map +1 -0
- package/server/state/server-notices.d.ts +12 -0
- package/server/state/server-notices.d.ts.map +1 -0
- package/server/state/server-notices.js +32 -0
- package/server/state/server-notices.js.map +1 -0
- package/server/state/worktree-cache.d.ts +25 -0
- package/server/state/worktree-cache.d.ts.map +1 -0
- package/server/state/worktree-cache.js +53 -0
- package/server/state/worktree-cache.js.map +1 -0
- package/server/ws/events.d.ts +45 -0
- package/server/ws/events.d.ts.map +1 -0
- package/server/ws/events.js +134 -0
- package/server/ws/events.js.map +1 -0
- package/server/ws/keepalive.d.ts +27 -0
- package/server/ws/keepalive.d.ts.map +1 -0
- package/server/ws/keepalive.js +63 -0
- package/server/ws/keepalive.js.map +1 -0
- package/server/ws/terminal-attach.d.ts +13 -0
- package/server/ws/terminal-attach.d.ts.map +1 -0
- package/server/ws/terminal-attach.js +233 -0
- package/server/ws/terminal-attach.js.map +1 -0
- package/server/ws/terminal-flow.d.ts +17 -0
- package/server/ws/terminal-flow.d.ts.map +1 -0
- package/server/ws/terminal-flow.js +64 -0
- package/server/ws/terminal-flow.js.map +1 -0
- package/server/ws/terminal.d.ts +69 -0
- package/server/ws/terminal.d.ts.map +1 -0
- package/server/ws/terminal.js +311 -0
- package/server/ws/terminal.js.map +1 -0
- package/web/assets/EditorPane-CzzT3iYY.js +123 -0
- package/web/assets/SymbolsNerdFontMono-Regular-CwEZqMeU.woff2 +0 -0
- package/web/assets/TerminalPane-CjbYzePr.js +68 -0
- package/web/assets/TerminalPane-DkTCHfhq.css +1 -0
- package/web/assets/file-icons-JBi09j0r.js +6 -0
- package/web/assets/index-CTTkRpnn.css +2 -0
- package/web/assets/index-CmhewzMp.js +34 -0
- package/web/assets/session-resume-7f-tB-ZU.js +2 -0
- package/web/assets/terminal-trace-PuuFRybC.js +2803 -0
- package/web/assets/useVirtualKeyboard-DgJb9u9d.js +1 -0
- package/web/index.html +52 -0
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import * as pty from "node-pty";
|
|
5
|
+
import { PromiseMutex } from "../lib/promise-mutex.js";
|
|
6
|
+
import { HeadlessTerminalStateCache } from "./headless-terminal-state-cache.js";
|
|
7
|
+
import { stripQueryEscapes } from "./scrollback-sanitize.js";
|
|
8
|
+
import * as sessionPolicy from "./session-policy.js";
|
|
9
|
+
const DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES = 256 * 1024;
|
|
10
|
+
const DEFAULT_HEADLESS_REPLAY_SCROLLBACK_LINES = 10_000;
|
|
11
|
+
const DEFAULT_HEADLESS_REPLAY_MAX_BYTES = DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES;
|
|
12
|
+
const DEFAULT_HEADLESS_STATE_MAX_SESSIONS = 8;
|
|
13
|
+
const DEFAULT_HEADLESS_STATE_TTL_MS = 10 * 60_000;
|
|
14
|
+
function readPositiveIntegerEnv(name) {
|
|
15
|
+
const raw = process.env[name];
|
|
16
|
+
if (!raw)
|
|
17
|
+
return null;
|
|
18
|
+
const value = Number(raw);
|
|
19
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
20
|
+
return null;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function readBooleanEnv(name) {
|
|
24
|
+
const raw = process.env[name];
|
|
25
|
+
if (raw === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
const value = raw.trim().toLowerCase();
|
|
28
|
+
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
|
29
|
+
return true;
|
|
30
|
+
if (value === "0" || value === "false" || value === "no" || value === "off")
|
|
31
|
+
return false;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function readHeadlessReplayEnabled() {
|
|
35
|
+
return (readBooleanEnv("PARASOR_HEADLESS_REPLAY") ??
|
|
36
|
+
readBooleanEnv("PARASOR_EXPERIMENT_HEADLESS_REPLAY") ??
|
|
37
|
+
true);
|
|
38
|
+
}
|
|
39
|
+
function utf8Tail(text, maxBytes) {
|
|
40
|
+
const buf = Buffer.from(text, "utf8");
|
|
41
|
+
if (buf.length <= maxBytes)
|
|
42
|
+
return text;
|
|
43
|
+
const slice = buf.subarray(buf.length - maxBytes);
|
|
44
|
+
let start = 0;
|
|
45
|
+
while (start < slice.length && start < 3 && (slice[start] & 0xc0) === 0x80) {
|
|
46
|
+
start++;
|
|
47
|
+
}
|
|
48
|
+
return slice.subarray(start).toString("utf8");
|
|
49
|
+
}
|
|
50
|
+
export class InProcessPtyHost {
|
|
51
|
+
store;
|
|
52
|
+
scrollbackLog;
|
|
53
|
+
daemonContext;
|
|
54
|
+
uploadsDir;
|
|
55
|
+
sessions = new Map();
|
|
56
|
+
globalDataListeners = [];
|
|
57
|
+
inputListeners = [];
|
|
58
|
+
ptyEnv = {};
|
|
59
|
+
inProcessLegacyReplayMaxBytes = readPositiveIntegerEnv("PARASOR_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES") ??
|
|
60
|
+
DEFAULT_IN_PROCESS_LEGACY_REPLAY_MAX_BYTES;
|
|
61
|
+
headlessReplayEnabled = readHeadlessReplayEnabled();
|
|
62
|
+
headlessReplayScrollbackLines = readPositiveIntegerEnv("PARASOR_HEADLESS_REPLAY_SCROLLBACK_LINES") ??
|
|
63
|
+
DEFAULT_HEADLESS_REPLAY_SCROLLBACK_LINES;
|
|
64
|
+
headlessReplayMaxBytes = readPositiveIntegerEnv("PARASOR_HEADLESS_REPLAY_MAX_BYTES") ??
|
|
65
|
+
DEFAULT_HEADLESS_REPLAY_MAX_BYTES;
|
|
66
|
+
headlessStateCache = this.headlessReplayEnabled
|
|
67
|
+
? new HeadlessTerminalStateCache({
|
|
68
|
+
cols: 80,
|
|
69
|
+
rows: 24,
|
|
70
|
+
scrollbackLines: this.headlessReplayScrollbackLines,
|
|
71
|
+
maxBytes: this.headlessReplayMaxBytes,
|
|
72
|
+
maxSessions: readPositiveIntegerEnv("PARASOR_HEADLESS_STATE_MAX_SESSIONS") ??
|
|
73
|
+
DEFAULT_HEADLESS_STATE_MAX_SESSIONS,
|
|
74
|
+
ttlMs: readPositiveIntegerEnv("PARASOR_HEADLESS_STATE_TTL_MS") ??
|
|
75
|
+
DEFAULT_HEADLESS_STATE_TTL_MS,
|
|
76
|
+
})
|
|
77
|
+
: null;
|
|
78
|
+
/**
|
|
79
|
+
* Attach fencing monotonic counter -- incremented per attach (initClient or
|
|
80
|
+
* attachClient), stamped on the resulting `AttachedClient` entry, and
|
|
81
|
+
* compared on `detachClient(..., expectedToken)`. Plain integer is fine
|
|
82
|
+
* because tokens are scoped to this host instance and the comparison
|
|
83
|
+
* is identity, not magnitude.
|
|
84
|
+
*/
|
|
85
|
+
nextAttachToken = 1;
|
|
86
|
+
mintAttachToken(caller) {
|
|
87
|
+
if (caller !== undefined)
|
|
88
|
+
return caller;
|
|
89
|
+
return this.nextAttachToken++;
|
|
90
|
+
}
|
|
91
|
+
onSessionExit = null;
|
|
92
|
+
constructor(store,
|
|
93
|
+
/**
|
|
94
|
+
* Disk-backed scrollback log. When null (daemon-side host, contract
|
|
95
|
+
* tests), this host records no scrollback and every replay path
|
|
96
|
+
* returns empty. That is intentional: the daemon forwards OUTPUT to
|
|
97
|
+
* the client over IPC and the client-side `RemotePtyHost` owns its
|
|
98
|
+
* own `ScrollbackLog` -- keeping a redundant copy on the daemon side
|
|
99
|
+
* would just double the disk footprint. `getScrollback()` and the
|
|
100
|
+
* `initClient` legacy replay degrade to "no replay" in that mode.
|
|
101
|
+
*/
|
|
102
|
+
scrollbackLog = null, daemonContext = null,
|
|
103
|
+
/**
|
|
104
|
+
* Canonical absolute path of `<rootDir>/uploads`. When supplied, every
|
|
105
|
+
* spawned PTY's env gets `PARASOR_UPLOAD_DIR=<uploadsDir>/<sessionId>`
|
|
106
|
+
* -- the per-session subdir, NOT the shared root -- so the Claude shim's
|
|
107
|
+
* `--add-dir` allowlists only that PTY's own drops (upload staging isolation codex
|
|
108
|
+
* Shared-root isolation review: the shared-root variant let session A read session
|
|
109
|
+
* B's uploads via `--add-dir`). Null in tests that don't exercise the
|
|
110
|
+
* upload pipeline.
|
|
111
|
+
*/
|
|
112
|
+
uploadsDir = null) {
|
|
113
|
+
this.store = store;
|
|
114
|
+
this.scrollbackLog = scrollbackLog;
|
|
115
|
+
this.daemonContext = daemonContext;
|
|
116
|
+
this.uploadsDir = uploadsDir;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Merge env vars into the per-PTY environment. Multiple calls accumulate
|
|
120
|
+
* (later calls override individual keys but preserve everything else),
|
|
121
|
+
* so the server can set the static shim PATH early during init and add
|
|
122
|
+
* the dynamic PARASOR_PORT later once probePort has resolved the actual
|
|
123
|
+
* listening port.
|
|
124
|
+
*/
|
|
125
|
+
setPtyEnv(env) {
|
|
126
|
+
this.ptyEnv = { ...this.ptyEnv, ...env };
|
|
127
|
+
}
|
|
128
|
+
buildSessionEnv(sessionId, projectId) {
|
|
129
|
+
const env = {};
|
|
130
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
131
|
+
if (v !== undefined)
|
|
132
|
+
env[k] = v;
|
|
133
|
+
}
|
|
134
|
+
Object.assign(env, this.ptyEnv, {
|
|
135
|
+
PROMPT_EOL_MARK: "",
|
|
136
|
+
TERM: "xterm-256color",
|
|
137
|
+
COLORTERM: "truecolor",
|
|
138
|
+
TERM_PROGRAM: "parasor",
|
|
139
|
+
PARASOR_SESSION_ID: sessionId,
|
|
140
|
+
PARASOR_PROJECT_ID: projectId,
|
|
141
|
+
});
|
|
142
|
+
if (this.uploadsDir) {
|
|
143
|
+
// Per-session subdir, eagerly created so the Claude wrapper's
|
|
144
|
+
// `--add-dir` flag references a real path even when the user never
|
|
145
|
+
// drops a file. The drops route's `acquire(sessionId)` is
|
|
146
|
+
// idempotent -- a later upload re-mkdirs the same path.
|
|
147
|
+
const sessionUploadDir = join(this.uploadsDir, sessionId);
|
|
148
|
+
mkdirSync(sessionUploadDir, { recursive: true, mode: 0o700 });
|
|
149
|
+
env.PARASOR_UPLOAD_DIR = sessionUploadDir;
|
|
150
|
+
}
|
|
151
|
+
return env;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create a session stub. The PTY is NOT spawned here -- it is deferred
|
|
155
|
+
* until the first WebSocket attach provides real viewport dimensions.
|
|
156
|
+
* This is the whole point of the deferred-spawn model: spawning at the
|
|
157
|
+
* exact client dims eliminates the SIGWINCH-driven prompt redraw that
|
|
158
|
+
* otherwise walks zsh's prompt down one row on every new-session open.
|
|
159
|
+
*/
|
|
160
|
+
async create(input) {
|
|
161
|
+
const id = randomUUID();
|
|
162
|
+
const mutex = new PromiseMutex();
|
|
163
|
+
const release = await mutex.acquire();
|
|
164
|
+
try {
|
|
165
|
+
const { spawnCmd, spawnArgs } = this.resolveCommand(input.command);
|
|
166
|
+
const info = {
|
|
167
|
+
id,
|
|
168
|
+
projectId: input.projectId,
|
|
169
|
+
pid: null,
|
|
170
|
+
state: "spawning",
|
|
171
|
+
generation: 1,
|
|
172
|
+
title: input.title ?? basename(spawnCmd),
|
|
173
|
+
...(input.title !== undefined && { titleManual: true }),
|
|
174
|
+
command: input.command,
|
|
175
|
+
cwd: input.cwd,
|
|
176
|
+
shell: spawnCmd,
|
|
177
|
+
createdAt: Date.now(),
|
|
178
|
+
};
|
|
179
|
+
// Build the SessionRecord stub. PID/PGID stay null until
|
|
180
|
+
// spawnProcess fills them on first WS attach. State is "running"
|
|
181
|
+
// from creation: from the daemon's perspective this slot is
|
|
182
|
+
// claimed and counts toward orphan reconciliation. If the daemon
|
|
183
|
+
// crashes between create() and spawnProcess, the next daemon's
|
|
184
|
+
// reconcile sees state=running + pid=null and transitions it to
|
|
185
|
+
// "lost" (orphan-cleanup.ts: no-pid path).
|
|
186
|
+
const record = this.daemonContext
|
|
187
|
+
? {
|
|
188
|
+
id,
|
|
189
|
+
projectId: input.projectId,
|
|
190
|
+
command: input.command,
|
|
191
|
+
cwd: input.cwd,
|
|
192
|
+
pid: null,
|
|
193
|
+
pgid: null,
|
|
194
|
+
argv: [spawnCmd, ...spawnArgs],
|
|
195
|
+
startedAt: new Date().toISOString(),
|
|
196
|
+
state: "running",
|
|
197
|
+
exitCode: null,
|
|
198
|
+
exitSignal: null,
|
|
199
|
+
daemonPid: this.daemonContext.pid,
|
|
200
|
+
daemonStartedAt: this.daemonContext.startedAt,
|
|
201
|
+
}
|
|
202
|
+
: null;
|
|
203
|
+
const managed = {
|
|
204
|
+
info,
|
|
205
|
+
process: null,
|
|
206
|
+
ptySize: null,
|
|
207
|
+
currentGeneration: 1,
|
|
208
|
+
attachedClients: new Map(),
|
|
209
|
+
outputPaused: false,
|
|
210
|
+
mutex,
|
|
211
|
+
bootstrapInput: input.bootstrapInput ?? null,
|
|
212
|
+
record,
|
|
213
|
+
};
|
|
214
|
+
this.sessions.set(id, managed);
|
|
215
|
+
this.store.mutateSessions((s) => {
|
|
216
|
+
s.sessions.push(structuredClone(info));
|
|
217
|
+
if (record) {
|
|
218
|
+
s.sessionRecords.push(structuredClone(record));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return info;
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
release();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async restart(id) {
|
|
228
|
+
const managed = this.sessions.get(id);
|
|
229
|
+
if (!managed)
|
|
230
|
+
throw new Error(`Session ${id} not found`);
|
|
231
|
+
const release = await managed.mutex.acquire();
|
|
232
|
+
try {
|
|
233
|
+
if (managed.info.state !== "ended") {
|
|
234
|
+
throw new Error(`Session ${id} is not ended; cannot restart`);
|
|
235
|
+
}
|
|
236
|
+
const nextGen = managed.currentGeneration + 1;
|
|
237
|
+
managed.currentGeneration = nextGen;
|
|
238
|
+
this.scrollbackLog?.bumpGeneration(id, nextGen);
|
|
239
|
+
managed.info = {
|
|
240
|
+
...managed.info,
|
|
241
|
+
pid: null,
|
|
242
|
+
state: "spawning",
|
|
243
|
+
generation: nextGen,
|
|
244
|
+
endedAt: undefined,
|
|
245
|
+
endReason: undefined,
|
|
246
|
+
};
|
|
247
|
+
managed.process = null;
|
|
248
|
+
managed.ptySize = null;
|
|
249
|
+
managed.attachedClients.clear();
|
|
250
|
+
// Reset the SessionRecord too: clear PID/PGID, re-arm state to
|
|
251
|
+
// "running" so the record participates in orphan reconciliation
|
|
252
|
+
// again, and refresh startedAt so the window starts now.
|
|
253
|
+
// daemonPid/daemonStartedAt stay this generation's -- restart()
|
|
254
|
+
// is by definition same-daemon (a different daemon would have
|
|
255
|
+
// already marked the record orphaned at boot).
|
|
256
|
+
if (managed.record && this.daemonContext) {
|
|
257
|
+
managed.record = {
|
|
258
|
+
...managed.record,
|
|
259
|
+
pid: null,
|
|
260
|
+
pgid: null,
|
|
261
|
+
state: "running",
|
|
262
|
+
exitCode: null,
|
|
263
|
+
exitSignal: null,
|
|
264
|
+
startedAt: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
this.persistRecord(managed.record);
|
|
267
|
+
}
|
|
268
|
+
this.persistSession(managed.info);
|
|
269
|
+
return managed.info;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
release();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
get(id) {
|
|
276
|
+
return this.sessions.get(id)?.info;
|
|
277
|
+
}
|
|
278
|
+
getScrollback(id) {
|
|
279
|
+
if (!this.sessions.has(id))
|
|
280
|
+
return null;
|
|
281
|
+
const tail = this.scrollbackLog?.readTail(id) ?? "";
|
|
282
|
+
return tail || null;
|
|
283
|
+
}
|
|
284
|
+
list() {
|
|
285
|
+
return [...this.sessions.values()].map((s) => s.info);
|
|
286
|
+
}
|
|
287
|
+
getForegroundProcess(id) {
|
|
288
|
+
const managed = this.sessions.get(id);
|
|
289
|
+
if (!managed?.process)
|
|
290
|
+
return null;
|
|
291
|
+
try {
|
|
292
|
+
const name = managed.process.process;
|
|
293
|
+
return name ? basename(name) : null;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
setTitle(id, title, titleManual = false) {
|
|
300
|
+
const managed = this.sessions.get(id);
|
|
301
|
+
if (!managed)
|
|
302
|
+
return false;
|
|
303
|
+
const currentManual = managed.info.titleManual === true;
|
|
304
|
+
if (managed.info.title === title && currentManual === titleManual) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
managed.info = titleManual
|
|
308
|
+
? { ...managed.info, title, titleManual: true }
|
|
309
|
+
: (() => {
|
|
310
|
+
const { titleManual: _drop, ...rest } = managed.info;
|
|
311
|
+
return { ...rest, title };
|
|
312
|
+
})();
|
|
313
|
+
this.persistSession(managed.info);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
setPinned(id, pinned) {
|
|
317
|
+
const managed = this.sessions.get(id);
|
|
318
|
+
if (!managed)
|
|
319
|
+
return false;
|
|
320
|
+
const current = managed.info.pinned === true;
|
|
321
|
+
if (current === pinned)
|
|
322
|
+
return false;
|
|
323
|
+
const next = pinned
|
|
324
|
+
? { ...managed.info, pinned: true }
|
|
325
|
+
: (() => {
|
|
326
|
+
const { pinned: _drop, ...rest } = managed.info;
|
|
327
|
+
return rest;
|
|
328
|
+
})();
|
|
329
|
+
managed.info = next;
|
|
330
|
+
this.persistSession(managed.info);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
listByProject(projectId) {
|
|
334
|
+
return this.list().filter((s) => s.projectId === projectId);
|
|
335
|
+
}
|
|
336
|
+
onSessionInput(listener) {
|
|
337
|
+
this.inputListeners.push(listener);
|
|
338
|
+
}
|
|
339
|
+
write(id, data, generation) {
|
|
340
|
+
const managed = this.sessions.get(id);
|
|
341
|
+
if (!managed?.process)
|
|
342
|
+
return;
|
|
343
|
+
/*
|
|
344
|
+
* PTY generation gate generation gate. Drop input tagged with a stale generation --
|
|
345
|
+
* happens when the previous PTY's TUI sent a DECRQM-style query and
|
|
346
|
+
* the terminal's reply is in-flight on the WS while we auto-resume
|
|
347
|
+
* a new shell. Without this, the reply lands on the new shell's
|
|
348
|
+
* stdin and visible fragments (e.g. "2026;2$y" from a DECRPM mode
|
|
349
|
+
* 2026 response) leak onto the prompt.
|
|
350
|
+
*
|
|
351
|
+
* `0` and `undefined` are both "no gating" sentinels -- used by
|
|
352
|
+
* legacy non-WS callers (osc7/port-detect taps), pre-init-ack
|
|
353
|
+
* queued web INPUT (web's `currentGenerationRef` defaults to 0
|
|
354
|
+
* before the init-ack populates it), and the daemon-IPC path when
|
|
355
|
+
* the legacy WRITE codec lacked a generation field. Aligns with
|
|
356
|
+
* the daemon's `handleWrite` semantics so in-process and daemon
|
|
357
|
+
* modes behave identically (PTY generation gate parity).
|
|
358
|
+
*/
|
|
359
|
+
if (sessionPolicy.shouldDropStaleInput(generation, managed.currentGeneration)) {
|
|
360
|
+
if (process.env.PARASOR_INPUT_DEBUG === "1") {
|
|
361
|
+
// eslint-disable-next-line no-console
|
|
362
|
+
console.error(`[input] drop stale gen=${generation} current=${managed.currentGeneration} session=${id.slice(0, 8)} bytes=${JSON.stringify(data.slice(0, 32))}`);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const listener of this.inputListeners) {
|
|
367
|
+
listener(id, data);
|
|
368
|
+
}
|
|
369
|
+
managed.process.write(data);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Resize the underlying PTY. The explicit resize caller is authoritative,
|
|
373
|
+
* but duplicate claims for the current PTY size must not issue another
|
|
374
|
+
* SIGWINCH.
|
|
375
|
+
*/
|
|
376
|
+
resize(id, cols, rows) {
|
|
377
|
+
const managed = this.sessions.get(id);
|
|
378
|
+
if (!managed?.process)
|
|
379
|
+
return;
|
|
380
|
+
if (managed.ptySize?.cols === cols && managed.ptySize.rows === rows) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (process.env.PARASOR_RESIZE_DEBUG === "1") {
|
|
384
|
+
// eslint-disable-next-line no-console
|
|
385
|
+
console.error(`[resize] session=${id.slice(0, 8)} -> ${cols}x${rows}`);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
managed.process.resize(cols, rows);
|
|
389
|
+
managed.ptySize = { cols, rows };
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// node-pty rejects invalid dims -- ignore
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Force a SIGWINCH without changing dims so a TUI repaints from scratch.
|
|
397
|
+
* Used when a client surface returns to the foreground (iOS background
|
|
398
|
+
* tab freeze, mobile visibilitychange). node-pty's resize() is a no-op
|
|
399
|
+
* when new dims equal current ones, so we briefly bump rows and snap
|
|
400
|
+
* back -- INK collapses the two calls into a single repaint.
|
|
401
|
+
*/
|
|
402
|
+
refresh(id) {
|
|
403
|
+
const managed = this.sessions.get(id);
|
|
404
|
+
if (!managed?.process)
|
|
405
|
+
return;
|
|
406
|
+
const proc = managed.process;
|
|
407
|
+
try {
|
|
408
|
+
// Read current rows via ptyProc internals isn't exposed, but bumping
|
|
409
|
+
// by +1 and snapping back is safe regardless of the current value --
|
|
410
|
+
// we don't need to know it.
|
|
411
|
+
const anyProc = proc;
|
|
412
|
+
const rows = anyProc.rows ?? 24;
|
|
413
|
+
const cols = anyProc.cols ?? 80;
|
|
414
|
+
proc.resize(cols, rows + 1);
|
|
415
|
+
proc.resize(cols, rows);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// ignore
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
pauseOutput(id, clientId) {
|
|
422
|
+
const managed = this.sessions.get(id);
|
|
423
|
+
if (!managed?.process)
|
|
424
|
+
return;
|
|
425
|
+
const client = managed.attachedClients.get(clientId);
|
|
426
|
+
if (!client)
|
|
427
|
+
return;
|
|
428
|
+
client.flowPaused = true;
|
|
429
|
+
this.applyOutputPauseState(managed);
|
|
430
|
+
}
|
|
431
|
+
resumeOutput(id, clientId) {
|
|
432
|
+
const managed = this.sessions.get(id);
|
|
433
|
+
if (!managed?.process)
|
|
434
|
+
return;
|
|
435
|
+
const client = managed.attachedClients.get(clientId);
|
|
436
|
+
if (!client)
|
|
437
|
+
return;
|
|
438
|
+
client.flowPaused = false;
|
|
439
|
+
this.applyOutputPauseState(managed);
|
|
440
|
+
}
|
|
441
|
+
shouldPauseOutput(managed) {
|
|
442
|
+
const flowPausedFlags = [...managed.attachedClients.values()].map((client) => client.flowPaused);
|
|
443
|
+
return sessionPolicy.shouldPauseOutputForClients(flowPausedFlags);
|
|
444
|
+
}
|
|
445
|
+
applyOutputPauseState(managed) {
|
|
446
|
+
const shouldPause = this.shouldPauseOutput(managed);
|
|
447
|
+
if (shouldPause === managed.outputPaused)
|
|
448
|
+
return;
|
|
449
|
+
managed.outputPaused = shouldPause;
|
|
450
|
+
try {
|
|
451
|
+
if (shouldPause) {
|
|
452
|
+
managed.process?.pause();
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
managed.process?.resume();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// ignore node-pty pause/resume failures; detach/reconnect can race exit
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Attach a WebSocket client. Spawns the PTY on first call (using the
|
|
464
|
+
* client's viewport dims), replays scrollback to the fresh listener,
|
|
465
|
+
* and adds the listener to the broadcast set. A subsequent call with
|
|
466
|
+
* the same clientId replaces the existing listener in place (no
|
|
467
|
+
* duplicate broadcast).
|
|
468
|
+
*/
|
|
469
|
+
async initClient(id, clientId, cols, rows, listener, callerToken) {
|
|
470
|
+
const managed = this.sessions.get(id);
|
|
471
|
+
if (!managed)
|
|
472
|
+
return { ok: false };
|
|
473
|
+
const release = await managed.mutex.acquire();
|
|
474
|
+
try {
|
|
475
|
+
if (managed.info.state === "ended") {
|
|
476
|
+
// Either auto-resume (silent re-spawn with scrollback retained) or
|
|
477
|
+
// refuse -- the client then renders the error pane.
|
|
478
|
+
if (!this.autoResumeIfSafe(managed, cols, rows))
|
|
479
|
+
return { ok: false };
|
|
480
|
+
}
|
|
481
|
+
else if (managed.info.state === "spawning") {
|
|
482
|
+
this.spawnProcess(managed, cols, rows);
|
|
483
|
+
}
|
|
484
|
+
const attachToken = this.mintAttachToken(callerToken);
|
|
485
|
+
managed.attachedClients.set(clientId, {
|
|
486
|
+
kind: "string",
|
|
487
|
+
listener,
|
|
488
|
+
attachToken,
|
|
489
|
+
flowPaused: false,
|
|
490
|
+
});
|
|
491
|
+
this.applyOutputPauseState(managed);
|
|
492
|
+
/*
|
|
493
|
+
* Replay scrollback to just this client so its xterm has state.
|
|
494
|
+
* Terminal query escapes (XTVERSION / DA / DSR) are stripped here
|
|
495
|
+
* because re-parsing them on the fresh xterm would make it emit
|
|
496
|
+
* the response again -- which then lands in the idle shell's
|
|
497
|
+
* readline buffer as visible garbage. The live path is untouched;
|
|
498
|
+
* the single legitimate response-per-query still happens when the
|
|
499
|
+
* app first sends the query.
|
|
500
|
+
*
|
|
501
|
+
* Attach fencing: if the listener throws (e.g. ws.send fails because
|
|
502
|
+
* the WS just closed), the Promise rejects before the caller can
|
|
503
|
+
* capture `attachToken`, so cleanupTerminalRelay later sees an
|
|
504
|
+
* undefined token and skips detach. Roll back the just-inserted
|
|
505
|
+
* entry inline -- the token-equality guard preserves a concurrent
|
|
506
|
+
* fresher attach (defensive: mutex makes this unreachable today).
|
|
507
|
+
*/
|
|
508
|
+
const replayText = this.scrollbackLog?.readTail(id) ?? "";
|
|
509
|
+
if (replayText) {
|
|
510
|
+
try {
|
|
511
|
+
listener(stripQueryEscapes(replayText));
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
const current = managed.attachedClients.get(clientId);
|
|
515
|
+
if (current?.attachToken === attachToken) {
|
|
516
|
+
managed.attachedClients.delete(clientId);
|
|
517
|
+
this.applyOutputPauseState(managed);
|
|
518
|
+
}
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { ok: true, attachToken };
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
release();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* capable attach.
|
|
530
|
+
*
|
|
531
|
+
* Held under `managed.mutex` for the entire negotiation so that any
|
|
532
|
+
* concurrent PTY broadcast queues behind us -- the live chunk listener
|
|
533
|
+
* is registered last, after we have already decided the replay strategy
|
|
534
|
+
* and snapshotted the chunk ring. This prevents init-ack / replay /
|
|
535
|
+
* live OUTPUT from interleaving on the wire.
|
|
536
|
+
*
|
|
537
|
+
* Falls back to JSON-path semantics when `capabilities.binary === false`
|
|
538
|
+
* (the WS handler will not call us in that case, but the contract
|
|
539
|
+
* stays well-defined).
|
|
540
|
+
*/
|
|
541
|
+
async attachClient(id, clientId, cols, rows, capabilities, sink) {
|
|
542
|
+
const managed = this.sessions.get(id);
|
|
543
|
+
if (!managed)
|
|
544
|
+
return { ok: false };
|
|
545
|
+
const release = await managed.mutex.acquire();
|
|
546
|
+
try {
|
|
547
|
+
if (managed.info.state === "ended") {
|
|
548
|
+
if (!this.autoResumeIfSafe(managed, cols, rows))
|
|
549
|
+
return { ok: false };
|
|
550
|
+
}
|
|
551
|
+
else if (managed.info.state === "spawning") {
|
|
552
|
+
this.spawnProcess(managed, cols, rows);
|
|
553
|
+
}
|
|
554
|
+
const negotiated = {
|
|
555
|
+
binary: capabilities.binary === true,
|
|
556
|
+
// chunkedReplay requires binary -- guarded explicitly so an
|
|
557
|
+
// ill-formed client (false/true) is canonicalised before we
|
|
558
|
+
// commit it into the response and the chunk listener.
|
|
559
|
+
chunkedReplay: capabilities.binary === true && capabilities.chunkedReplay === true,
|
|
560
|
+
};
|
|
561
|
+
const generation = managed.currentGeneration;
|
|
562
|
+
const ringSnapshot = this.scrollbackLog?.ringState(id, generation) ?? {
|
|
563
|
+
generation,
|
|
564
|
+
lastDeliveredSeq: null,
|
|
565
|
+
oldestSeq: null,
|
|
566
|
+
};
|
|
567
|
+
let replay = "none";
|
|
568
|
+
let chunks;
|
|
569
|
+
let fullReplay;
|
|
570
|
+
let replayDiagnostics;
|
|
571
|
+
// Translate the wire-side string `lastSeen.seq` into BigInt for
|
|
572
|
+
// ring comparison. Malformed inputs (NaN, negative, non-decimal)
|
|
573
|
+
// collapse to "no cursor" so we never throw on the hot path.
|
|
574
|
+
const lastSeenForRing = sessionPolicy.parseLastSeen(capabilities.lastSeen);
|
|
575
|
+
// Disk tail is the single source of truth for full replay. Read it
|
|
576
|
+
// lazily and at most once: the common delta-replay reconnect never
|
|
577
|
+
// needs it, and readTail does a flush + synchronous readFileSync of
|
|
578
|
+
// up to the 4 MB tail under the session mutex.
|
|
579
|
+
let diskTailCache;
|
|
580
|
+
const getDiskTail = () => {
|
|
581
|
+
if (diskTailCache === undefined) {
|
|
582
|
+
diskTailCache = this.scrollbackLog?.readTail(id) ?? "";
|
|
583
|
+
}
|
|
584
|
+
return diskTailCache;
|
|
585
|
+
};
|
|
586
|
+
if (negotiated.chunkedReplay && this.scrollbackLog) {
|
|
587
|
+
const decision = this.scrollbackLog.readSince(id, lastSeenForRing);
|
|
588
|
+
if (decision.kind === "delta") {
|
|
589
|
+
replay = "delta";
|
|
590
|
+
chunks = decision.chunks.map((c) => ({
|
|
591
|
+
generation,
|
|
592
|
+
seq: c.seq,
|
|
593
|
+
data: c.data,
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
else if (decision.kind === "full") {
|
|
597
|
+
const tail = getDiskTail();
|
|
598
|
+
replay = "full";
|
|
599
|
+
const resolved = await this.buildFullReplay(id, tail, cols, rows);
|
|
600
|
+
fullReplay = resolved.fullReplay;
|
|
601
|
+
replayDiagnostics = resolved.replayDiagnostics;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
if (lastSeenForRing) {
|
|
605
|
+
replay = "none";
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
const tail = getDiskTail();
|
|
609
|
+
replay = tail ? "full" : "none";
|
|
610
|
+
if (replay === "full") {
|
|
611
|
+
const resolved = await this.buildFullReplay(id, tail, cols, rows);
|
|
612
|
+
fullReplay = resolved.fullReplay;
|
|
613
|
+
replayDiagnostics = resolved.replayDiagnostics;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (negotiated.binary) {
|
|
619
|
+
// Binary OUTPUT but no chunked replay -- emit the disk tail once
|
|
620
|
+
// via the legacy `replay` envelope so the new xterm has state,
|
|
621
|
+
// then live OUTPUT continues.
|
|
622
|
+
const tail = getDiskTail();
|
|
623
|
+
if (tail) {
|
|
624
|
+
replay = "full";
|
|
625
|
+
const resolved = await this.buildFullReplay(id, tail, cols, rows);
|
|
626
|
+
fullReplay = resolved.fullReplay;
|
|
627
|
+
replayDiagnostics = resolved.replayDiagnostics;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const attachToken = this.mintAttachToken(undefined);
|
|
631
|
+
managed.attachedClients.set(clientId, {
|
|
632
|
+
kind: "chunk",
|
|
633
|
+
listener: sink.onChunk,
|
|
634
|
+
onExit: sink.onExit,
|
|
635
|
+
attachToken,
|
|
636
|
+
flowPaused: false,
|
|
637
|
+
});
|
|
638
|
+
this.applyOutputPauseState(managed);
|
|
639
|
+
return {
|
|
640
|
+
ok: true,
|
|
641
|
+
capabilities: negotiated,
|
|
642
|
+
serverState: {
|
|
643
|
+
generation: ringSnapshot.generation,
|
|
644
|
+
lastDeliveredSeq: ringSnapshot.lastDeliveredSeq === null
|
|
645
|
+
? null
|
|
646
|
+
: ringSnapshot.lastDeliveredSeq.toString(),
|
|
647
|
+
oldestSeq: ringSnapshot.oldestSeq === null
|
|
648
|
+
? null
|
|
649
|
+
: ringSnapshot.oldestSeq.toString(),
|
|
650
|
+
},
|
|
651
|
+
replay,
|
|
652
|
+
chunks,
|
|
653
|
+
fullReplay,
|
|
654
|
+
replayDiagnostics,
|
|
655
|
+
attachToken,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
release();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async buildFullReplay(id, tail, cols, rows) {
|
|
663
|
+
const rawBytes = Buffer.byteLength(tail, "utf8");
|
|
664
|
+
if (this.headlessReplayEnabled && this.headlessStateCache) {
|
|
665
|
+
try {
|
|
666
|
+
const headlessSnapshot = (await this.headlessStateCache.snapshot(id, { cols, rows })) ??
|
|
667
|
+
(await this.headlessStateCache.rebuild(id, tail, { cols, rows }));
|
|
668
|
+
if (!headlessSnapshot) {
|
|
669
|
+
throw new Error("empty headless replay snapshot");
|
|
670
|
+
}
|
|
671
|
+
const snapshot = headlessSnapshot.snapshot;
|
|
672
|
+
return {
|
|
673
|
+
fullReplay: snapshot.text,
|
|
674
|
+
replayDiagnostics: {
|
|
675
|
+
source: headlessSnapshot.source,
|
|
676
|
+
rawBytes: snapshot.rawBytes,
|
|
677
|
+
replayBytes: snapshot.snapshotBytes,
|
|
678
|
+
headlessDurationMs: snapshot.durationMs,
|
|
679
|
+
headlessBufferLines: snapshot.bufferLines,
|
|
680
|
+
headlessEmittedLines: snapshot.emittedLines,
|
|
681
|
+
scrollbackLines: this.headlessReplayScrollbackLines,
|
|
682
|
+
maxBytes: this.headlessReplayMaxBytes,
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
console.warn(`[terminal] in-process headless replay snapshot failed for session=${id.slice(0, 8)}: ${err.message}`);
|
|
688
|
+
const fullReplay = stripQueryEscapes(utf8Tail(tail, this.inProcessLegacyReplayMaxBytes));
|
|
689
|
+
return {
|
|
690
|
+
fullReplay,
|
|
691
|
+
replayDiagnostics: {
|
|
692
|
+
source: "headless-fallback",
|
|
693
|
+
rawBytes,
|
|
694
|
+
replayBytes: Buffer.byteLength(fullReplay, "utf8"),
|
|
695
|
+
scrollbackLines: this.headlessReplayScrollbackLines,
|
|
696
|
+
maxBytes: this.headlessReplayMaxBytes,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const fullReplay = stripQueryEscapes(utf8Tail(tail, this.inProcessLegacyReplayMaxBytes));
|
|
702
|
+
return {
|
|
703
|
+
fullReplay,
|
|
704
|
+
replayDiagnostics: {
|
|
705
|
+
source: "raw-tail",
|
|
706
|
+
rawBytes,
|
|
707
|
+
replayBytes: Buffer.byteLength(fullReplay, "utf8"),
|
|
708
|
+
maxBytes: this.inProcessLegacyReplayMaxBytes,
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
updateHeadlessState(sessionId, data) {
|
|
713
|
+
void this.headlessStateCache
|
|
714
|
+
?.writeExisting(sessionId, data)
|
|
715
|
+
.catch((err) => {
|
|
716
|
+
console.warn(`[terminal] in-process headless state update failed for session=${sessionId.slice(0, 8)}: ${err.message}`);
|
|
717
|
+
this.headlessStateCache?.delete(sessionId);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
detachClient(id, clientId, expectedToken) {
|
|
721
|
+
const managed = this.sessions.get(id);
|
|
722
|
+
if (!managed)
|
|
723
|
+
return;
|
|
724
|
+
if (expectedToken !== undefined) {
|
|
725
|
+
const entry = managed.attachedClients.get(clientId);
|
|
726
|
+
if (!entry || entry.attachToken !== expectedToken)
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
managed.attachedClients.delete(clientId);
|
|
730
|
+
this.applyOutputPauseState(managed);
|
|
731
|
+
}
|
|
732
|
+
async dispose(id) {
|
|
733
|
+
const managed = this.sessions.get(id);
|
|
734
|
+
if (!managed)
|
|
735
|
+
return;
|
|
736
|
+
const release = await managed.mutex.acquire();
|
|
737
|
+
try {
|
|
738
|
+
if (managed.process) {
|
|
739
|
+
managed.info.state = "ended"; // prevent exit callback from acting
|
|
740
|
+
try {
|
|
741
|
+
managed.process.kill();
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
// ignore kill errors
|
|
745
|
+
}
|
|
746
|
+
managed.process = null;
|
|
747
|
+
}
|
|
748
|
+
this.sessions.delete(id);
|
|
749
|
+
this.scrollbackLog?.remove(id);
|
|
750
|
+
this.store.mutateSessions((s) => {
|
|
751
|
+
s.sessions = s.sessions.filter((sess) => sess.id !== id);
|
|
752
|
+
s.sessionRecords = s.sessionRecords.filter((rec) => rec.id !== id);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
release();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async disposeAll() {
|
|
760
|
+
const ids = [...this.sessions.keys()];
|
|
761
|
+
await Promise.all(ids.map((id) => this.dispose(id)));
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Graceful-shutdown variant: kill every live PTY and mark each session
|
|
765
|
+
* as graceful, but keep the session in state so it can be restored
|
|
766
|
+
* (with scrollback) on the next server start. Unlike dispose, does not
|
|
767
|
+
* remove sessions from the store. The optional `reason` lets the
|
|
768
|
+
* daemon shutdown path distinguish itself from the server shutdown
|
|
769
|
+
* path so the UI can pick the correct error/recovery branch -- a
|
|
770
|
+
* "daemon-graceful" exit means the PTY children themselves are gone
|
|
771
|
+
* (the daemon owned them), while a "server-graceful" exit in daemon
|
|
772
|
+
* mode would be unusual (server unit shutdown but the daemon's PTYs
|
|
773
|
+
* survive).
|
|
774
|
+
*/
|
|
775
|
+
async shutdownAll(reason = { type: "server-graceful" }) {
|
|
776
|
+
const entries = [...this.sessions.values()];
|
|
777
|
+
await Promise.all(entries.map(async (managed) => {
|
|
778
|
+
const release = await managed.mutex.acquire();
|
|
779
|
+
try {
|
|
780
|
+
if (managed.info.state === "ended")
|
|
781
|
+
return;
|
|
782
|
+
managed.info = {
|
|
783
|
+
...managed.info,
|
|
784
|
+
state: "ended",
|
|
785
|
+
pid: null,
|
|
786
|
+
endedAt: Date.now(),
|
|
787
|
+
endReason: reason,
|
|
788
|
+
};
|
|
789
|
+
if (managed.process) {
|
|
790
|
+
try {
|
|
791
|
+
managed.process.kill();
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// ignore
|
|
795
|
+
}
|
|
796
|
+
managed.process = null;
|
|
797
|
+
}
|
|
798
|
+
this.persistSession(managed.info);
|
|
799
|
+
// Mirror the shutdown into the SessionRecord: from the
|
|
800
|
+
// orphan-reconcile point of view this PTY is gone (state
|
|
801
|
+
// "exited", PID null). exitSignal records the signal that
|
|
802
|
+
// would have been delivered -- node-pty's `kill()` with no
|
|
803
|
+
// argument sends SIGHUP (controlling-terminal hangup), so we
|
|
804
|
+
// mirror that here. We do *not* remove the record -- the next
|
|
805
|
+
// daemon boot will see state="exited" and pass it through
|
|
806
|
+
// unchanged (orphan-cleanup ignores already-terminal records).
|
|
807
|
+
if (managed.record && this.daemonContext) {
|
|
808
|
+
managed.record = {
|
|
809
|
+
...managed.record,
|
|
810
|
+
pid: null,
|
|
811
|
+
pgid: null,
|
|
812
|
+
state: "exited",
|
|
813
|
+
exitSignal: "SIGHUP",
|
|
814
|
+
};
|
|
815
|
+
this.persistRecord(managed.record);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
release();
|
|
820
|
+
}
|
|
821
|
+
}));
|
|
822
|
+
// Ensure any buffered scrollback writes are on disk before shutdown
|
|
823
|
+
// completes; the marker file is written after this returns.
|
|
824
|
+
this.scrollbackLog?.flushAll();
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Restore a session from persisted AppState. Sessions that were running
|
|
828
|
+
* or spawning at server/daemon exit time carry no `endReason` yet --
|
|
829
|
+
* fill it in based on whether the previous shutdown was graceful
|
|
830
|
+
* (marker present) or a crash (marker absent). The host context picks
|
|
831
|
+
* the correct prefix: `daemon-*` when this is the host instance owned
|
|
832
|
+
* by the `parasor-pty-host` daemon (PTY children belonged to the
|
|
833
|
+
* daemon), `server-*` otherwise. Sessions that already have
|
|
834
|
+
* `endReason` (either from an earlier process exit or from
|
|
835
|
+
* `shutdownAll`) keep it.
|
|
836
|
+
*/
|
|
837
|
+
loadPersistedSession(session, wasGracefulShutdown) {
|
|
838
|
+
const fallback = sessionPolicy.deriveLoadFallbackEndReason(this.daemonContext !== null, wasGracefulShutdown);
|
|
839
|
+
const endReason = session.endReason ?? fallback;
|
|
840
|
+
// Pick up any pre-existing record for this session from AppState so
|
|
841
|
+
// restart() / dispose() / shutdownAll() round-trip correctly. If the
|
|
842
|
+
// store has no matching record (in-process upgrade scenario, or the
|
|
843
|
+
// previous shutdown was a crash that lost the record), leave it null
|
|
844
|
+
// -- orphan-cleanup will not touch a record that does not exist.
|
|
845
|
+
const existingRecord = this.store.get().sessionRecords.find((r) => r.id === session.id) ?? null;
|
|
846
|
+
const managed = {
|
|
847
|
+
info: { ...session, state: "ended", pid: null, endReason },
|
|
848
|
+
process: null,
|
|
849
|
+
ptySize: null,
|
|
850
|
+
currentGeneration: session.generation,
|
|
851
|
+
attachedClients: new Map(),
|
|
852
|
+
outputPaused: false,
|
|
853
|
+
mutex: new PromiseMutex(),
|
|
854
|
+
bootstrapInput: null,
|
|
855
|
+
record: existingRecord ? structuredClone(existingRecord) : null,
|
|
856
|
+
};
|
|
857
|
+
this.sessions.set(session.id, managed);
|
|
858
|
+
// The persisted copy from state.json may still carry `state: "running"`
|
|
859
|
+
// (crash) or lack endReason; sync the corrected info so REST, WS
|
|
860
|
+
// snapshots, and the on-disk state all agree.
|
|
861
|
+
this.persistSession(managed.info);
|
|
862
|
+
}
|
|
863
|
+
onSessionData(callback) {
|
|
864
|
+
this.globalDataListeners.push(callback);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Attempt a silent re-spawn, preserving the previous scrollback (so
|
|
868
|
+
* the user sees continuity) plus a visible separator line that says
|
|
869
|
+
* exactly when the restart happened. Caller must hold managed.mutex.
|
|
870
|
+
* Returns true if re-spawn was initiated; false if the session is not
|
|
871
|
+
* auto-resumable (caller should surface an error).
|
|
872
|
+
*/
|
|
873
|
+
autoResumeIfSafe(managed, cols, rows) {
|
|
874
|
+
if (!sessionPolicy.isAutoResumable(managed.info.command, managed.info.endReason)) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
const separator = sessionPolicy.buildRestartSeparator();
|
|
878
|
+
this.scrollbackLog?.append(managed.info.id, separator);
|
|
879
|
+
this.updateHeadlessState(managed.info.id, separator);
|
|
880
|
+
const nextGen = managed.currentGeneration + 1;
|
|
881
|
+
managed.currentGeneration = nextGen;
|
|
882
|
+
this.scrollbackLog?.bumpGeneration(managed.info.id, nextGen);
|
|
883
|
+
managed.info = {
|
|
884
|
+
...managed.info,
|
|
885
|
+
pid: null,
|
|
886
|
+
state: "spawning",
|
|
887
|
+
generation: nextGen,
|
|
888
|
+
endedAt: undefined,
|
|
889
|
+
endReason: undefined,
|
|
890
|
+
};
|
|
891
|
+
managed.attachedClients.clear();
|
|
892
|
+
this.persistSession(managed.info);
|
|
893
|
+
// Record participates in orphan reconciliation again. Same rule as
|
|
894
|
+
// restart(): same daemon, so daemonPid/daemonStartedAt stay.
|
|
895
|
+
if (managed.record && this.daemonContext) {
|
|
896
|
+
managed.record = {
|
|
897
|
+
...managed.record,
|
|
898
|
+
pid: null,
|
|
899
|
+
pgid: null,
|
|
900
|
+
state: "running",
|
|
901
|
+
exitCode: null,
|
|
902
|
+
exitSignal: null,
|
|
903
|
+
startedAt: new Date().toISOString(),
|
|
904
|
+
};
|
|
905
|
+
this.persistRecord(managed.record);
|
|
906
|
+
}
|
|
907
|
+
this.spawnProcess(managed, cols, rows);
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Spawn the underlying PTY process and wire up its handlers. Called
|
|
912
|
+
* from `initClient` (deferred spawn) and from the test-only eager path.
|
|
913
|
+
* Caller must hold managed.mutex.
|
|
914
|
+
*/
|
|
915
|
+
spawnProcess(managed, cols, rows) {
|
|
916
|
+
const { spawnCmd, spawnArgs } = this.resolveCommand(managed.info.command);
|
|
917
|
+
const proc = pty.spawn(spawnCmd, spawnArgs, {
|
|
918
|
+
name: "xterm-256color",
|
|
919
|
+
cols,
|
|
920
|
+
rows,
|
|
921
|
+
cwd: managed.info.cwd,
|
|
922
|
+
env: this.buildSessionEnv(managed.info.id, managed.info.projectId),
|
|
923
|
+
});
|
|
924
|
+
managed.process = proc;
|
|
925
|
+
managed.ptySize = { cols, rows };
|
|
926
|
+
managed.outputPaused = false;
|
|
927
|
+
managed.info = {
|
|
928
|
+
...managed.info,
|
|
929
|
+
pid: proc.pid,
|
|
930
|
+
state: "running",
|
|
931
|
+
};
|
|
932
|
+
this.persistSession(managed.info);
|
|
933
|
+
// node-pty calls setsid -> the child is its own session leader and
|
|
934
|
+
// the leader's pid == pgid. We can persist the same value as both
|
|
935
|
+
// pid and pgid; the daemon shutdown path uses kill(-pgid, ...) to
|
|
936
|
+
// kill the whole process group atomically (by design).
|
|
937
|
+
if (managed.record && this.daemonContext) {
|
|
938
|
+
managed.record = {
|
|
939
|
+
...managed.record,
|
|
940
|
+
pid: proc.pid,
|
|
941
|
+
pgid: proc.pid,
|
|
942
|
+
state: "running",
|
|
943
|
+
// Refresh startedAt so the record's "running" window is the
|
|
944
|
+
// actual spawn time (not the create-stub time).
|
|
945
|
+
startedAt: new Date().toISOString(),
|
|
946
|
+
};
|
|
947
|
+
this.persistRecord(managed.record);
|
|
948
|
+
}
|
|
949
|
+
this.attachProcHandlers(managed, managed.currentGeneration);
|
|
950
|
+
const bootstrapInput = managed.bootstrapInput;
|
|
951
|
+
if (bootstrapInput) {
|
|
952
|
+
managed.bootstrapInput = null;
|
|
953
|
+
this.write(managed.info.id, bootstrapInput, managed.currentGeneration);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
attachProcHandlers(managed, generationAtSpawn) {
|
|
957
|
+
const id = managed.info.id;
|
|
958
|
+
/*
|
|
959
|
+
* PTY generation gate: per-spawn batching state. `pendingData` and `flushScheduled`
|
|
960
|
+
* MUST live in this closure, not on `managed`, otherwise an old-gen
|
|
961
|
+
* setImmediate flush that fires AFTER auto-resume has spawned a new
|
|
962
|
+
* PTY can consume bytes from the new PTY's onData handler (which
|
|
963
|
+
* appends to the shared `managed.pendingData`) and either (a) misroute
|
|
964
|
+
* them under the old generation through globalDataListeners or (b)
|
|
965
|
+
* drop them entirely because the `generationStillCurrent` gate fires
|
|
966
|
+
* `false` for the merged batch. With a per-spawn closure each generation
|
|
967
|
+
* has an isolated buffer and an isolated flush slot, so old-gen flushes
|
|
968
|
+
* see only old-gen bytes and new-gen flushes see only new-gen bytes.
|
|
969
|
+
*/
|
|
970
|
+
let pendingData = "";
|
|
971
|
+
let flushScheduled = false;
|
|
972
|
+
managed.process?.onData((data) => {
|
|
973
|
+
pendingData += data;
|
|
974
|
+
if (!flushScheduled) {
|
|
975
|
+
flushScheduled = true;
|
|
976
|
+
setImmediate(() => {
|
|
977
|
+
const batch = pendingData;
|
|
978
|
+
pendingData = "";
|
|
979
|
+
flushScheduled = false;
|
|
980
|
+
// : 1 broadcast = 1 chunk. Compute the
|
|
981
|
+
// (gen, seq) pair once per flush so every chunk-flavored
|
|
982
|
+
// listener observes the same coordinates. The buffer is shared
|
|
983
|
+
// across listeners -- none of them should mutate it.
|
|
984
|
+
//
|
|
985
|
+
// PTY generation gate: tag with the spawn-time generation, NOT the live
|
|
986
|
+
// `managed.currentGeneration`. If auto-resume bumped the gen
|
|
987
|
+
// between data-arrival and this setImmediate flush, the live
|
|
988
|
+
// counter is already pointing at the new PTY, but every byte
|
|
989
|
+
// in `batch` was emitted by the old one. Using the live gen
|
|
990
|
+
// here would mis-tag old terminal-mode-query responses with
|
|
991
|
+
// the new generation, defeating the whole point of PTY generation gate.
|
|
992
|
+
const generation = generationAtSpawn;
|
|
993
|
+
/*
|
|
994
|
+
* PTY generation gate: if auto-resume bumped the gen between data-arrival
|
|
995
|
+
* and this flush, skip the chunk-ring append and the live
|
|
996
|
+
* client broadcast. The new ring (allocated by
|
|
997
|
+
* `bumpGeneration`) must not be overwritten by an
|
|
998
|
+
* `appendChunk(id, OLDGEN, ...)` that would reset it to
|
|
999
|
+
* fresh state with seq=0. Newly-attached clients of the
|
|
1000
|
+
* new session must not see old-PTY noise.
|
|
1001
|
+
*
|
|
1002
|
+
* PTY generation gate: also skip the disk `scrollbackLog.append()`
|
|
1003
|
+
* when stale. Otherwise auto-resume leaves a residue of old-PTY
|
|
1004
|
+
* bytes in the on-disk rehydration tail -- symmetric to the
|
|
1005
|
+
* daemon-side drop in remote-host.ts:551. Daemon-IPC propagation
|
|
1006
|
+
* (globalDataListeners) still fires below with `generation`, so
|
|
1007
|
+
* the remote-side gate decides whether to keep or drop.
|
|
1008
|
+
*/
|
|
1009
|
+
const generationStillCurrent = generation === managed.currentGeneration;
|
|
1010
|
+
let seq = null;
|
|
1011
|
+
let batchBuf = null;
|
|
1012
|
+
if (generationStillCurrent) {
|
|
1013
|
+
this.scrollbackLog?.append(id, batch);
|
|
1014
|
+
this.updateHeadlessState(id, batch);
|
|
1015
|
+
let hasChunkClient = false;
|
|
1016
|
+
for (const client of managed.attachedClients.values()) {
|
|
1017
|
+
if (client.kind === "chunk") {
|
|
1018
|
+
hasChunkClient = true;
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (hasChunkClient && this.scrollbackLog) {
|
|
1023
|
+
batchBuf = Buffer.from(batch, "utf8");
|
|
1024
|
+
seq = this.scrollbackLog.appendChunk(id, generation, batchBuf);
|
|
1025
|
+
}
|
|
1026
|
+
// Broadcast to every attached client; isolate listener faults
|
|
1027
|
+
// so one throwing client can't starve the others.
|
|
1028
|
+
for (const client of managed.attachedClients.values()) {
|
|
1029
|
+
try {
|
|
1030
|
+
if (client.kind === "string") {
|
|
1031
|
+
client.listener(batch);
|
|
1032
|
+
}
|
|
1033
|
+
else if (seq !== null && batchBuf) {
|
|
1034
|
+
client.listener(generation, seq, batchBuf);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
// ignore listener errors to keep broadcast loop alive
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// same isolation as attached-client listeners.
|
|
1043
|
+
// A throwing global listener (debug recorder, scrollback log) used
|
|
1044
|
+
// to leak as an uncaught exception inside setImmediate.
|
|
1045
|
+
for (const listener of this.globalDataListeners) {
|
|
1046
|
+
try {
|
|
1047
|
+
listener(id, batch, generation);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
// ignore listener errors
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
// node-pty's onExit handler treats the callback
|
|
1057
|
+
// return value as fire-and-forget. If we hand it an async function
|
|
1058
|
+
// and any await inside rejects (mutex.acquire, persistSession,
|
|
1059
|
+
// persistRecord), the rejection becomes an unhandledRejection. Wrap
|
|
1060
|
+
// the body in a sync function that explicitly catches the inner
|
|
1061
|
+
// promise so onExit cannot bubble.
|
|
1062
|
+
managed.process?.onExit(({ exitCode, signal }) => {
|
|
1063
|
+
void (async () => {
|
|
1064
|
+
const release = await managed.mutex.acquire();
|
|
1065
|
+
try {
|
|
1066
|
+
if (!this.sessions.has(id))
|
|
1067
|
+
return;
|
|
1068
|
+
if (managed.currentGeneration !== generationAtSpawn)
|
|
1069
|
+
return;
|
|
1070
|
+
// shutdownAll/dispose already set state=ended and recorded the
|
|
1071
|
+
// authoritative endReason; don't overwrite it with the kill signal
|
|
1072
|
+
// that happens to fire afterwards.
|
|
1073
|
+
if (managed.info.state === "ended")
|
|
1074
|
+
return;
|
|
1075
|
+
const endReason = sessionPolicy.deriveEndReason(signal, exitCode);
|
|
1076
|
+
managed.info = {
|
|
1077
|
+
...managed.info,
|
|
1078
|
+
state: "ended",
|
|
1079
|
+
pid: null,
|
|
1080
|
+
endedAt: Date.now(),
|
|
1081
|
+
endReason,
|
|
1082
|
+
};
|
|
1083
|
+
managed.process = null;
|
|
1084
|
+
// : notify binary-capable clients via the EXIT
|
|
1085
|
+
// chunk-listener channel. Legacy string clients learn about
|
|
1086
|
+
// exit via /ws/events SESSION_END (existing behavior).
|
|
1087
|
+
const numericExit = typeof exitCode === "number" && Number.isFinite(exitCode)
|
|
1088
|
+
? exitCode
|
|
1089
|
+
: 0;
|
|
1090
|
+
for (const client of managed.attachedClients.values()) {
|
|
1091
|
+
if (client.kind === "chunk" && client.onExit) {
|
|
1092
|
+
try {
|
|
1093
|
+
client.onExit(numericExit);
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
// ignore listener faults; broadcast loop is dead anyway
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
this.persistSession(managed.info);
|
|
1101
|
+
// Record the natural exit. node-pty exposes `signal` as a
|
|
1102
|
+
// numeric POSIX signal (or undefined when the child exited
|
|
1103
|
+
// normally); the SessionRecord schema stores the signal *name*
|
|
1104
|
+
// and a `number | null` exit code. deriveRecordExit coerces both
|
|
1105
|
+
// (null exit code when killed, `SIG<n>` for unmapped signals).
|
|
1106
|
+
if (managed.record && this.daemonContext) {
|
|
1107
|
+
const { exitCode: recordExitCode, exitSignal } = sessionPolicy.deriveRecordExit(exitCode, signal);
|
|
1108
|
+
managed.record = {
|
|
1109
|
+
...managed.record,
|
|
1110
|
+
pid: null,
|
|
1111
|
+
pgid: null,
|
|
1112
|
+
state: "exited",
|
|
1113
|
+
exitCode: recordExitCode,
|
|
1114
|
+
exitSignal,
|
|
1115
|
+
};
|
|
1116
|
+
this.persistRecord(managed.record);
|
|
1117
|
+
}
|
|
1118
|
+
this.onSessionExit?.(id, generationAtSpawn, endReason);
|
|
1119
|
+
}
|
|
1120
|
+
finally {
|
|
1121
|
+
release();
|
|
1122
|
+
}
|
|
1123
|
+
})().catch(() => {
|
|
1124
|
+
// Last-resort guard -- anything thrown inside the body (e.g. a
|
|
1125
|
+
// future store.mutateSessions() rejection) gets swallowed so node-pty
|
|
1126
|
+
// doesn't see an unhandledRejection-shaped event.
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
persistSession(info) {
|
|
1131
|
+
this.store.mutateSessions((s) => {
|
|
1132
|
+
const idx = s.sessions.findIndex((sess) => sess.id === info.id);
|
|
1133
|
+
if (idx >= 0) {
|
|
1134
|
+
s.sessions[idx] = structuredClone(info);
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
s.sessions.push(structuredClone(info));
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Upsert a SessionRecord into AppState. Mirror of `persistSession` for
|
|
1143
|
+
* the daemon-orphan-tracking side of the data model. Caller must hold
|
|
1144
|
+
* managed.mutex (record updates are interleaved with session updates,
|
|
1145
|
+
* single-record-per-session invariant guards against torn writes).
|
|
1146
|
+
*/
|
|
1147
|
+
persistRecord(record) {
|
|
1148
|
+
this.store.mutateSessions((s) => {
|
|
1149
|
+
const idx = s.sessionRecords.findIndex((rec) => rec.id === record.id);
|
|
1150
|
+
if (idx >= 0) {
|
|
1151
|
+
s.sessionRecords[idx] = structuredClone(record);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
s.sessionRecords.push(structuredClone(record));
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Test-only eager spawn. Production code path is always deferred via
|
|
1160
|
+
* `initClient`. This helper exists so unit tests that need a live PTY
|
|
1161
|
+
* without a WebSocket can still drive the manager.
|
|
1162
|
+
*/
|
|
1163
|
+
async testEagerSpawn(id, cols = 80, rows = 24) {
|
|
1164
|
+
const managed = this.sessions.get(id);
|
|
1165
|
+
if (!managed)
|
|
1166
|
+
throw new Error(`Session ${id} not found`);
|
|
1167
|
+
const release = await managed.mutex.acquire();
|
|
1168
|
+
try {
|
|
1169
|
+
if (managed.info.state !== "spawning")
|
|
1170
|
+
return;
|
|
1171
|
+
this.spawnProcess(managed, cols, rows);
|
|
1172
|
+
}
|
|
1173
|
+
finally {
|
|
1174
|
+
release();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
resolveCommand(command) {
|
|
1178
|
+
return sessionPolicy.resolveSessionCommand(command, {
|
|
1179
|
+
bashRcPath: this.ptyEnv.PARASOR_BASH_RC,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
//# sourceMappingURL=in-process-host.js.map
|