gitspace 0.2.0-rc.20 → 0.2.0-rc.21
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/package.json +11 -6
- package/.claude/settings.local.json +0 -10
- package/.gitspace/bundle.json +0 -50
- package/.gitspace/events.json +0 -11
- package/.gitspace/processes.json +0 -23
- package/.gitspace/scripts/select/01-status.sh +0 -39
- package/.gitspace/scripts/setup/01-install-deps.sh +0 -12
- package/.gitspace/scripts/setup/02-typecheck.sh +0 -16
- package/AGENTS.md +0 -469
- package/CLAUDE.md +0 -1
- package/bun.lock +0 -794
- package/docs/CONNECTION.md +0 -623
- package/docs/GATEWAY-WORKER.md +0 -319
- package/docs/GETTING-STARTED.md +0 -448
- package/docs/GITSPACE-PLATFORM.md +0 -1819
- package/docs/INFRASTRUCTURE.md +0 -1347
- package/docs/PROTOCOL.md +0 -619
- package/docs/QUICKSTART.md +0 -183
- package/docs/RELAY.md +0 -327
- package/docs/REMOTE-DESIGN.md +0 -554
- package/docs/ROADMAP.md +0 -564
- package/docs/SITE_DOCS_FIGMA_MAKE.md +0 -1176
- package/docs/STACK-DESIGN.md +0 -588
- package/docs/UNIFIED_ARCHITECTURE.md +0 -138
- package/experiments/pty-benchmark.ts +0 -148
- package/experiments/pty-latency.ts +0 -100
- package/experiments/router/client.ts +0 -199
- package/experiments/router/protocol.ts +0 -74
- package/experiments/router/router.ts +0 -217
- package/experiments/router/session.ts +0 -180
- package/experiments/router/test.ts +0 -133
- package/experiments/socket-bandwidth.ts +0 -77
- package/homebrew/gitspace.rb +0 -45
- package/landing-page/ATTRIBUTIONS.md +0 -3
- package/landing-page/README.md +0 -11
- package/landing-page/bun.lock +0 -801
- package/landing-page/guidelines/Guidelines.md +0 -61
- package/landing-page/index.html +0 -37
- package/landing-page/package.json +0 -90
- package/landing-page/postcss.config.mjs +0 -15
- package/landing-page/public/_redirects +0 -1
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +0 -53
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +0 -27
- package/landing-page/src/app/components/ui/accordion.tsx +0 -66
- package/landing-page/src/app/components/ui/alert-dialog.tsx +0 -157
- package/landing-page/src/app/components/ui/alert.tsx +0 -66
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +0 -11
- package/landing-page/src/app/components/ui/avatar.tsx +0 -53
- package/landing-page/src/app/components/ui/badge.tsx +0 -46
- package/landing-page/src/app/components/ui/breadcrumb.tsx +0 -109
- package/landing-page/src/app/components/ui/button.tsx +0 -57
- package/landing-page/src/app/components/ui/calendar.tsx +0 -75
- package/landing-page/src/app/components/ui/card.tsx +0 -92
- package/landing-page/src/app/components/ui/carousel.tsx +0 -241
- package/landing-page/src/app/components/ui/chart.tsx +0 -353
- package/landing-page/src/app/components/ui/checkbox.tsx +0 -32
- package/landing-page/src/app/components/ui/collapsible.tsx +0 -33
- package/landing-page/src/app/components/ui/command.tsx +0 -177
- package/landing-page/src/app/components/ui/context-menu.tsx +0 -252
- package/landing-page/src/app/components/ui/dialog.tsx +0 -135
- package/landing-page/src/app/components/ui/drawer.tsx +0 -132
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +0 -257
- package/landing-page/src/app/components/ui/form.tsx +0 -168
- package/landing-page/src/app/components/ui/hover-card.tsx +0 -44
- package/landing-page/src/app/components/ui/input-otp.tsx +0 -77
- package/landing-page/src/app/components/ui/input.tsx +0 -21
- package/landing-page/src/app/components/ui/label.tsx +0 -24
- package/landing-page/src/app/components/ui/menubar.tsx +0 -276
- package/landing-page/src/app/components/ui/navigation-menu.tsx +0 -168
- package/landing-page/src/app/components/ui/pagination.tsx +0 -127
- package/landing-page/src/app/components/ui/popover.tsx +0 -48
- package/landing-page/src/app/components/ui/progress.tsx +0 -31
- package/landing-page/src/app/components/ui/radio-group.tsx +0 -45
- package/landing-page/src/app/components/ui/resizable.tsx +0 -56
- package/landing-page/src/app/components/ui/scroll-area.tsx +0 -58
- package/landing-page/src/app/components/ui/select.tsx +0 -189
- package/landing-page/src/app/components/ui/separator.tsx +0 -28
- package/landing-page/src/app/components/ui/sheet.tsx +0 -139
- package/landing-page/src/app/components/ui/sidebar.tsx +0 -726
- package/landing-page/src/app/components/ui/skeleton.tsx +0 -13
- package/landing-page/src/app/components/ui/slider.tsx +0 -63
- package/landing-page/src/app/components/ui/sonner.tsx +0 -25
- package/landing-page/src/app/components/ui/switch.tsx +0 -31
- package/landing-page/src/app/components/ui/table.tsx +0 -116
- package/landing-page/src/app/components/ui/tabs.tsx +0 -66
- package/landing-page/src/app/components/ui/textarea.tsx +0 -18
- package/landing-page/src/app/components/ui/toggle-group.tsx +0 -73
- package/landing-page/src/app/components/ui/toggle.tsx +0 -47
- package/landing-page/src/app/components/ui/tooltip.tsx +0 -61
- package/landing-page/src/app/components/ui/use-mobile.ts +0 -21
- package/landing-page/src/app/components/ui/utils.ts +0 -6
- package/landing-page/src/components/docs/DocsContent.tsx +0 -801
- package/landing-page/src/components/docs/DocsSidebar.tsx +0 -90
- package/landing-page/src/components/landing/CTA.tsx +0 -59
- package/landing-page/src/components/landing/Comparison.tsx +0 -84
- package/landing-page/src/components/landing/FaultyTerminal.tsx +0 -424
- package/landing-page/src/components/landing/Features.tsx +0 -201
- package/landing-page/src/components/landing/Hero.tsx +0 -142
- package/landing-page/src/components/landing/Pricing.tsx +0 -140
- package/landing-page/src/components/landing/Roadmap.tsx +0 -86
- package/landing-page/src/components/landing/Security.tsx +0 -81
- package/landing-page/src/components/landing/TerminalWindow.tsx +0 -27
- package/landing-page/src/components/landing/UseCases.tsx +0 -55
- package/landing-page/src/components/landing/Workflow.tsx +0 -101
- package/landing-page/src/components/layout/DashboardNavbar.tsx +0 -37
- package/landing-page/src/components/layout/Footer.tsx +0 -55
- package/landing-page/src/components/layout/LandingNavbar.tsx +0 -82
- package/landing-page/src/components/ui/badge.tsx +0 -39
- package/landing-page/src/components/ui/breadcrumb.tsx +0 -115
- package/landing-page/src/components/ui/button.tsx +0 -57
- package/landing-page/src/components/ui/card.tsx +0 -79
- package/landing-page/src/components/ui/mock-terminal.tsx +0 -68
- package/landing-page/src/components/ui/separator.tsx +0 -28
- package/landing-page/src/lib/utils.ts +0 -6
- package/landing-page/src/main.tsx +0 -10
- package/landing-page/src/pages/Dashboard.tsx +0 -133
- package/landing-page/src/pages/DocsPage.tsx +0 -79
- package/landing-page/src/pages/LandingPage.tsx +0 -31
- package/landing-page/src/pages/TerminalView.tsx +0 -106
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +0 -3
- package/landing-page/src/styles/tailwind.css +0 -4
- package/landing-page/src/styles/theme.css +0 -181
- package/landing-page/vite.config.ts +0 -19
- package/scripts/GHOSTTY_TAB_BUG.md +0 -106
- package/scripts/build.ts +0 -298
- package/scripts/migrate-secrets.ts +0 -77
- package/scripts/release.ts +0 -140
- package/scripts/sample-events.ts +0 -263
- package/scripts/test-tabs-minimal.ts +0 -68
- package/scripts/test-tabs-workaround.ts +0 -95
- package/scripts/test-tabs.ts +0 -171
- package/src/__tests__/test-utils.ts +0 -298
- package/src/app/input/__tests__/sessionCommands.test.ts +0 -40
- package/src/app/input/sessionCommands.ts +0 -94
- package/src/app/session/__tests__/useAttachController.test.ts +0 -229
- package/src/app/session/createSessionBackend.bun.ts +0 -76
- package/src/app/session/createSessionBackend.web.ts +0 -104
- package/src/app/session/types.ts +0 -16
- package/src/app/session/useAttachController.ts +0 -220
- package/src/app/session/useProcessActions.ts +0 -201
- package/src/app/session/useSessionClient.ts +0 -35
- package/src/app/session/useWorkspaceDeleteFlow.ts +0 -170
- package/src/app.tui.tsx +0 -2929
- package/src/app.web.tsx +0 -1454
- package/src/commands/__tests__/connect-key.test.ts +0 -10
- package/src/commands/__tests__/events.test.ts +0 -201
- package/src/commands/__tests__/notifications.test.ts +0 -349
- package/src/commands/__tests__/process.test.ts +0 -251
- package/src/commands/__tests__/serve-messages.test.ts +0 -190
- package/src/commands/__tests__/serve-process-hosting.test.ts +0 -63
- package/src/commands/access.ts +0 -298
- package/src/commands/add.ts +0 -455
- package/src/commands/auth.ts +0 -369
- package/src/commands/bundle.ts +0 -232
- package/src/commands/config.ts +0 -242
- package/src/commands/connect-key.ts +0 -1
- package/src/commands/connect.ts +0 -576
- package/src/commands/directory.ts +0 -16
- package/src/commands/events.ts +0 -157
- package/src/commands/host.ts +0 -566
- package/src/commands/identity.ts +0 -184
- package/src/commands/linear.ts +0 -717
- package/src/commands/list.ts +0 -181
- package/src/commands/migrate.ts +0 -52
- package/src/commands/notifications.ts +0 -351
- package/src/commands/process.ts +0 -104
- package/src/commands/relay.ts +0 -315
- package/src/commands/remove.ts +0 -279
- package/src/commands/review.ts +0 -787
- package/src/commands/serve.ts +0 -1946
- package/src/commands/share.ts +0 -451
- package/src/commands/status.ts +0 -125
- package/src/commands/switch.ts +0 -361
- package/src/commands/tmux.ts +0 -317
- package/src/components/DPad.web.tsx +0 -343
- package/src/components/DiffViewer.web.tsx +0 -1192
- package/src/components/Events.tsx +0 -137
- package/src/components/Events.tui.tsx +0 -129
- package/src/components/Events.web.tsx +0 -386
- package/src/components/FloatingControls.web.tsx +0 -112
- package/src/components/FloatingJogWheel.web.tsx +0 -240
- package/src/components/Flow.tsx +0 -458
- package/src/components/Flow.tui.tsx +0 -343
- package/src/components/Flow.web.tsx +0 -442
- package/src/components/Inbox.tsx +0 -448
- package/src/components/Inbox.tui.tsx +0 -262
- package/src/components/Inbox.web.tsx +0 -329
- package/src/components/MachineList.tsx +0 -187
- package/src/components/MachineList.tui.tsx +0 -161
- package/src/components/MachineList.web.tsx +0 -210
- package/src/components/NumPad.web.tsx +0 -270
- package/src/components/ProjectList.tsx +0 -175
- package/src/components/ProjectList.tui.tsx +0 -109
- package/src/components/ProjectList.web.tsx +0 -143
- package/src/components/ProjectOnboardingStep.ts +0 -23
- package/src/components/ProjectOnboardingStep.tui.tsx +0 -88
- package/src/components/ProjectOnboardingStep.web.tsx +0 -59
- package/src/components/RemoteMachineScreen.tui.tsx +0 -690
- package/src/components/ScriptTerminal.tui.tsx +0 -160
- package/src/components/ScriptTerminal.web.tsx +0 -89
- package/src/components/SessionTerminal.tui.tsx +0 -406
- package/src/components/SessionTerminal.web.tsx +0 -467
- package/src/components/SpacesBrowser.tsx +0 -540
- package/src/components/SpacesBrowser.tui.tsx +0 -258
- package/src/components/SpacesBrowser.web.tsx +0 -332
- package/src/components/TerminalControls.web.tsx +0 -464
- package/src/components/ThreadPanel.web.tsx +0 -798
- package/src/components/__tests__/SpacesBrowser.test.ts +0 -541
- package/src/components/__tests__/SpacesBrowser.tui.test.tsx +0 -249
- package/src/components/__tests__/script-terminal-buffer.tui.test.ts +0 -72
- package/src/components/index.ts +0 -105
- package/src/components/review-decision-colors.ts +0 -11
- package/src/components/script-terminal-buffer.tui.ts +0 -37
- package/src/components/session-terminal-page-navigation.ts +0 -48
- package/src/components/terminal-bracketed-paste.tui.test.ts +0 -43
- package/src/components/terminal-bracketed-paste.tui.ts +0 -46
- package/src/core/__tests__/access.test.ts +0 -240
- package/src/core/__tests__/bundle-refresh.test.ts +0 -567
- package/src/core/__tests__/bundle.test.ts +0 -209
- package/src/core/__tests__/github-review.test.ts +0 -781
- package/src/core/__tests__/project-lifecycle.test.ts +0 -137
- package/src/core/__tests__/workspace-lifecycle.test.ts +0 -159
- package/src/core/__tests__/workspace.test.ts +0 -149
- package/src/core/access.ts +0 -277
- package/src/core/bundle-refresh.ts +0 -1064
- package/src/core/bundle.ts +0 -326
- package/src/core/config.ts +0 -405
- package/src/core/git.ts +0 -768
- package/src/core/github-review.ts +0 -761
- package/src/core/github.ts +0 -151
- package/src/core/identity.ts +0 -631
- package/src/core/linear.ts +0 -403
- package/src/core/preferences-service.ts +0 -17
- package/src/core/project-catalog.ts +0 -52
- package/src/core/project-lifecycle.ts +0 -163
- package/src/core/review-executor.ts +0 -316
- package/src/core/review.ts +0 -407
- package/src/core/secret-runtime.ts +0 -167
- package/src/core/shell.ts +0 -117
- package/src/core/trusted-relays.ts +0 -315
- package/src/core/workspace-lifecycle.ts +0 -216
- package/src/core/workspace.ts +0 -363
- package/src/hooks/__tests__/useLocalSession.tui.test.ts +0 -557
- package/src/hooks/index.ts +0 -8
- package/src/hooks/index.tui.ts +0 -32
- package/src/hooks/useDaemonStatus.tui.ts +0 -174
- package/src/hooks/useLocalSession.tui.ts +0 -395
- package/src/hooks/useRelayConnection.web.ts +0 -54
- package/src/hooks/useRemoteMachines.tui.ts +0 -166
- package/src/hooks/useRemoteTerminal.tui.ts +0 -22
- package/src/hooks/useReview.web.ts +0 -248
- package/src/hooks/useTerminal.web.ts +0 -36
- package/src/hooks/useUserActivity.ts +0 -61
- package/src/hooks/useVisualViewport.web.ts +0 -104
- package/src/index.ts +0 -1376
- package/src/lib/events/__tests__/collector-filter.test.ts +0 -105
- package/src/lib/events/__tests__/store-query.test.ts +0 -103
- package/src/lib/events/collector.ts +0 -494
- package/src/lib/events/filters.ts +0 -26
- package/src/lib/events/index.ts +0 -11
- package/src/lib/events/indexer.ts +0 -14
- package/src/lib/events/paths.ts +0 -69
- package/src/lib/events/reader.ts +0 -212
- package/src/lib/events/store.ts +0 -141
- package/src/lib/invite.web.ts +0 -58
- package/src/lib/preferences-service.web.ts +0 -41
- package/src/lib/processes/__tests__/config.test.ts +0 -83
- package/src/lib/processes/__tests__/names.test.ts +0 -125
- package/src/lib/processes/__tests__/schema.test.ts +0 -208
- package/src/lib/processes/__tests__/watchdog.test.ts +0 -210
- package/src/lib/processes/autostart.ts +0 -16
- package/src/lib/processes/config.ts +0 -187
- package/src/lib/processes/control.ts +0 -53
- package/src/lib/processes/editor.ts +0 -32
- package/src/lib/processes/events-config.ts +0 -37
- package/src/lib/processes/index.ts +0 -14
- package/src/lib/processes/instances.ts +0 -20
- package/src/lib/processes/manager.ts +0 -131
- package/src/lib/processes/names.ts +0 -71
- package/src/lib/processes/registry.ts +0 -26
- package/src/lib/processes/runner.ts +0 -211
- package/src/lib/processes/scheduler.ts +0 -17
- package/src/lib/processes/schema.ts +0 -74
- package/src/lib/processes/session-list.ts +0 -15
- package/src/lib/processes/state.ts +0 -82
- package/src/lib/processes/watchdog.test.ts +0 -79
- package/src/lib/processes/watchdog.ts +0 -106
- package/src/lib/remote-session/__tests__/protocol.test.ts +0 -291
- package/src/lib/remote-session/index.ts +0 -7
- package/src/lib/remote-session/protocol.ts +0 -443
- package/src/lib/remote-session/session-handler.ts +0 -1298
- package/src/lib/remote-session/workspace-scanner.ts +0 -161
- package/src/lib/sonner.web.ts +0 -1
- package/src/lib/storage/identity-store.web.ts +0 -94
- package/src/lib/tmux-lite/README.md +0 -81
- package/src/lib/tmux-lite/cli.ts +0 -855
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +0 -349
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +0 -291
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +0 -142
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +0 -339
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +0 -477
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +0 -499
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +0 -371
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +0 -573
- package/src/lib/tmux-lite/crypto/access-control.test.ts +0 -512
- package/src/lib/tmux-lite/crypto/access-control.ts +0 -320
- package/src/lib/tmux-lite/crypto/frames.test.ts +0 -262
- package/src/lib/tmux-lite/crypto/frames.ts +0 -141
- package/src/lib/tmux-lite/crypto/handshake.ts +0 -894
- package/src/lib/tmux-lite/crypto/identity.test.ts +0 -220
- package/src/lib/tmux-lite/crypto/identity.ts +0 -286
- package/src/lib/tmux-lite/crypto/index.ts +0 -51
- package/src/lib/tmux-lite/crypto/invites.test.ts +0 -381
- package/src/lib/tmux-lite/crypto/invites.ts +0 -215
- package/src/lib/tmux-lite/crypto/keyexchange.ts +0 -435
- package/src/lib/tmux-lite/crypto/keys.test.ts +0 -58
- package/src/lib/tmux-lite/crypto/keys.ts +0 -47
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +0 -169
- package/src/lib/tmux-lite/crypto/secretbox.ts +0 -124
- package/src/lib/tmux-lite/handshake-handler.ts +0 -451
- package/src/lib/tmux-lite/process-run.integration.test.ts +0 -266
- package/src/lib/tmux-lite/protocol.test.ts +0 -307
- package/src/lib/tmux-lite/protocol.ts +0 -291
- package/src/lib/tmux-lite/relay-client.ts +0 -506
- package/src/lib/tmux-lite/server-lifecycle.test.ts +0 -212
- package/src/lib/tmux-lite/server.ts +0 -1412
- package/src/lib/tmux-lite/shell-integration.sh +0 -37
- package/src/lib/tmux-lite/terminal-queries.test.ts +0 -54
- package/src/lib/tmux-lite/terminal-queries.ts +0 -49
- package/src/notifications/__tests__/useNotifications.test.ts +0 -739
- package/src/notifications/index.ts +0 -32
- package/src/notifications/policy.test.ts +0 -424
- package/src/notifications/policy.ts +0 -139
- package/src/notifications/types.ts +0 -82
- package/src/notifications/useNotifications.ts +0 -350
- package/src/pages/ReviewPage.web.tsx +0 -511
- package/src/preferences/index.ts +0 -1
- package/src/preferences/types.ts +0 -9
- package/src/relay/__tests__/e2e-flow.test.ts +0 -1284
- package/src/relay/__tests__/helpers/auth.ts +0 -354
- package/src/relay/__tests__/helpers/ports.ts +0 -51
- package/src/relay/__tests__/protocol-validation.test.ts +0 -265
- package/src/relay/authorization.ts +0 -303
- package/src/relay/embedded-assets.generated.d.ts +0 -15
- package/src/relay/identity.ts +0 -352
- package/src/relay/index.ts +0 -57
- package/src/relay/pipes.test.ts +0 -427
- package/src/relay/pipes.ts +0 -195
- package/src/relay/protocol.ts +0 -804
- package/src/relay/registries.test.ts +0 -437
- package/src/relay/registries.ts +0 -593
- package/src/relay/server.test.ts +0 -1323
- package/src/relay/server.ts +0 -1128
- package/src/relay/signing.ts +0 -238
- package/src/relay/types.ts +0 -69
- package/src/relay-client/__tests__/machine-directory-client.test.ts +0 -152
- package/src/relay-client/__tests__/useMachineDirectory.test.ts +0 -172
- package/src/relay-client/adapters/browser.ts +0 -27
- package/src/relay-client/adapters/node.ts +0 -29
- package/src/relay-client/index.ts +0 -33
- package/src/relay-client/machine-directory-client.ts +0 -244
- package/src/relay-client/useMachineDirectory.ts +0 -175
- package/src/serve/client-session-manager.ts +0 -635
- package/src/serve/daemon.ts +0 -497
- package/src/serve/pty-session.ts +0 -236
- package/src/serve/types.ts +0 -174
- package/src/session/__tests__/backend-manager.test.ts +0 -101
- package/src/session/__tests__/local-session-backend.test.ts +0 -1129
- package/src/session/__tests__/reducer.test.ts +0 -80
- package/src/session/__tests__/remote-session-backend.test.ts +0 -995
- package/src/session/__tests__/session-name.test.ts +0 -35
- package/src/session/__tests__/useBundleRefreshAttachFlow.test.ts +0 -431
- package/src/session/__tests__/useRemoteSessionClient.test.ts +0 -424
- package/src/session/__tests__/workspace-shell-hooks.integration.test.ts +0 -268
- package/src/session/__tests__/workspace-shell-hooks.test.ts +0 -24
- package/src/session/adapters/browser-remote.ts +0 -101
- package/src/session/adapters/node-remote.ts +0 -135
- package/src/session/backend-key.ts +0 -5
- package/src/session/backend-manager.ts +0 -80
- package/src/session/backend.ts +0 -93
- package/src/session/backends/local-session-backend.ts +0 -1119
- package/src/session/backends/remote-session-backend.ts +0 -1378
- package/src/session/crypto/__tests__/web-terminal.test.ts +0 -1158
- package/src/session/crypto/frames.web.ts +0 -205
- package/src/session/crypto/handshake.web.ts +0 -396
- package/src/session/crypto/identity.web.ts +0 -133
- package/src/session/crypto/keyexchange.web.ts +0 -246
- package/src/session/crypto/relay-signing.web.ts +0 -53
- package/src/session/events.ts +0 -38
- package/src/session/index.ts +0 -116
- package/src/session/reducer.ts +0 -274
- package/src/session/selectors.ts +0 -28
- package/src/session/session-name.ts +0 -50
- package/src/session/types.ts +0 -101
- package/src/session/useBundleRefreshAttachFlow.ts +0 -608
- package/src/session/useRemoteSessionClient.ts +0 -424
- package/src/session/useSessionEngine.ts +0 -432
- package/src/session/workspace-shell-hooks.ts +0 -35
- package/src/tui/__tests__/input-text.test.ts +0 -24
- package/src/tui/__tests__/local-terminal-sync.test.ts +0 -82
- package/src/tui/__tests__/session-terminal-page-navigation.test.ts +0 -94
- package/src/tui/app.tsx +0 -2
- package/src/tui/index.ts +0 -18
- package/src/tui/input-text.ts +0 -38
- package/src/tui/local-terminal-sync.ts +0 -41
- package/src/types/bundle-refresh.ts +0 -42
- package/src/types/bundle.ts +0 -130
- package/src/types/config.ts +0 -287
- package/src/types/errors.ts +0 -292
- package/src/types/events.ts +0 -91
- package/src/types/identity.ts +0 -284
- package/src/types/processes.ts +0 -45
- package/src/types/review.ts +0 -349
- package/src/types/script-phase.ts +0 -3
- package/src/types/workspace-fuzzy.ts +0 -49
- package/src/types/workspace.ts +0 -151
- package/src/utils/__tests__/onboarding.test.ts +0 -358
- package/src/utils/__tests__/run-scripts.test.ts +0 -535
- package/src/utils/__tests__/run-workspace-scripts.test.ts +0 -406
- package/src/utils/__tests__/workspace-setup.integration.test.ts +0 -633
- package/src/utils/__tests__/workspace-state.test.ts +0 -78
- package/src/utils/bun-socket-writer.ts +0 -80
- package/src/utils/clipboard.ts +0 -53
- package/src/utils/deps.test.ts +0 -31
- package/src/utils/deps.ts +0 -145
- package/src/utils/device.web.ts +0 -163
- package/src/utils/fuzzy-match.ts +0 -125
- package/src/utils/hostnames.ts +0 -43
- package/src/utils/hunk-header.ts +0 -17
- package/src/utils/id.ts +0 -9
- package/src/utils/logger.ts +0 -127
- package/src/utils/markdown.ts +0 -254
- package/src/utils/normalize-env-key.ts +0 -13
- package/src/utils/onboarding.ts +0 -279
- package/src/utils/prompts.ts +0 -176
- package/src/utils/run-commands.ts +0 -112
- package/src/utils/run-scripts.ts +0 -337
- package/src/utils/run-workspace-scripts.ts +0 -355
- package/src/utils/sanitize.test.ts +0 -149
- package/src/utils/sanitize.ts +0 -162
- package/src/utils/secrets.ts +0 -836
- package/src/utils/shell-escape.ts +0 -40
- package/src/utils/utf8.ts +0 -79
- package/src/utils/workspace-id.ts +0 -55
- package/src/utils/workspace-state.ts +0 -427
- package/src/version.generated.d.ts +0 -2
- package/todo-security.md +0 -92
- package/tsconfig.json +0 -29
- package/web/README.md +0 -73
- package/web/bun.lock +0 -675
- package/web/eslint.config.js +0 -23
- package/web/index.css +0 -249
- package/web/index.html +0 -16
- package/web/main.tsx +0 -10
- package/web/package.json +0 -39
- package/web/public/vite.svg +0 -1
- package/web/tsconfig.app.json +0 -35
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -39
- package/worker/bun.lock +0 -237
- package/worker/package.json +0 -22
- package/worker/schema.sql +0 -96
- package/worker/src/handlers/auth.ts +0 -451
- package/worker/src/handlers/subdomains.ts +0 -376
- package/worker/src/handlers/user.ts +0 -98
- package/worker/src/index.ts +0 -70
- package/worker/src/middleware/auth.ts +0 -152
- package/worker/src/services/cloudflare.ts +0 -609
- package/worker/src/types.ts +0 -96
- package/worker/tsconfig.json +0 -15
- package/worker/wrangler.toml +0 -26
|
@@ -1,1298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote session handler - processes browse and PTY commands
|
|
3
|
-
*
|
|
4
|
-
* Handles the encrypted messages between client and machine after X3DH handshake.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createFrame, openFrame } from "../tmux-lite/crypto/frames";
|
|
8
|
-
import { scanWorkspaces } from "./workspace-scanner";
|
|
9
|
-
import {
|
|
10
|
-
parseRemoteMessage,
|
|
11
|
-
serializeRemoteMessage,
|
|
12
|
-
type ClientToMachineMessage,
|
|
13
|
-
type MachineToClientMessage,
|
|
14
|
-
type SessionInfo,
|
|
15
|
-
} from "./protocol";
|
|
16
|
-
import type { SessionKeys, AccessType } from "../../types/identity.js";
|
|
17
|
-
|
|
18
|
-
// Import tmux-lite API for session management
|
|
19
|
-
import {
|
|
20
|
-
listSessions,
|
|
21
|
-
createSession,
|
|
22
|
-
killSession,
|
|
23
|
-
isServerRunning,
|
|
24
|
-
ensureServer,
|
|
25
|
-
getInbox,
|
|
26
|
-
clearInbox,
|
|
27
|
-
markInboxRead,
|
|
28
|
-
type Session,
|
|
29
|
-
} from "../tmux-lite/cli";
|
|
30
|
-
|
|
31
|
-
// Import project loading
|
|
32
|
-
import { listProjectSummaries } from "../../core/project-catalog";
|
|
33
|
-
|
|
34
|
-
// Import workspace operations
|
|
35
|
-
import { deleteWorkspaceCore } from "../../core/workspace";
|
|
36
|
-
import { prepareWorkspaceForSession } from "../../core/workspace-lifecycle";
|
|
37
|
-
|
|
38
|
-
// Import review operations
|
|
39
|
-
import { executeLocalReviewOperation } from "../../core/review-executor.js";
|
|
40
|
-
import type { ReviewOperation, ReviewResult } from "../../types/review.js";
|
|
41
|
-
import { getNotificationConfig, updateNotificationConfig } from "../../core/config";
|
|
42
|
-
import {
|
|
43
|
-
getBundleRefreshPlan,
|
|
44
|
-
applyBundleRefreshSubmission,
|
|
45
|
-
} from '../../core/bundle-refresh.js';
|
|
46
|
-
import { buildSessionName } from '../../session/session-name.js';
|
|
47
|
-
import { buildWorkspaceSessionHooks } from '../../session/workspace-shell-hooks.js';
|
|
48
|
-
import { matchesWorkspaceId, toCanonicalWorkspaceId } from '../../utils/workspace-id.js';
|
|
49
|
-
|
|
50
|
-
// Process & events imports
|
|
51
|
-
import { parseProcessSessionName } from "../processes/names.js";
|
|
52
|
-
import { readWorkspaceSnapshots } from "../events/reader.js";
|
|
53
|
-
import { resolveWorkspaceRef } from "../events/paths.js";
|
|
54
|
-
import { loadSavedEventFilters } from "../events/filters.js";
|
|
55
|
-
import { getProcessSpecs, startProcessInstance, stopProcessInstance } from "../processes/manager.js";
|
|
56
|
-
import { autostartProcesses } from "../processes/autostart.js";
|
|
57
|
-
import { startProcessScheduler } from "../processes/scheduler.js";
|
|
58
|
-
import {
|
|
59
|
-
loadProcessesConfigWithDiagnostics,
|
|
60
|
-
loadProcessesConfig,
|
|
61
|
-
getProcessDefinition,
|
|
62
|
-
} from "../processes/config.js";
|
|
63
|
-
import { normalizeProcessInstanceCount } from "../processes/instances.js";
|
|
64
|
-
import { readProjectConfig } from "../../core/config.js";
|
|
65
|
-
import { existsSync } from "fs";
|
|
66
|
-
|
|
67
|
-
import { logger } from "../../utils/logger.js";
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Session state for a connected client
|
|
71
|
-
*/
|
|
72
|
-
export type ClientState = "browsing" | "attached";
|
|
73
|
-
|
|
74
|
-
export interface RemoteClientSession {
|
|
75
|
-
connectionId: string;
|
|
76
|
-
state: ClientState;
|
|
77
|
-
sessionKeys: SessionKeys;
|
|
78
|
-
/** Access type granted to this client */
|
|
79
|
-
accessType?: AccessType;
|
|
80
|
-
/** For session-invite: the specific session ID access was granted to */
|
|
81
|
-
grantedSessionId?: string;
|
|
82
|
-
/** Attached tmux-lite session ID (set after attach_session) */
|
|
83
|
-
attachedSessionId?: string;
|
|
84
|
-
/** Path to tmux-lite session socket (set after attach_session) */
|
|
85
|
-
sessionSocketPath?: string;
|
|
86
|
-
/** When true, PTY writes from this client are blocked server-side */
|
|
87
|
-
viewOnly?: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ============================================================================
|
|
91
|
-
// Permission Helpers
|
|
92
|
-
// ============================================================================
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if access type grants management permission
|
|
96
|
-
*/
|
|
97
|
-
function canManage(accessType: AccessType | undefined): boolean {
|
|
98
|
-
return accessType === 'full';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Check if client can attach to a specific session
|
|
103
|
-
*/
|
|
104
|
-
function canAttachSession(
|
|
105
|
-
accessType: AccessType | undefined,
|
|
106
|
-
grantedSessionId: string | undefined,
|
|
107
|
-
targetSessionId: string
|
|
108
|
-
): boolean {
|
|
109
|
-
if (accessType === 'full') return true;
|
|
110
|
-
if (accessType === 'session-invite') {
|
|
111
|
-
return grantedSessionId === targetSessionId;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const MUTATING_REVIEW_OPERATIONS = new Set<ReviewOperation['op']>([
|
|
117
|
-
'create_thread',
|
|
118
|
-
'add_reply',
|
|
119
|
-
'update_thread',
|
|
120
|
-
'update_comment',
|
|
121
|
-
'delete_comment',
|
|
122
|
-
'import_github',
|
|
123
|
-
'push_github',
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
function isMutatingReviewOperation(operation: ReviewOperation): boolean {
|
|
127
|
-
return MUTATING_REVIEW_OPERATIONS.has(operation.op);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function normalizeWorkspaceIdToken(workspaceId: string): string {
|
|
131
|
-
return workspaceId.includes(':') ? workspaceId.split(':').pop() ?? workspaceId : workspaceId;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function matchesWorkspaceIdToken(parsedWorkspaceId: string, workspaceId: string): boolean {
|
|
135
|
-
return normalizeWorkspaceIdToken(parsedWorkspaceId) === normalizeWorkspaceIdToken(workspaceId);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface RemoteSessionHandlerOptions {
|
|
139
|
-
processHostDomain?: string;
|
|
140
|
-
onProcessesChanged?: (workspacePath: string) => void | Promise<void>;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Remote session handler
|
|
145
|
-
*/
|
|
146
|
-
export class RemoteSessionHandler {
|
|
147
|
-
private tmuxLiteAvailable = false;
|
|
148
|
-
private processSchedulers = new Map<string, NodeJS.Timer>();
|
|
149
|
-
private processHostDomain?: string;
|
|
150
|
-
private onProcessesChanged?: (workspacePath: string) => void | Promise<void>;
|
|
151
|
-
|
|
152
|
-
constructor(options: RemoteSessionHandlerOptions = {}) {
|
|
153
|
-
this.processHostDomain = options.processHostDomain;
|
|
154
|
-
this.onProcessesChanged = options.onProcessesChanged;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Initialize - check if tmux-lite is available
|
|
159
|
-
*/
|
|
160
|
-
async initialize(): Promise<void> {
|
|
161
|
-
try {
|
|
162
|
-
this.tmuxLiteAvailable = await isServerRunning();
|
|
163
|
-
if (!this.tmuxLiteAvailable) {
|
|
164
|
-
// Try to start the server
|
|
165
|
-
await ensureServer();
|
|
166
|
-
this.tmuxLiteAvailable = true;
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {
|
|
169
|
-
console.warn("[remote-session] tmux-lite not available:", e);
|
|
170
|
-
this.tmuxLiteAvailable = false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Handle an encrypted message from a client
|
|
176
|
-
*
|
|
177
|
-
* @param session - Client session info
|
|
178
|
-
* @param encryptedData - Encrypted frame data
|
|
179
|
-
* @param sendResponse - Callback to send encrypted response
|
|
180
|
-
*/
|
|
181
|
-
async handleMessage(
|
|
182
|
-
session: RemoteClientSession,
|
|
183
|
-
encryptedData: Uint8Array,
|
|
184
|
-
sendResponse: (data: Uint8Array) => void
|
|
185
|
-
): Promise<void> {
|
|
186
|
-
try {
|
|
187
|
-
// Decrypt the frame
|
|
188
|
-
const frame = await openFrame(encryptedData, session.sessionKeys.receiveKey);
|
|
189
|
-
if (!frame) {
|
|
190
|
-
console.error("[remote-session] Failed to decrypt frame");
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Parse as JSON message
|
|
195
|
-
const json = new TextDecoder().decode(frame.data);
|
|
196
|
-
const msg = parseRemoteMessage(json);
|
|
197
|
-
|
|
198
|
-
if (!msg) {
|
|
199
|
-
console.error("[remote-session] Failed to parse message");
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Handle based on message type
|
|
204
|
-
await this.processMessage(session, msg as ClientToMachineMessage, sendResponse);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
console.error("[remote-session] Error handling message:", e);
|
|
207
|
-
await this.sendError(session, sendResponse, "INTERNAL_ERROR", "Failed to process message");
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Process a client message
|
|
213
|
-
*/
|
|
214
|
-
private async processMessage(
|
|
215
|
-
session: RemoteClientSession,
|
|
216
|
-
msg: ClientToMachineMessage,
|
|
217
|
-
sendResponse: (data: Uint8Array) => void
|
|
218
|
-
): Promise<void> {
|
|
219
|
-
switch (msg.type) {
|
|
220
|
-
case "list_workspaces":
|
|
221
|
-
await this.handleListWorkspaces(session, sendResponse);
|
|
222
|
-
break;
|
|
223
|
-
|
|
224
|
-
case "list_sessions":
|
|
225
|
-
await this.handleListSessions(session, msg.workspaceId, sendResponse);
|
|
226
|
-
break;
|
|
227
|
-
|
|
228
|
-
case "attach_session":
|
|
229
|
-
// Permission check for attach_session is done in handleAttachSession
|
|
230
|
-
// because it depends on whether creating new session or attaching existing
|
|
231
|
-
await this.handleAttachSession(session, msg, sendResponse);
|
|
232
|
-
break;
|
|
233
|
-
|
|
234
|
-
// Note: resize, detach, and pty_input are handled in attached mode
|
|
235
|
-
// via client-session-manager using tmux-lite's SessionCtrl protocol,
|
|
236
|
-
// not through this JSON-RPC handler.
|
|
237
|
-
|
|
238
|
-
case "list_projects":
|
|
239
|
-
await this.handleListProjects(session, sendResponse);
|
|
240
|
-
break;
|
|
241
|
-
|
|
242
|
-
case "kill_session":
|
|
243
|
-
// Security: Requires management permission
|
|
244
|
-
if (!canManage(session.accessType)) {
|
|
245
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to kill sessions");
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
await this.handleKillSession(session, msg.sessionId, sendResponse);
|
|
249
|
-
break;
|
|
250
|
-
|
|
251
|
-
case "delete_workspace":
|
|
252
|
-
// Security: Requires management permission
|
|
253
|
-
if (!canManage(session.accessType)) {
|
|
254
|
-
const normalizedWorkspaceId = msg.workspaceId.startsWith(`${msg.projectName}:`)
|
|
255
|
-
? msg.workspaceId.slice(msg.projectName.length + 1)
|
|
256
|
-
: msg.workspaceId;
|
|
257
|
-
await this.sendError(
|
|
258
|
-
session,
|
|
259
|
-
sendResponse,
|
|
260
|
-
"PERMISSION_DENIED",
|
|
261
|
-
"Requires full access to delete workspaces",
|
|
262
|
-
{ workspaceId: `${msg.projectName}:${normalizedWorkspaceId}` }
|
|
263
|
-
);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
await this.handleDeleteWorkspace(
|
|
267
|
-
session,
|
|
268
|
-
msg.projectName,
|
|
269
|
-
msg.workspaceId,
|
|
270
|
-
msg.scriptPolicy,
|
|
271
|
-
sendResponse
|
|
272
|
-
);
|
|
273
|
-
break;
|
|
274
|
-
|
|
275
|
-
case "get_inbox":
|
|
276
|
-
await this.handleGetInbox(session, sendResponse);
|
|
277
|
-
break;
|
|
278
|
-
|
|
279
|
-
case "clear_inbox":
|
|
280
|
-
await this.handleClearInbox(session, msg.id, sendResponse);
|
|
281
|
-
break;
|
|
282
|
-
|
|
283
|
-
case "mark_inbox_read":
|
|
284
|
-
await this.handleMarkInboxRead(session, msg.id, sendResponse);
|
|
285
|
-
break;
|
|
286
|
-
|
|
287
|
-
case "get_notification_config":
|
|
288
|
-
await this.handleGetNotificationConfig(session, sendResponse);
|
|
289
|
-
break;
|
|
290
|
-
|
|
291
|
-
case "update_notification_config":
|
|
292
|
-
await this.handleUpdateNotificationConfig(session, msg.config, sendResponse);
|
|
293
|
-
break;
|
|
294
|
-
|
|
295
|
-
case 'get_bundle_refresh_plan':
|
|
296
|
-
if (!canManage(session.accessType)) {
|
|
297
|
-
await this.sendError(
|
|
298
|
-
session,
|
|
299
|
-
sendResponse,
|
|
300
|
-
'PERMISSION_DENIED',
|
|
301
|
-
'Requires full access to inspect bundle refresh requirements'
|
|
302
|
-
);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await this.handleGetBundleRefreshPlan(
|
|
306
|
-
session,
|
|
307
|
-
msg.projectName,
|
|
308
|
-
msg.workspaceId,
|
|
309
|
-
sendResponse
|
|
310
|
-
);
|
|
311
|
-
break;
|
|
312
|
-
|
|
313
|
-
case 'apply_bundle_refresh':
|
|
314
|
-
if (!canManage(session.accessType)) {
|
|
315
|
-
await this.sendError(
|
|
316
|
-
session,
|
|
317
|
-
sendResponse,
|
|
318
|
-
'PERMISSION_DENIED',
|
|
319
|
-
'Requires full access to apply bundle refresh'
|
|
320
|
-
);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
await this.handleApplyBundleRefresh(
|
|
324
|
-
session,
|
|
325
|
-
msg.projectName,
|
|
326
|
-
msg.workspaceId,
|
|
327
|
-
msg.submission,
|
|
328
|
-
sendResponse
|
|
329
|
-
);
|
|
330
|
-
break;
|
|
331
|
-
|
|
332
|
-
case 'review_request':
|
|
333
|
-
await this.handleReviewRequest(
|
|
334
|
-
session,
|
|
335
|
-
msg.requestId,
|
|
336
|
-
msg.operation,
|
|
337
|
-
sendResponse
|
|
338
|
-
);
|
|
339
|
-
break;
|
|
340
|
-
|
|
341
|
-
case "get_events":
|
|
342
|
-
await this.handleGetEvents(
|
|
343
|
-
session,
|
|
344
|
-
msg.workspacePath,
|
|
345
|
-
msg.processName,
|
|
346
|
-
undefined,
|
|
347
|
-
msg.filter,
|
|
348
|
-
msg.limit,
|
|
349
|
-
msg.sinceMs,
|
|
350
|
-
sendResponse
|
|
351
|
-
);
|
|
352
|
-
break;
|
|
353
|
-
|
|
354
|
-
case "start_process":
|
|
355
|
-
if (!canManage(session.accessType)) {
|
|
356
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to start processes");
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
await this.handleStartProcess(session, msg.workspaceId, msg.processName, msg.instance, sendResponse);
|
|
360
|
-
break;
|
|
361
|
-
|
|
362
|
-
case "stop_process":
|
|
363
|
-
if (!canManage(session.accessType)) {
|
|
364
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to stop processes");
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
await this.handleStopProcess(session, msg.workspaceId, msg.processName, sendResponse);
|
|
368
|
-
break;
|
|
369
|
-
|
|
370
|
-
default: {
|
|
371
|
-
// Exhaustiveness check - log unknown message types
|
|
372
|
-
const unknownMsg = msg as { type: string };
|
|
373
|
-
console.warn("[remote-session] Unknown message type:", unknownMsg.type);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Handle list_workspaces request
|
|
380
|
-
*/
|
|
381
|
-
private async handleListWorkspaces(
|
|
382
|
-
session: RemoteClientSession,
|
|
383
|
-
sendResponse: (data: Uint8Array) => void
|
|
384
|
-
): Promise<void> {
|
|
385
|
-
const workspaces = await scanWorkspaces();
|
|
386
|
-
|
|
387
|
-
// Add session counts from tmux-lite
|
|
388
|
-
if (this.tmuxLiteAvailable) {
|
|
389
|
-
try {
|
|
390
|
-
const sessions = await listSessions();
|
|
391
|
-
for (const workspace of workspaces) {
|
|
392
|
-
// Count sessions for this workspace by matching cwd.
|
|
393
|
-
// Note: Session cwd is set once at creation time and does NOT change
|
|
394
|
-
// as users navigate within the shell. This is intentional - we want to
|
|
395
|
-
// show sessions that were *created for* this workspace.
|
|
396
|
-
const workspaceSessions = sessions.filter(s => s.cwd === workspace.path);
|
|
397
|
-
workspace.sessionCount = workspaceSessions.length;
|
|
398
|
-
|
|
399
|
-
// Load process config for the workspace
|
|
400
|
-
const processConfig = loadProcessesConfigWithDiagnostics(workspace.path);
|
|
401
|
-
workspace.processes = processConfig.config.processes.map((process) => ({
|
|
402
|
-
name: process.name,
|
|
403
|
-
instances: process.instances,
|
|
404
|
-
ports: process.ports,
|
|
405
|
-
}));
|
|
406
|
-
workspace.processConfigError = processConfig.error ?? undefined;
|
|
407
|
-
}
|
|
408
|
-
} catch {
|
|
409
|
-
// Ignore errors - just use 0 session counts
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Attach serve domain if configured
|
|
414
|
-
if (this.processHostDomain) {
|
|
415
|
-
for (const workspace of workspaces) {
|
|
416
|
-
workspace.serveDomain = this.processHostDomain;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
await this.sendMessage(session, sendResponse, {
|
|
421
|
-
type: "workspace_list",
|
|
422
|
-
workspaces: workspaces.map((workspace) => ({
|
|
423
|
-
...workspace,
|
|
424
|
-
id: toCanonicalWorkspaceId(workspace),
|
|
425
|
-
})),
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Handle list_sessions request
|
|
431
|
-
*/
|
|
432
|
-
private async handleListSessions(
|
|
433
|
-
session: RemoteClientSession,
|
|
434
|
-
workspaceId: string | undefined,
|
|
435
|
-
sendResponse: (data: Uint8Array) => void
|
|
436
|
-
): Promise<void> {
|
|
437
|
-
let sessions: SessionInfo[] = [];
|
|
438
|
-
|
|
439
|
-
if (this.tmuxLiteAvailable) {
|
|
440
|
-
try {
|
|
441
|
-
const allSessions = await listSessions();
|
|
442
|
-
const workspaces = await scanWorkspaces();
|
|
443
|
-
|
|
444
|
-
// Build a map of workspace path -> workspace info
|
|
445
|
-
const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
|
|
446
|
-
|
|
447
|
-
sessions = allSessions
|
|
448
|
-
.filter(s => {
|
|
449
|
-
if (!workspaceId) return true;
|
|
450
|
-
// Try process session name first
|
|
451
|
-
const parsed = parseProcessSessionName(s.name);
|
|
452
|
-
if (parsed) return matchesWorkspaceIdToken(parsed.workspaceId, workspaceId);
|
|
453
|
-
// Fall back to cwd matching
|
|
454
|
-
const ws = workspacePathMap.get(s.cwd);
|
|
455
|
-
return ws ? matchesWorkspaceId(ws, workspaceId) : false;
|
|
456
|
-
})
|
|
457
|
-
.map(s => {
|
|
458
|
-
const parsed = parseProcessSessionName(s.name);
|
|
459
|
-
let ws = workspacePathMap.get(s.cwd);
|
|
460
|
-
if (!ws && parsed) {
|
|
461
|
-
ws = workspaces.find(workspace => matchesWorkspaceId(workspace, parsed.workspaceId));
|
|
462
|
-
}
|
|
463
|
-
if (!ws) {
|
|
464
|
-
ws = workspaces.find(workspace => s.cwd.startsWith(workspace.path));
|
|
465
|
-
}
|
|
466
|
-
return {
|
|
467
|
-
id: s.id,
|
|
468
|
-
name: s.name,
|
|
469
|
-
workspaceId: ws ? toCanonicalWorkspaceId(ws) : (parsed?.workspaceId ?? "unknown"),
|
|
470
|
-
attached: s.attached,
|
|
471
|
-
createdAt: s.createdAt,
|
|
472
|
-
processTitle: s.processTitle,
|
|
473
|
-
exitCode: s.exitCode,
|
|
474
|
-
processName: (s as any).processName ?? parsed?.processName,
|
|
475
|
-
processInstance: (s as any).processInstance ?? parsed?.instance,
|
|
476
|
-
};
|
|
477
|
-
});
|
|
478
|
-
} catch (e) {
|
|
479
|
-
console.error("[remote-session] Failed to list sessions:", e);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
await this.sendMessage(session, sendResponse, {
|
|
484
|
-
type: "session_list",
|
|
485
|
-
sessions,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Handle attach_session request
|
|
491
|
-
*/
|
|
492
|
-
private async handleAttachSession(
|
|
493
|
-
session: RemoteClientSession,
|
|
494
|
-
msg: {
|
|
495
|
-
sessionId?: string;
|
|
496
|
-
workspaceId?: string;
|
|
497
|
-
sessionName?: string;
|
|
498
|
-
cols?: number;
|
|
499
|
-
rows?: number;
|
|
500
|
-
scriptPolicy?: 'auto' | 'skip';
|
|
501
|
-
viewOnly?: boolean;
|
|
502
|
-
command?: string;
|
|
503
|
-
args?: string[];
|
|
504
|
-
env?: Record<string, string>;
|
|
505
|
-
},
|
|
506
|
-
sendResponse: (data: Uint8Array) => void
|
|
507
|
-
): Promise<void> {
|
|
508
|
-
console.log("[remote-session] handleAttachSession:", JSON.stringify(msg));
|
|
509
|
-
|
|
510
|
-
if (!this.tmuxLiteAvailable) {
|
|
511
|
-
await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
let targetSession: Session | null = null;
|
|
517
|
-
|
|
518
|
-
// If no session ID, create new session in workspace
|
|
519
|
-
if (!msg.sessionId && msg.workspaceId) {
|
|
520
|
-
const requestedWorkspaceId = msg.workspaceId;
|
|
521
|
-
// Security: Creating new sessions requires full/manage access
|
|
522
|
-
if (!canManage(session.accessType)) {
|
|
523
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to create sessions");
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Find the workspace path
|
|
528
|
-
const workspaces = await scanWorkspaces();
|
|
529
|
-
const workspace = workspaces.find((w) => matchesWorkspaceId(w, requestedWorkspaceId));
|
|
530
|
-
|
|
531
|
-
if (!workspace) {
|
|
532
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const sessions = await listSessions();
|
|
537
|
-
const sessionName = buildSessionName({
|
|
538
|
-
projectName: workspace.projectName,
|
|
539
|
-
workspaceName: workspace.id,
|
|
540
|
-
requestedName: msg.sessionName,
|
|
541
|
-
sessions,
|
|
542
|
-
});
|
|
543
|
-
console.log(`[remote-session] Selected session name: ${sessionName}`);
|
|
544
|
-
|
|
545
|
-
if (msg.command) {
|
|
546
|
-
// Skip workspace scripts when a custom command is specified
|
|
547
|
-
targetSession = await createSession(sessionName, workspace.path, {
|
|
548
|
-
command: msg.command,
|
|
549
|
-
args: msg.args,
|
|
550
|
-
env: msg.env,
|
|
551
|
-
});
|
|
552
|
-
console.log(`[remote-session] Created session (custom cmd): ${targetSession.name} (id: ${targetSession.id})`);
|
|
553
|
-
} else {
|
|
554
|
-
// Run setup/select scripts for the workspace with output streaming.
|
|
555
|
-
console.log(`[remote-session] Running workspace scripts for: ${workspace.id}`);
|
|
556
|
-
|
|
557
|
-
// Track current phase for script_output messages
|
|
558
|
-
let currentPhase: 'pre' | 'setup' | 'select' = 'pre';
|
|
559
|
-
|
|
560
|
-
const scriptResult = await prepareWorkspaceForSession({
|
|
561
|
-
projectName: workspace.projectName,
|
|
562
|
-
workspacePath: workspace.path,
|
|
563
|
-
workspaceName: workspace.id,
|
|
564
|
-
interactiveScripts: false,
|
|
565
|
-
bundleMode: 'error-if-changed',
|
|
566
|
-
scriptPolicy: msg.scriptPolicy ?? 'auto',
|
|
567
|
-
onOutput: (data) => {
|
|
568
|
-
void this.sendMessage(session, sendResponse, {
|
|
569
|
-
type: 'script_output',
|
|
570
|
-
phase: currentPhase,
|
|
571
|
-
data: data.toString('base64'),
|
|
572
|
-
}).catch((error) => {
|
|
573
|
-
logger.debug(`[remote-session] Failed to stream script output: ${error instanceof Error ? error.message : String(error)}`);
|
|
574
|
-
});
|
|
575
|
-
},
|
|
576
|
-
onPhaseStart: (phase) => {
|
|
577
|
-
currentPhase = phase;
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
if (!scriptResult.success) {
|
|
582
|
-
console.error(`[remote-session] ${scriptResult.phase} scripts failed:`, scriptResult.error);
|
|
583
|
-
await this.sendMessage(session, sendResponse, {
|
|
584
|
-
type: 'script_output',
|
|
585
|
-
phase: scriptResult.phase,
|
|
586
|
-
data: '',
|
|
587
|
-
done: true,
|
|
588
|
-
error: scriptResult.error,
|
|
589
|
-
});
|
|
590
|
-
const code =
|
|
591
|
-
'bundleNeedsRefresh' in scriptResult && scriptResult.bundleNeedsRefresh
|
|
592
|
-
? 'BUNDLE_REFRESH_REQUIRED'
|
|
593
|
-
: scriptResult.phase === 'setup'
|
|
594
|
-
? 'SETUP_SCRIPT_FAILED'
|
|
595
|
-
: scriptResult.phase === 'select'
|
|
596
|
-
? 'SELECT_SCRIPT_FAILED'
|
|
597
|
-
: 'PRE_SCRIPT_FAILED';
|
|
598
|
-
await this.sendError(
|
|
599
|
-
session,
|
|
600
|
-
sendResponse,
|
|
601
|
-
code,
|
|
602
|
-
`Workspace scripts failed during ${scriptResult.phase} phase: ${scriptResult.error}`
|
|
603
|
-
);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Send final script_output indicating success
|
|
608
|
-
await this.sendMessage(session, sendResponse, {
|
|
609
|
-
type: 'script_output',
|
|
610
|
-
phase: currentPhase,
|
|
611
|
-
data: '',
|
|
612
|
-
done: true,
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
targetSession = await createSession(sessionName, workspace.path, {
|
|
616
|
-
hooks: buildWorkspaceSessionHooks(workspace.projectName, workspace.id),
|
|
617
|
-
});
|
|
618
|
-
console.log(`[remote-session] Created session: ${targetSession.name} (id: ${targetSession.id})`);
|
|
619
|
-
}
|
|
620
|
-
} else if (msg.sessionId) {
|
|
621
|
-
// Security: Check if client can attach to this session
|
|
622
|
-
if (!canAttachSession(session.accessType, session.grantedSessionId, msg.sessionId)) {
|
|
623
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Not authorized to attach to this session");
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Find existing session
|
|
628
|
-
const sessions = await listSessions();
|
|
629
|
-
targetSession = sessions.find(s => s.id === msg.sessionId) ?? null;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
if (!targetSession) {
|
|
633
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Session not found");
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
session.state = "attached";
|
|
638
|
-
session.attachedSessionId = targetSession.id;
|
|
639
|
-
session.sessionSocketPath = targetSession.socketPath;
|
|
640
|
-
session.viewOnly = msg.viewOnly ?? false;
|
|
641
|
-
|
|
642
|
-
// Send confirmation - ClientSessionManager will connect to the socket
|
|
643
|
-
await this.sendMessage(session, sendResponse, {
|
|
644
|
-
type: "attached",
|
|
645
|
-
sessionId: targetSession.id,
|
|
646
|
-
sessionName: targetSession.name,
|
|
647
|
-
cols: msg.cols ?? 80,
|
|
648
|
-
rows: msg.rows ?? 24,
|
|
649
|
-
});
|
|
650
|
-
} catch (e) {
|
|
651
|
-
console.error("[remote-session] Failed to attach session:", e);
|
|
652
|
-
const detail = e instanceof Error ? e.message : String(e);
|
|
653
|
-
await this.sendError(session, sendResponse, "ATTACH_FAILED", `Failed to attach to session: ${detail}`);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Handle list_projects request
|
|
659
|
-
*/
|
|
660
|
-
private async handleListProjects(
|
|
661
|
-
session: RemoteClientSession,
|
|
662
|
-
sendResponse: (data: Uint8Array) => void
|
|
663
|
-
): Promise<void> {
|
|
664
|
-
try {
|
|
665
|
-
const projects = listProjectSummaries();
|
|
666
|
-
await this.sendMessage(session, sendResponse, {
|
|
667
|
-
type: "project_list",
|
|
668
|
-
projects: projects.map(p => ({
|
|
669
|
-
name: p.name,
|
|
670
|
-
repository: p.repository,
|
|
671
|
-
workspaceCount: p.workspaceCount,
|
|
672
|
-
isCurrent: p.isCurrent,
|
|
673
|
-
})),
|
|
674
|
-
});
|
|
675
|
-
} catch (e) {
|
|
676
|
-
console.error("[remote-session] Failed to list projects:", e);
|
|
677
|
-
await this.sendError(session, sendResponse, "LIST_FAILED", "Failed to list projects");
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Handle kill_session request
|
|
683
|
-
*/
|
|
684
|
-
private async handleKillSession(
|
|
685
|
-
session: RemoteClientSession,
|
|
686
|
-
sessionId: string,
|
|
687
|
-
sendResponse: (data: Uint8Array) => void
|
|
688
|
-
): Promise<void> {
|
|
689
|
-
if (!this.tmuxLiteAvailable) {
|
|
690
|
-
await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
// Look up the session's workspaceId before killing
|
|
696
|
-
const sessions = await listSessions();
|
|
697
|
-
const workspaces = await scanWorkspaces();
|
|
698
|
-
const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
|
|
699
|
-
const targetSession = sessions.find(s => s.id === sessionId);
|
|
700
|
-
const workspace = targetSession ? workspacePathMap.get(targetSession.cwd) : undefined;
|
|
701
|
-
const workspaceId = workspace ? toCanonicalWorkspaceId(workspace) : "unknown";
|
|
702
|
-
|
|
703
|
-
await killSession(sessionId);
|
|
704
|
-
// Wait a bit for the server to process the kill
|
|
705
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
706
|
-
await this.sendMessage(session, sendResponse, {
|
|
707
|
-
type: "session_killed",
|
|
708
|
-
sessionId,
|
|
709
|
-
workspaceId,
|
|
710
|
-
});
|
|
711
|
-
} catch (e) {
|
|
712
|
-
console.error("[remote-session] Failed to kill session:", e);
|
|
713
|
-
await this.sendError(session, sendResponse, "KILL_FAILED", "Failed to kill session");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Handle delete_workspace request
|
|
719
|
-
*/
|
|
720
|
-
private async handleDeleteWorkspace(
|
|
721
|
-
session: RemoteClientSession,
|
|
722
|
-
projectName: string,
|
|
723
|
-
workspaceId: string,
|
|
724
|
-
scriptPolicy: 'auto' | 'skip' | undefined,
|
|
725
|
-
sendResponse: (data: Uint8Array) => void
|
|
726
|
-
): Promise<void> {
|
|
727
|
-
const normalizedWorkspaceId = workspaceId.startsWith(`${projectName}:`)
|
|
728
|
-
? workspaceId.slice(projectName.length + 1)
|
|
729
|
-
: workspaceId;
|
|
730
|
-
const canonicalWorkspaceId = `${projectName}:${normalizedWorkspaceId}`;
|
|
731
|
-
let emittedDone = false;
|
|
732
|
-
const emitDone = async (error?: string) => {
|
|
733
|
-
await this.sendMessage(session, sendResponse, {
|
|
734
|
-
type: 'script_output',
|
|
735
|
-
phase: 'remove',
|
|
736
|
-
data: '',
|
|
737
|
-
done: true,
|
|
738
|
-
error,
|
|
739
|
-
});
|
|
740
|
-
emittedDone = true;
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
try {
|
|
744
|
-
const result = await deleteWorkspaceCore(projectName, normalizedWorkspaceId, {
|
|
745
|
-
nonInteractive: true, // Remote context - scripts can't prompt for input
|
|
746
|
-
removeScriptPolicy: scriptPolicy === 'skip' ? 'skip' : 'enforce',
|
|
747
|
-
onScriptOutput: (data) => {
|
|
748
|
-
void this.sendMessage(session, sendResponse, {
|
|
749
|
-
type: 'script_output',
|
|
750
|
-
phase: 'remove',
|
|
751
|
-
data: data.toString('base64'),
|
|
752
|
-
}).catch((error) => {
|
|
753
|
-
logger.debug(`[remote-session] Failed to stream remove script output: ${error instanceof Error ? error.message : String(error)}`);
|
|
754
|
-
});
|
|
755
|
-
},
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
if (!result.success) {
|
|
759
|
-
const message = result.error || 'Failed to delete workspace';
|
|
760
|
-
await emitDone(message);
|
|
761
|
-
|
|
762
|
-
if (result.errorCode === 'REMOVE_SCRIPT_FAILED') {
|
|
763
|
-
await this.sendError(session, sendResponse, 'REMOVE_SCRIPT_FAILED', message, {
|
|
764
|
-
workspaceId: canonicalWorkspaceId,
|
|
765
|
-
});
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const errorCode = result.errorCode === 'WORKSPACE_NOT_FOUND' || message.includes('not exist')
|
|
770
|
-
? 'NOT_FOUND'
|
|
771
|
-
: 'DELETE_FAILED';
|
|
772
|
-
await this.sendError(session, sendResponse, errorCode, message, {
|
|
773
|
-
workspaceId: canonicalWorkspaceId,
|
|
774
|
-
});
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
await emitDone();
|
|
779
|
-
|
|
780
|
-
await this.sendMessage(session, sendResponse, {
|
|
781
|
-
type: "workspace_deleted",
|
|
782
|
-
workspaceId: canonicalWorkspaceId,
|
|
783
|
-
});
|
|
784
|
-
} catch (e) {
|
|
785
|
-
console.error("[remote-session] Failed to delete workspace:", e);
|
|
786
|
-
if (!emittedDone) {
|
|
787
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
788
|
-
await emitDone(message);
|
|
789
|
-
}
|
|
790
|
-
await this.sendError(session, sendResponse, "DELETE_FAILED", "Failed to delete workspace", {
|
|
791
|
-
workspaceId: canonicalWorkspaceId,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Handle get_inbox request
|
|
798
|
-
* Returns unread count bounded by active sessions (one per session max).
|
|
799
|
-
*/
|
|
800
|
-
private async handleGetInbox(
|
|
801
|
-
session: RemoteClientSession,
|
|
802
|
-
sendResponse: (data: Uint8Array) => void
|
|
803
|
-
): Promise<void> {
|
|
804
|
-
try {
|
|
805
|
-
const [items, activeSessions] = await Promise.all([
|
|
806
|
-
getInbox(),
|
|
807
|
-
listSessions(),
|
|
808
|
-
]);
|
|
809
|
-
|
|
810
|
-
// Build a set of active session IDs
|
|
811
|
-
const activeSessionIds = new Set(activeSessions.map(s => s.id));
|
|
812
|
-
|
|
813
|
-
// Count unique sessions that have unread items AND are still active
|
|
814
|
-
const activeSessionsWithUnread = new Set<string>();
|
|
815
|
-
for (const item of items) {
|
|
816
|
-
if (!item.read && activeSessionIds.has(item.sessionId)) {
|
|
817
|
-
activeSessionsWithUnread.add(item.sessionId);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
await this.sendMessage(session, sendResponse, {
|
|
822
|
-
type: "inbox_list",
|
|
823
|
-
items,
|
|
824
|
-
unreadCount: activeSessionsWithUnread.size,
|
|
825
|
-
});
|
|
826
|
-
} catch (e) {
|
|
827
|
-
console.error("[remote-session] Failed to get inbox:", e);
|
|
828
|
-
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to get inbox");
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Handle clear_inbox request
|
|
834
|
-
*/
|
|
835
|
-
private async handleClearInbox(
|
|
836
|
-
session: RemoteClientSession,
|
|
837
|
-
id: string | undefined,
|
|
838
|
-
sendResponse: (data: Uint8Array) => void
|
|
839
|
-
): Promise<void> {
|
|
840
|
-
try {
|
|
841
|
-
await clearInbox(id);
|
|
842
|
-
await this.sendMessage(session, sendResponse, {
|
|
843
|
-
type: "inbox_cleared",
|
|
844
|
-
id,
|
|
845
|
-
});
|
|
846
|
-
} catch (e) {
|
|
847
|
-
console.error("[remote-session] Failed to clear inbox:", e);
|
|
848
|
-
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to clear inbox");
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Handle mark_inbox_read request
|
|
854
|
-
*/
|
|
855
|
-
private async handleMarkInboxRead(
|
|
856
|
-
session: RemoteClientSession,
|
|
857
|
-
id: string,
|
|
858
|
-
sendResponse: (data: Uint8Array) => void
|
|
859
|
-
): Promise<void> {
|
|
860
|
-
try {
|
|
861
|
-
await markInboxRead(id);
|
|
862
|
-
await this.sendMessage(session, sendResponse, {
|
|
863
|
-
type: "inbox_marked_read",
|
|
864
|
-
id,
|
|
865
|
-
});
|
|
866
|
-
} catch (e) {
|
|
867
|
-
console.error("[remote-session] Failed to mark inbox read:", e);
|
|
868
|
-
await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to mark inbox item as read");
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Handle get_notification_config request
|
|
874
|
-
*/
|
|
875
|
-
private async handleGetNotificationConfig(
|
|
876
|
-
session: RemoteClientSession,
|
|
877
|
-
sendResponse: (data: Uint8Array) => void
|
|
878
|
-
): Promise<void> {
|
|
879
|
-
try {
|
|
880
|
-
const config = getNotificationConfig();
|
|
881
|
-
await this.sendMessage(session, sendResponse, {
|
|
882
|
-
type: "notification_config",
|
|
883
|
-
config,
|
|
884
|
-
});
|
|
885
|
-
} catch (e) {
|
|
886
|
-
console.error("[remote-session] Failed to read notification config:", e);
|
|
887
|
-
await this.sendError(session, sendResponse, "CONFIG_FAILED", "Failed to read notification config");
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Handle update_notification_config request
|
|
893
|
-
*/
|
|
894
|
-
private async handleUpdateNotificationConfig(
|
|
895
|
-
session: RemoteClientSession,
|
|
896
|
-
config: import("../../notifications/types.js").NotificationConfig,
|
|
897
|
-
sendResponse: (data: Uint8Array) => void
|
|
898
|
-
): Promise<void> {
|
|
899
|
-
// Security: only full-access clients can change machine preferences.
|
|
900
|
-
if (!canManage(session.accessType)) {
|
|
901
|
-
await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to update settings");
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
try {
|
|
906
|
-
const updated = updateNotificationConfig(config);
|
|
907
|
-
await this.sendMessage(session, sendResponse, {
|
|
908
|
-
type: "notification_config_updated",
|
|
909
|
-
config: updated,
|
|
910
|
-
});
|
|
911
|
-
} catch (e) {
|
|
912
|
-
console.error("[remote-session] Failed to update notification config:", e);
|
|
913
|
-
await this.sendError(session, sendResponse, "CONFIG_FAILED", "Failed to update notification config");
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
private async handleGetBundleRefreshPlan(
|
|
918
|
-
session: RemoteClientSession,
|
|
919
|
-
projectName: string,
|
|
920
|
-
workspaceId: string,
|
|
921
|
-
sendResponse: (data: Uint8Array) => void
|
|
922
|
-
): Promise<void> {
|
|
923
|
-
try {
|
|
924
|
-
const workspace = await this.resolveWorkspace(projectName, workspaceId);
|
|
925
|
-
const plan = await getBundleRefreshPlan(projectName, workspace.path, `${projectName}:${workspace.id}`);
|
|
926
|
-
await this.sendMessage(session, sendResponse, {
|
|
927
|
-
type: 'bundle_refresh_plan',
|
|
928
|
-
plan,
|
|
929
|
-
});
|
|
930
|
-
} catch (error) {
|
|
931
|
-
const message = error instanceof Error ? error.message : 'Failed to build bundle refresh plan';
|
|
932
|
-
await this.sendError(session, sendResponse, 'BUNDLE_REFRESH_PLAN_FAILED', message);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
private async handleApplyBundleRefresh(
|
|
937
|
-
session: RemoteClientSession,
|
|
938
|
-
projectName: string,
|
|
939
|
-
workspaceId: string,
|
|
940
|
-
submission: import('../../types/bundle-refresh.js').BundleRefreshSubmission,
|
|
941
|
-
sendResponse: (data: Uint8Array) => void
|
|
942
|
-
): Promise<void> {
|
|
943
|
-
try {
|
|
944
|
-
const workspace = await this.resolveWorkspace(projectName, workspaceId);
|
|
945
|
-
await applyBundleRefreshSubmission(projectName, workspace.path, submission);
|
|
946
|
-
await this.sendMessage(session, sendResponse, {
|
|
947
|
-
type: 'bundle_refresh_applied',
|
|
948
|
-
projectName,
|
|
949
|
-
workspaceId: `${projectName}:${workspace.id}`,
|
|
950
|
-
});
|
|
951
|
-
} catch (error) {
|
|
952
|
-
const message = error instanceof Error ? error.message : 'Failed to apply bundle refresh';
|
|
953
|
-
await this.sendError(session, sendResponse, 'BUNDLE_REFRESH_APPLY_FAILED', message);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// ============================================================================
|
|
958
|
-
// Review Request Handling
|
|
959
|
-
// ============================================================================
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* Handle a review_request message by dispatching to the appropriate
|
|
963
|
-
* review operation and responding with a review_response.
|
|
964
|
-
*/
|
|
965
|
-
private async handleReviewRequest(
|
|
966
|
-
session: RemoteClientSession,
|
|
967
|
-
requestId: string,
|
|
968
|
-
operation: ReviewOperation,
|
|
969
|
-
sendResponse: (data: Uint8Array) => void
|
|
970
|
-
): Promise<void> {
|
|
971
|
-
if (isMutatingReviewOperation(operation) && !canManage(session.accessType)) {
|
|
972
|
-
await this.sendMessage(session, sendResponse, {
|
|
973
|
-
type: 'review_response',
|
|
974
|
-
requestId,
|
|
975
|
-
error: {
|
|
976
|
-
code: 'PERMISSION_DENIED',
|
|
977
|
-
message: 'Requires full access to perform this review operation',
|
|
978
|
-
},
|
|
979
|
-
});
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
try {
|
|
984
|
-
const result = await this.executeReviewOperation(operation);
|
|
985
|
-
await this.sendMessage(session, sendResponse, {
|
|
986
|
-
type: 'review_response',
|
|
987
|
-
requestId,
|
|
988
|
-
result,
|
|
989
|
-
});
|
|
990
|
-
} catch (error) {
|
|
991
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
992
|
-
await this.sendMessage(session, sendResponse, {
|
|
993
|
-
type: 'review_response',
|
|
994
|
-
requestId,
|
|
995
|
-
error: { code: 'REVIEW_ERROR', message },
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Delegate to the shared executeLocalReviewOperation from review-executor.ts.
|
|
1002
|
-
* This is the single authoritative implementation used by both the remote
|
|
1003
|
-
* session handler and the local session backend.
|
|
1004
|
-
*/
|
|
1005
|
-
private async executeReviewOperation(operation: ReviewOperation): Promise<ReviewResult> {
|
|
1006
|
-
return executeLocalReviewOperation(operation, scanWorkspaces);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
private async resolveWorkspace(
|
|
1010
|
-
projectName: string,
|
|
1011
|
-
workspaceId: string
|
|
1012
|
-
): Promise<{ id: string; path: string }> {
|
|
1013
|
-
const normalizedWorkspaceId = workspaceId.startsWith(`${projectName}:`)
|
|
1014
|
-
? workspaceId.slice(projectName.length + 1)
|
|
1015
|
-
: workspaceId;
|
|
1016
|
-
|
|
1017
|
-
const workspaces = await scanWorkspaces();
|
|
1018
|
-
const workspace = workspaces.find(
|
|
1019
|
-
(item) => item.projectName === projectName && item.id === normalizedWorkspaceId
|
|
1020
|
-
);
|
|
1021
|
-
|
|
1022
|
-
if (!workspace) {
|
|
1023
|
-
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
return {
|
|
1027
|
-
id: workspace.id,
|
|
1028
|
-
path: workspace.path,
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
/**
|
|
1033
|
-
* Send an encrypted message to client
|
|
1034
|
-
*/
|
|
1035
|
-
private async sendMessage(
|
|
1036
|
-
session: RemoteClientSession,
|
|
1037
|
-
sendResponse: (data: Uint8Array) => void,
|
|
1038
|
-
msg: MachineToClientMessage
|
|
1039
|
-
): Promise<void> {
|
|
1040
|
-
const json = serializeRemoteMessage(msg);
|
|
1041
|
-
const data = new TextEncoder().encode(json);
|
|
1042
|
-
const frame = await createFrame(0, data, session.sessionKeys.sendKey);
|
|
1043
|
-
sendResponse(frame);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Send an error message to client
|
|
1048
|
-
*/
|
|
1049
|
-
private async sendError(
|
|
1050
|
-
session: RemoteClientSession,
|
|
1051
|
-
sendResponse: (data: Uint8Array) => void,
|
|
1052
|
-
code: string,
|
|
1053
|
-
message: string,
|
|
1054
|
-
options?: { workspaceId?: string }
|
|
1055
|
-
): Promise<void> {
|
|
1056
|
-
await this.sendMessage(session, sendResponse, {
|
|
1057
|
-
type: "error",
|
|
1058
|
-
code,
|
|
1059
|
-
message,
|
|
1060
|
-
workspaceId: options?.workspaceId,
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Handle get_events request
|
|
1066
|
-
*/
|
|
1067
|
-
private async handleGetEvents(
|
|
1068
|
-
session: RemoteClientSession,
|
|
1069
|
-
workspacePath: string,
|
|
1070
|
-
processName: string | undefined,
|
|
1071
|
-
_processInstance: number | undefined,
|
|
1072
|
-
filter: import("../../types/events.js").WideEventFilter | undefined,
|
|
1073
|
-
limit: number | undefined,
|
|
1074
|
-
sinceMs: number | undefined,
|
|
1075
|
-
sendResponse: (data: Uint8Array) => void
|
|
1076
|
-
): Promise<void> {
|
|
1077
|
-
try {
|
|
1078
|
-
const workspaceRef = resolveWorkspaceRef(workspacePath);
|
|
1079
|
-
if (!workspaceRef || !existsSync(workspaceRef.workspacePath)) {
|
|
1080
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const savedEventFilters = loadSavedEventFilters(workspaceRef.workspacePath);
|
|
1085
|
-
|
|
1086
|
-
const projectConfig = readProjectConfig(workspaceRef.projectName);
|
|
1087
|
-
const snapshots = readWorkspaceSnapshots(workspaceRef.workspacePath, {
|
|
1088
|
-
maxBytes: projectConfig.events?.snapshotCacheMaxBytes,
|
|
1089
|
-
maxTimeline: projectConfig.events?.maxTimeline,
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
const resolvedFilter = { ...filter };
|
|
1093
|
-
if (processName && !resolvedFilter.processName) {
|
|
1094
|
-
resolvedFilter.processName = processName;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const filtered = snapshots
|
|
1098
|
-
.filter((snapshot) => {
|
|
1099
|
-
if (sinceMs !== undefined && snapshot.updatedAt < sinceMs) return false;
|
|
1100
|
-
if (!resolvedFilter) return true;
|
|
1101
|
-
if (resolvedFilter.processName && snapshot.processName !== resolvedFilter.processName) return false;
|
|
1102
|
-
if (resolvedFilter.level && snapshot.level !== resolvedFilter.level) return false;
|
|
1103
|
-
if (resolvedFilter.message && !snapshot.message.includes(resolvedFilter.message)) return false;
|
|
1104
|
-
if (resolvedFilter.eventName && snapshot.eventName !== resolvedFilter.eventName) return false;
|
|
1105
|
-
if (resolvedFilter.correlationId && snapshot.correlationId !== resolvedFilter.correlationId) return false;
|
|
1106
|
-
return true;
|
|
1107
|
-
})
|
|
1108
|
-
.slice(0, limit ?? 200);
|
|
1109
|
-
|
|
1110
|
-
const events = filtered.map((snapshot) => ({
|
|
1111
|
-
eventId: snapshot.lastEventId,
|
|
1112
|
-
eventName: snapshot.eventName,
|
|
1113
|
-
level: snapshot.level,
|
|
1114
|
-
timestamp: new Date(snapshot.updatedAt).toISOString(),
|
|
1115
|
-
timestampMs: snapshot.updatedAt,
|
|
1116
|
-
message: snapshot.message,
|
|
1117
|
-
sessionId: '',
|
|
1118
|
-
workspaceId: workspaceRef.workspaceId,
|
|
1119
|
-
projectName: workspaceRef.projectName,
|
|
1120
|
-
processName: snapshot.processName,
|
|
1121
|
-
processInstance: snapshot.processInstance,
|
|
1122
|
-
raw: snapshot.raw ?? {},
|
|
1123
|
-
kind: 'wide' as const,
|
|
1124
|
-
correlationId: snapshot.correlationId,
|
|
1125
|
-
timeline: Object.values(snapshot.timelineMap),
|
|
1126
|
-
timelineMap: snapshot.timelineMap,
|
|
1127
|
-
timelineOrder: snapshot.timelineOrder,
|
|
1128
|
-
}));
|
|
1129
|
-
|
|
1130
|
-
// Chunk responses to stay under payload limit
|
|
1131
|
-
const maxPayloadBytes = 900_000;
|
|
1132
|
-
const requestId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1133
|
-
const buildPayload = (
|
|
1134
|
-
chunk: import("../../types/events.js").WideEvent[],
|
|
1135
|
-
chunkIndex: number,
|
|
1136
|
-
totalChunks: number,
|
|
1137
|
-
) => ({
|
|
1138
|
-
type: "events_list" as const,
|
|
1139
|
-
workspaceId: workspaceRef.workspaceId,
|
|
1140
|
-
events: chunk,
|
|
1141
|
-
liveEventIds: [] as string[],
|
|
1142
|
-
savedEventFilters,
|
|
1143
|
-
requestId,
|
|
1144
|
-
chunkIndex,
|
|
1145
|
-
totalChunks,
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
const chunks: import("../../types/events.js").WideEvent[][] = [];
|
|
1149
|
-
let chunk: import("../../types/events.js").WideEvent[] = [];
|
|
1150
|
-
for (const event of events) {
|
|
1151
|
-
chunk.push(event);
|
|
1152
|
-
const payloadSize = Buffer.byteLength(JSON.stringify(buildPayload(chunk, 0, 1)));
|
|
1153
|
-
if (payloadSize > maxPayloadBytes) {
|
|
1154
|
-
if (chunk.length === 1) {
|
|
1155
|
-
chunks.push(chunk);
|
|
1156
|
-
chunk = [];
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
const last = chunk.pop();
|
|
1160
|
-
chunks.push(chunk);
|
|
1161
|
-
chunk = last ? [last] : [];
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
if (chunk.length > 0) {
|
|
1166
|
-
chunks.push(chunk);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
if (chunks.length === 0) {
|
|
1170
|
-
chunks.push([]);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const totalChunks = chunks.length;
|
|
1174
|
-
for (let i = 0; i < totalChunks; i += 1) {
|
|
1175
|
-
await this.sendMessage(session, sendResponse, buildPayload(chunks[i], i, totalChunks));
|
|
1176
|
-
}
|
|
1177
|
-
} catch (e) {
|
|
1178
|
-
console.error("[remote-session] Failed to get events:", e);
|
|
1179
|
-
await this.sendError(session, sendResponse, "EVENTS_FAILED", "Failed to get events");
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* Handle start_process request
|
|
1185
|
-
*/
|
|
1186
|
-
private async handleStartProcess(
|
|
1187
|
-
session: RemoteClientSession,
|
|
1188
|
-
workspaceId: string,
|
|
1189
|
-
processName: string,
|
|
1190
|
-
instance: number | undefined,
|
|
1191
|
-
sendResponse: (data: Uint8Array) => void
|
|
1192
|
-
): Promise<void> {
|
|
1193
|
-
try {
|
|
1194
|
-
const workspaces = await scanWorkspaces();
|
|
1195
|
-
const workspace = workspaces.find((w) => matchesWorkspaceId(w, workspaceId));
|
|
1196
|
-
if (!workspace) {
|
|
1197
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const processConfig = loadProcessesConfig(workspace.path);
|
|
1202
|
-
const processDefinition = getProcessDefinition(processConfig, processName);
|
|
1203
|
-
if (!processDefinition) {
|
|
1204
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Process not found");
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (normalizeProcessInstanceCount(processDefinition.instances) === 0) {
|
|
1208
|
-
await this.sendError(session, sendResponse, "PROCESS_DISABLED", `Process is disabled (instances: 0): ${processName}`);
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const specs = getProcessSpecs(workspace.path).filter(
|
|
1213
|
-
(spec) => spec.name === processName && (instance === undefined || spec.instance === instance)
|
|
1214
|
-
);
|
|
1215
|
-
if (specs.length === 0) {
|
|
1216
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Process not found");
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const sessions: string[] = [];
|
|
1221
|
-
for (const spec of specs) {
|
|
1222
|
-
const result = await startProcessInstance(workspace.path, spec);
|
|
1223
|
-
sessions.push(result.sessionId);
|
|
1224
|
-
}
|
|
1225
|
-
if (this.onProcessesChanged) {
|
|
1226
|
-
Promise.resolve(this.onProcessesChanged(workspace.path)).catch(() => undefined);
|
|
1227
|
-
}
|
|
1228
|
-
if (!this.processSchedulers.has(workspace.path)) {
|
|
1229
|
-
this.processSchedulers.set(workspace.path, startProcessScheduler(workspace.path));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
await this.sendMessage(session, sendResponse, {
|
|
1233
|
-
type: "process_started",
|
|
1234
|
-
workspaceId,
|
|
1235
|
-
processName,
|
|
1236
|
-
sessionId: sessions[0],
|
|
1237
|
-
sessionIds: sessions,
|
|
1238
|
-
});
|
|
1239
|
-
} catch (e) {
|
|
1240
|
-
console.error("[remote-session] Failed to start process:", e);
|
|
1241
|
-
await this.sendError(session, sendResponse, "PROCESS_FAILED", "Failed to start process");
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* Handle stop_process request
|
|
1247
|
-
*/
|
|
1248
|
-
private async handleStopProcess(
|
|
1249
|
-
session: RemoteClientSession,
|
|
1250
|
-
workspaceId: string,
|
|
1251
|
-
processName: string,
|
|
1252
|
-
sendResponse: (data: Uint8Array) => void
|
|
1253
|
-
): Promise<void> {
|
|
1254
|
-
try {
|
|
1255
|
-
const workspaces = await scanWorkspaces();
|
|
1256
|
-
const workspace = workspaces.find((w) => matchesWorkspaceId(w, workspaceId));
|
|
1257
|
-
if (!workspace) {
|
|
1258
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
const specs = getProcessSpecs(workspace.path).filter(spec => spec.name === processName);
|
|
1263
|
-
if (specs.length === 0) {
|
|
1264
|
-
await this.sendError(session, sendResponse, "NOT_FOUND", "Process not found");
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
for (const spec of specs) {
|
|
1269
|
-
await stopProcessInstance(workspace.path, spec);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (this.onProcessesChanged) {
|
|
1273
|
-
Promise.resolve(this.onProcessesChanged(workspace.path)).catch(() => undefined);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
await this.sendMessage(session, sendResponse, {
|
|
1277
|
-
type: "process_stopped",
|
|
1278
|
-
workspaceId,
|
|
1279
|
-
processName,
|
|
1280
|
-
});
|
|
1281
|
-
} catch (e) {
|
|
1282
|
-
console.error("[remote-session] Failed to stop process:", e);
|
|
1283
|
-
await this.sendError(session, sendResponse, "PROCESS_FAILED", "Failed to stop process");
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
/**
|
|
1288
|
-
* Cleanup
|
|
1289
|
-
*/
|
|
1290
|
-
async cleanup(): Promise<void> {
|
|
1291
|
-
// Clean up process schedulers
|
|
1292
|
-
for (const timer of this.processSchedulers.values()) {
|
|
1293
|
-
clearInterval(timer);
|
|
1294
|
-
}
|
|
1295
|
-
this.processSchedulers.clear();
|
|
1296
|
-
this.tmuxLiteAvailable = false;
|
|
1297
|
-
}
|
|
1298
|
-
}
|