@wangyaoshen/remux 0.3.8-dev.a8ceb0c
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/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- package/vitest.config.js +10 -0
package/tui/go.mod
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module github.com/eisber/remux/tui
|
|
2
|
+
|
|
3
|
+
go 1.26.1
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
github.com/charmbracelet/bubbletea v1.3.10
|
|
7
|
+
github.com/charmbracelet/lipgloss v1.1.0
|
|
8
|
+
github.com/gorilla/websocket v1.5.3
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
require (
|
|
12
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
13
|
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
|
14
|
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
|
15
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
|
16
|
+
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
17
|
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
|
18
|
+
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
19
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
20
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
21
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
|
22
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
23
|
+
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
24
|
+
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
25
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
26
|
+
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
27
|
+
github.com/muesli/termenv v0.16.0 // indirect
|
|
28
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
29
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
30
|
+
golang.org/x/sys v0.38.0 // indirect
|
|
31
|
+
golang.org/x/text v0.3.8 // indirect
|
|
32
|
+
)
|
package/tui/go.sum
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
2
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
3
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
4
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
5
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
6
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
7
|
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
8
|
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
9
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
10
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
11
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
12
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
13
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
14
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
15
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
16
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
17
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
18
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
19
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
20
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
21
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
22
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
23
|
+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
24
|
+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
25
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
26
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
27
|
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
28
|
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
29
|
+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
30
|
+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
31
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
32
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
33
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
34
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
35
|
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
36
|
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
37
|
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
38
|
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
39
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
40
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
41
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
42
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
43
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
44
|
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
45
|
+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
46
|
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
47
|
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
48
|
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
49
|
+
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
|
50
|
+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
package/tui/main.go
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// remux-tui is a cross-platform terminal client for remux servers.
|
|
2
|
+
//
|
|
3
|
+
// It connects to one or more remux hosts via WebSocket and provides
|
|
4
|
+
// a unified terminal session view using bubbletea.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
//
|
|
8
|
+
// remux-tui connect <url> Connect to a single host
|
|
9
|
+
// remux-tui connect Connect to all saved hosts
|
|
10
|
+
// remux-tui hosts add <name> <url> Save a host
|
|
11
|
+
// remux-tui hosts list List saved hosts
|
|
12
|
+
// remux-tui hosts remove <name> Remove a saved host
|
|
13
|
+
package main
|
|
14
|
+
|
|
15
|
+
import (
|
|
16
|
+
"fmt"
|
|
17
|
+
"net/url"
|
|
18
|
+
"os"
|
|
19
|
+
"strings"
|
|
20
|
+
|
|
21
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
22
|
+
"github.com/eisber/remux/tui/client"
|
|
23
|
+
"github.com/eisber/remux/tui/config"
|
|
24
|
+
"github.com/eisber/remux/tui/ui"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
func main() {
|
|
28
|
+
if len(os.Args) < 2 {
|
|
29
|
+
printUsage()
|
|
30
|
+
os.Exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
switch os.Args[1] {
|
|
34
|
+
case "connect":
|
|
35
|
+
runConnect(os.Args[2:])
|
|
36
|
+
case "hosts":
|
|
37
|
+
runHosts(os.Args[2:])
|
|
38
|
+
case "version":
|
|
39
|
+
fmt.Println("remux-tui v0.1.0")
|
|
40
|
+
case "help", "--help", "-h":
|
|
41
|
+
printUsage()
|
|
42
|
+
default:
|
|
43
|
+
// If it looks like a URL, treat it as `connect <url>`.
|
|
44
|
+
if strings.HasPrefix(os.Args[1], "http") || strings.HasPrefix(os.Args[1], "ws") {
|
|
45
|
+
runConnect(os.Args[1:])
|
|
46
|
+
} else {
|
|
47
|
+
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
|
48
|
+
printUsage()
|
|
49
|
+
os.Exit(1)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func printUsage() {
|
|
55
|
+
fmt.Println(`remux-tui — cross-platform terminal client for remux servers
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
remux-tui connect [url] Connect to a host (or all saved hosts)
|
|
59
|
+
remux-tui hosts add <name> <url> Save a host connection
|
|
60
|
+
remux-tui hosts list List saved hosts
|
|
61
|
+
remux-tui hosts remove <name> Remove a saved host
|
|
62
|
+
remux-tui version Show version
|
|
63
|
+
|
|
64
|
+
Keyboard shortcuts (when connected):
|
|
65
|
+
Ctrl-O Session picker (switch between sessions/hosts)
|
|
66
|
+
Ctrl-D Detach (quit client, sessions keep running)
|
|
67
|
+
|
|
68
|
+
Environment:
|
|
69
|
+
REMUX_PASSWORD Password for authentication`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func runConnect(args []string) {
|
|
73
|
+
manager := client.NewHostManager()
|
|
74
|
+
defer manager.Close()
|
|
75
|
+
|
|
76
|
+
password := os.Getenv("REMUX_PASSWORD")
|
|
77
|
+
|
|
78
|
+
if len(args) > 0 {
|
|
79
|
+
// Connect to a single URL.
|
|
80
|
+
rawURL := args[0]
|
|
81
|
+
host, err := parseHostURL(rawURL)
|
|
82
|
+
if err != nil {
|
|
83
|
+
fmt.Fprintf(os.Stderr, "invalid URL: %s\n", err)
|
|
84
|
+
os.Exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
model := tui.NewModel(manager)
|
|
88
|
+
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
89
|
+
|
|
90
|
+
if err := manager.AddHost(host, password); err != nil {
|
|
91
|
+
fmt.Fprintf(os.Stderr, "failed to connect to %s: %s\n", host.Name, err)
|
|
92
|
+
os.Exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
conn := manager.GetConnection(host.Name)
|
|
96
|
+
tui.SetupCallbacks(manager, p)
|
|
97
|
+
tui.SetupConnectionCallbacks(conn, p)
|
|
98
|
+
|
|
99
|
+
if _, err := p.Run(); err != nil {
|
|
100
|
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
|
101
|
+
os.Exit(1)
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Connect to all saved hosts.
|
|
107
|
+
cfgPath := config.DefaultConfigPath()
|
|
108
|
+
cfg, err := config.Load(cfgPath)
|
|
109
|
+
if err != nil {
|
|
110
|
+
fmt.Fprintf(os.Stderr, "failed to load config: %s\n", err)
|
|
111
|
+
os.Exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if len(cfg.Hosts) == 0 {
|
|
115
|
+
fmt.Println("No saved hosts. Use:")
|
|
116
|
+
fmt.Println(" remux-tui hosts add <name> <url>")
|
|
117
|
+
fmt.Println(" remux-tui connect <url>")
|
|
118
|
+
os.Exit(0)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
model := tui.NewModel(manager)
|
|
122
|
+
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
123
|
+
|
|
124
|
+
for _, host := range cfg.Hosts {
|
|
125
|
+
if err := manager.AddHost(host, password); err != nil {
|
|
126
|
+
fmt.Fprintf(os.Stderr, "warning: failed to connect to %s: %s\n", host.Name, err)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
conn := manager.GetConnection(host.Name)
|
|
130
|
+
tui.SetupConnectionCallbacks(conn, p)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
tui.SetupCallbacks(manager, p)
|
|
134
|
+
|
|
135
|
+
if _, err := p.Run(); err != nil {
|
|
136
|
+
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
|
137
|
+
os.Exit(1)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func runHosts(args []string) {
|
|
142
|
+
cfgPath := config.DefaultConfigPath()
|
|
143
|
+
|
|
144
|
+
if len(args) == 0 {
|
|
145
|
+
args = []string{"list"}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
switch args[0] {
|
|
149
|
+
case "add":
|
|
150
|
+
if len(args) < 3 {
|
|
151
|
+
fmt.Fprintln(os.Stderr, "usage: remux-tui hosts add <name> <url>")
|
|
152
|
+
os.Exit(1)
|
|
153
|
+
}
|
|
154
|
+
name := args[1]
|
|
155
|
+
rawURL := args[2]
|
|
156
|
+
host, err := parseHostURL(rawURL)
|
|
157
|
+
if err != nil {
|
|
158
|
+
fmt.Fprintf(os.Stderr, "invalid URL: %s\n", err)
|
|
159
|
+
os.Exit(1)
|
|
160
|
+
}
|
|
161
|
+
host.Name = name
|
|
162
|
+
|
|
163
|
+
cfg, err := config.Load(cfgPath)
|
|
164
|
+
if err != nil {
|
|
165
|
+
fmt.Fprintf(os.Stderr, "failed to load config: %s\n", err)
|
|
166
|
+
os.Exit(1)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Replace existing host with same name.
|
|
170
|
+
replaced := false
|
|
171
|
+
for i, h := range cfg.Hosts {
|
|
172
|
+
if h.Name == name {
|
|
173
|
+
cfg.Hosts[i] = host
|
|
174
|
+
replaced = true
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if !replaced {
|
|
179
|
+
cfg.Hosts = append(cfg.Hosts, host)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if err := config.Save(cfgPath, cfg); err != nil {
|
|
183
|
+
fmt.Fprintf(os.Stderr, "failed to save config: %s\n", err)
|
|
184
|
+
os.Exit(1)
|
|
185
|
+
}
|
|
186
|
+
fmt.Printf("saved host %q → %s\n", name, host.URL)
|
|
187
|
+
|
|
188
|
+
case "list":
|
|
189
|
+
cfg, err := config.Load(cfgPath)
|
|
190
|
+
if err != nil {
|
|
191
|
+
fmt.Fprintf(os.Stderr, "failed to load config: %s\n", err)
|
|
192
|
+
os.Exit(1)
|
|
193
|
+
}
|
|
194
|
+
if len(cfg.Hosts) == 0 {
|
|
195
|
+
fmt.Println("no saved hosts")
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
for _, h := range cfg.Hosts {
|
|
199
|
+
fmt.Printf(" %s → %s\n", h.Name, h.URL)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "remove":
|
|
203
|
+
if len(args) < 2 {
|
|
204
|
+
fmt.Fprintln(os.Stderr, "usage: remux-tui hosts remove <name>")
|
|
205
|
+
os.Exit(1)
|
|
206
|
+
}
|
|
207
|
+
name := args[1]
|
|
208
|
+
cfg, err := config.Load(cfgPath)
|
|
209
|
+
if err != nil {
|
|
210
|
+
fmt.Fprintf(os.Stderr, "failed to load config: %s\n", err)
|
|
211
|
+
os.Exit(1)
|
|
212
|
+
}
|
|
213
|
+
filtered := cfg.Hosts[:0]
|
|
214
|
+
found := false
|
|
215
|
+
for _, h := range cfg.Hosts {
|
|
216
|
+
if h.Name == name {
|
|
217
|
+
found = true
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
filtered = append(filtered, h)
|
|
221
|
+
}
|
|
222
|
+
if !found {
|
|
223
|
+
fmt.Fprintf(os.Stderr, "host %q not found\n", name)
|
|
224
|
+
os.Exit(1)
|
|
225
|
+
}
|
|
226
|
+
cfg.Hosts = filtered
|
|
227
|
+
if err := config.Save(cfgPath, cfg); err != nil {
|
|
228
|
+
fmt.Fprintf(os.Stderr, "failed to save config: %s\n", err)
|
|
229
|
+
os.Exit(1)
|
|
230
|
+
}
|
|
231
|
+
fmt.Printf("removed host %q\n", name)
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
fmt.Fprintf(os.Stderr, "unknown hosts command: %s\n", args[0])
|
|
235
|
+
os.Exit(1)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// parseHostURL extracts a Host from a URL, pulling the token from the query string.
|
|
240
|
+
func parseHostURL(rawURL string) (client.Host, error) {
|
|
241
|
+
u, err := url.Parse(rawURL)
|
|
242
|
+
if err != nil {
|
|
243
|
+
return client.Host{}, err
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
token := u.Query().Get("token")
|
|
247
|
+
// Remove token from URL for storage.
|
|
248
|
+
q := u.Query()
|
|
249
|
+
q.Del("token")
|
|
250
|
+
u.RawQuery = q.Encode()
|
|
251
|
+
|
|
252
|
+
baseURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
|
|
253
|
+
name := u.Hostname()
|
|
254
|
+
|
|
255
|
+
return client.Host{
|
|
256
|
+
Name: name,
|
|
257
|
+
URL: baseURL,
|
|
258
|
+
Token: token,
|
|
259
|
+
}, nil
|
|
260
|
+
}
|
|
261
|
+
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Package tests contains integration tests that connect to a real remux server.
|
|
2
|
+
//
|
|
3
|
+
// These tests require:
|
|
4
|
+
// - Node.js (for running the remux server)
|
|
5
|
+
// - The remux repo built at ../remux (adjacent to this repo)
|
|
6
|
+
//
|
|
7
|
+
// Run with: go test -tags=integration ./tests/ -v
|
|
8
|
+
package tests
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"fmt"
|
|
13
|
+
"net"
|
|
14
|
+
"net/http"
|
|
15
|
+
"os"
|
|
16
|
+
"os/exec"
|
|
17
|
+
"path/filepath"
|
|
18
|
+
"runtime"
|
|
19
|
+
"strings"
|
|
20
|
+
"testing"
|
|
21
|
+
"time"
|
|
22
|
+
|
|
23
|
+
"github.com/gorilla/websocket"
|
|
24
|
+
"github.com/yaoshenwang/remux-tui/internal/client"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
func findFreePort() (int, error) {
|
|
28
|
+
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
29
|
+
if err != nil {
|
|
30
|
+
return 0, err
|
|
31
|
+
}
|
|
32
|
+
port := l.Addr().(*net.TCPAddr).Port
|
|
33
|
+
l.Close()
|
|
34
|
+
return port, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func findRemuxDir() string {
|
|
38
|
+
// Look for the remux repo relative to this test file.
|
|
39
|
+
candidates := []string{
|
|
40
|
+
filepath.Join("..", "..", "remux"), // C:\work\remux from C:\work\remux-tui\tests
|
|
41
|
+
filepath.Join("..", "..", "..", "remux"),
|
|
42
|
+
}
|
|
43
|
+
for _, c := range candidates {
|
|
44
|
+
abs, _ := filepath.Abs(c)
|
|
45
|
+
if _, err := os.Stat(filepath.Join(abs, "package.json")); err == nil {
|
|
46
|
+
return abs
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// startRemuxServer builds and starts a remux server on a free port.
|
|
53
|
+
// Returns the port, token, password, and a cleanup function.
|
|
54
|
+
func startRemuxServer(t *testing.T) (port int, token, password string, cleanup func()) {
|
|
55
|
+
t.Helper()
|
|
56
|
+
|
|
57
|
+
remuxDir := findRemuxDir()
|
|
58
|
+
if remuxDir == "" {
|
|
59
|
+
t.Skip("remux repo not found at ../remux — skipping integration test")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if remux is built.
|
|
63
|
+
distCli := filepath.Join(remuxDir, "dist", "backend", "cli.js")
|
|
64
|
+
if _, err := os.Stat(distCli); os.IsNotExist(err) {
|
|
65
|
+
// Try to build it.
|
|
66
|
+
t.Log("building remux...")
|
|
67
|
+
buildCmd := exec.Command("npm", "run", "build")
|
|
68
|
+
buildCmd.Dir = remuxDir
|
|
69
|
+
if out, err := buildCmd.CombinedOutput(); err != nil {
|
|
70
|
+
t.Skipf("failed to build remux: %s\n%s", err, out)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
port, err := findFreePort()
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatalf("find free port: %v", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Start remux server with no tunnel, explicit password.
|
|
80
|
+
password = "test-password-123"
|
|
81
|
+
cmd := exec.Command("node", distCli,
|
|
82
|
+
"--port", fmt.Sprintf("%d", port),
|
|
83
|
+
"--password", password,
|
|
84
|
+
"--no-tunnel",
|
|
85
|
+
"--session", "test-session",
|
|
86
|
+
)
|
|
87
|
+
cmd.Dir = remuxDir
|
|
88
|
+
cmd.Env = append(os.Environ(), "VITE_DEV_MODE=1")
|
|
89
|
+
|
|
90
|
+
// Capture stdout to extract token.
|
|
91
|
+
stdout := &strings.Builder{}
|
|
92
|
+
cmd.Stdout = stdout
|
|
93
|
+
cmd.Stderr = stdout
|
|
94
|
+
|
|
95
|
+
if err := cmd.Start(); err != nil {
|
|
96
|
+
t.Fatalf("start remux: %v", err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Wait for server to be ready.
|
|
100
|
+
deadline := time.Now().Add(15 * time.Second)
|
|
101
|
+
for time.Now().Before(deadline) {
|
|
102
|
+
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/config", port))
|
|
103
|
+
if err == nil {
|
|
104
|
+
resp.Body.Close()
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
time.Sleep(200 * time.Millisecond)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Extract token from /api/config.
|
|
111
|
+
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/config", port))
|
|
112
|
+
if err != nil {
|
|
113
|
+
cmd.Process.Kill()
|
|
114
|
+
t.Fatalf("server not ready: %v\nOutput: %s", err, stdout.String())
|
|
115
|
+
}
|
|
116
|
+
defer resp.Body.Close()
|
|
117
|
+
|
|
118
|
+
var configResp struct {
|
|
119
|
+
Token string `json:"token"`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The token is in the URL printed by the server, or we can get it from
|
|
123
|
+
// the server output. For a cleaner approach, let's parse the stdout.
|
|
124
|
+
output := stdout.String()
|
|
125
|
+
|
|
126
|
+
// Token is in the URL as ?token=<value>
|
|
127
|
+
for _, line := range strings.Split(output, "\n") {
|
|
128
|
+
if idx := strings.Index(line, "token="); idx >= 0 {
|
|
129
|
+
tokenPart := line[idx+6:]
|
|
130
|
+
// Token ends at & or end of line or space
|
|
131
|
+
end := len(tokenPart)
|
|
132
|
+
for i, c := range tokenPart {
|
|
133
|
+
if c == '&' || c == ' ' || c == '\n' || c == '\r' {
|
|
134
|
+
end = i
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
configResp.Token = tokenPart[:end]
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if configResp.Token == "" {
|
|
144
|
+
// Fall back: try connecting without token to see error.
|
|
145
|
+
cmd.Process.Kill()
|
|
146
|
+
t.Fatalf("could not extract token from server output:\n%s", output)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
token = configResp.Token
|
|
150
|
+
t.Logf("remux server started on port %d (token=%s...)", port, token[:8])
|
|
151
|
+
|
|
152
|
+
cleanup = func() {
|
|
153
|
+
if runtime.GOOS == "windows" {
|
|
154
|
+
exec.Command("taskkill", "/pid", fmt.Sprintf("%d", cmd.Process.Pid), "/t", "/f").Run()
|
|
155
|
+
} else {
|
|
156
|
+
cmd.Process.Kill()
|
|
157
|
+
}
|
|
158
|
+
cmd.Wait()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return port, token, password, cleanup
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func TestIntegrationConnectToRemux(t *testing.T) {
|
|
165
|
+
if os.Getenv("REMUX_INTEGRATION") != "1" {
|
|
166
|
+
t.Skip("set REMUX_INTEGRATION=1 to run integration tests")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
port, token, password, cleanup := startRemuxServer(t)
|
|
170
|
+
defer cleanup()
|
|
171
|
+
|
|
172
|
+
// Test 1: Connect via our client library.
|
|
173
|
+
host := client.Host{
|
|
174
|
+
Name: "test",
|
|
175
|
+
URL: fmt.Sprintf("http://127.0.0.1:%d", port),
|
|
176
|
+
Token: token,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
conn := client.NewConnection(host, password)
|
|
180
|
+
|
|
181
|
+
var gotTerminalData bool
|
|
182
|
+
doneCh := make(chan struct{}, 2)
|
|
183
|
+
|
|
184
|
+
conn.OnTerminalData(func(data []byte) {
|
|
185
|
+
if !gotTerminalData {
|
|
186
|
+
gotTerminalData = true
|
|
187
|
+
t.Logf("received terminal data: %d bytes", len(data))
|
|
188
|
+
doneCh <- struct{}{}
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
conn.OnStateUpdate(func(state *client.StateSnapshot) {
|
|
193
|
+
t.Logf("received state: %d sessions", len(state.Sessions))
|
|
194
|
+
doneCh <- struct{}{}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
conn.OnError(func(err error) {
|
|
198
|
+
t.Logf("connection error: %v", err)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if err := conn.Connect(); err != nil {
|
|
202
|
+
t.Fatalf("connect failed: %v", err)
|
|
203
|
+
}
|
|
204
|
+
defer conn.Close()
|
|
205
|
+
|
|
206
|
+
// Wait for either terminal data or state update.
|
|
207
|
+
select {
|
|
208
|
+
case <-doneCh:
|
|
209
|
+
t.Log("received first message from server")
|
|
210
|
+
case <-time.After(10 * time.Second):
|
|
211
|
+
t.Fatal("timeout waiting for server messages")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Test 2: Send input and verify it doesn't error.
|
|
215
|
+
if err := conn.SendInput("echo hello\r"); err != nil {
|
|
216
|
+
t.Fatalf("send input failed: %v", err)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Test 3: Send resize.
|
|
220
|
+
if err := conn.SendResize(120, 40); err != nil {
|
|
221
|
+
t.Fatalf("send resize failed: %v", err)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
t.Log("integration test passed")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func TestIntegrationRawWebSocket(t *testing.T) {
|
|
228
|
+
if os.Getenv("REMUX_INTEGRATION") != "1" {
|
|
229
|
+
t.Skip("set REMUX_INTEGRATION=1 to run integration tests")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
port, token, password, cleanup := startRemuxServer(t)
|
|
233
|
+
defer cleanup()
|
|
234
|
+
|
|
235
|
+
// Test raw WebSocket protocol directly.
|
|
236
|
+
controlURL := fmt.Sprintf("ws://127.0.0.1:%d/ws/control", port)
|
|
237
|
+
controlConn, _, err := websocket.DefaultDialer.Dial(controlURL, nil)
|
|
238
|
+
if err != nil {
|
|
239
|
+
t.Fatalf("dial control: %v", err)
|
|
240
|
+
}
|
|
241
|
+
defer controlConn.Close()
|
|
242
|
+
|
|
243
|
+
// Send auth.
|
|
244
|
+
authMsg := map[string]interface{}{
|
|
245
|
+
"type": "auth",
|
|
246
|
+
"token": token,
|
|
247
|
+
"password": password,
|
|
248
|
+
}
|
|
249
|
+
if err := controlConn.WriteJSON(authMsg); err != nil {
|
|
250
|
+
t.Fatalf("send auth: %v", err)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Read auth response.
|
|
254
|
+
_, raw, err := controlConn.ReadMessage()
|
|
255
|
+
if err != nil {
|
|
256
|
+
t.Fatalf("read auth response: %v", err)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var resp map[string]interface{}
|
|
260
|
+
json.Unmarshal(raw, &resp)
|
|
261
|
+
|
|
262
|
+
if resp["type"] != "auth_ok" {
|
|
263
|
+
t.Fatalf("expected auth_ok, got: %v", resp)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
clientID, ok := resp["clientId"].(string)
|
|
267
|
+
if !ok || clientID == "" {
|
|
268
|
+
t.Fatal("missing clientId in auth_ok")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
t.Logf("authenticated with clientId=%s", clientID)
|
|
272
|
+
|
|
273
|
+
// Read a few more messages to see state/session_picker.
|
|
274
|
+
for i := 0; i < 5; i++ {
|
|
275
|
+
controlConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
276
|
+
_, raw, err := controlConn.ReadMessage()
|
|
277
|
+
if err != nil {
|
|
278
|
+
break
|
|
279
|
+
}
|
|
280
|
+
json.Unmarshal(raw, &resp)
|
|
281
|
+
t.Logf("received: type=%v", resp["type"])
|
|
282
|
+
}
|
|
283
|
+
}
|