@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.
Files changed (183) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  4. package/.github/dependabot.yml +33 -0
  5. package/.github/workflows/ci.yml +65 -0
  6. package/.github/workflows/deploy.yml +65 -0
  7. package/.github/workflows/publish.yml +312 -0
  8. package/.github/workflows/release-please.yml +21 -0
  9. package/.gitmodules +3 -0
  10. package/.nvmrc +1 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CLAUDE.md +104 -0
  13. package/Dockerfile +23 -0
  14. package/LICENSE +21 -0
  15. package/README.md +120 -0
  16. package/apps/ios/Config/signing.xcconfig +4 -0
  17. package/apps/ios/Package.swift +26 -0
  18. package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
  19. package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  20. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
  21. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
  22. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
  23. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
  24. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
  25. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
  26. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
  27. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
  28. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
  29. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
  30. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
  31. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
  32. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
  33. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
  34. package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
  35. package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
  36. package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
  37. package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
  38. package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
  39. package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
  40. package/apps/ios/Sources/Remux/RootView.swift +130 -0
  41. package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
  42. package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
  43. package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
  44. package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
  45. package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
  46. package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
  47. package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
  48. package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
  49. package/apps/macos/Package.swift +37 -0
  50. package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
  51. package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
  52. package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
  53. package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
  54. package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
  55. package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
  56. package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
  57. package/apps/macos/Resources/terminfo/67/ghostty +0 -0
  58. package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
  59. package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
  60. package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
  61. package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
  62. package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
  63. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
  64. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
  65. package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
  66. package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
  67. package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
  68. package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
  69. package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
  70. package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
  71. package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
  72. package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
  73. package/apps/macos/Sources/Remux/SocketController.swift +258 -0
  74. package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
  75. package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
  76. package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
  77. package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
  78. package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
  79. package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
  80. package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
  81. package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
  82. package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
  83. package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
  84. package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
  85. package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
  86. package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
  87. package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
  88. package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
  89. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
  90. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
  91. package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
  92. package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
  93. package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
  94. package/build.mjs +33 -0
  95. package/native/android/DecodeGoldenPayloads.kt +487 -0
  96. package/native/android/ProtocolModels.kt +188 -0
  97. package/native/ios/DecodeGoldenPayloads.swift +711 -0
  98. package/native/ios/ProtocolModels.swift +200 -0
  99. package/package.json +45 -0
  100. package/packages/RemuxKit/Package.swift +27 -0
  101. package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
  102. package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
  103. package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
  104. package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
  105. package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
  106. package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
  107. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
  108. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
  109. package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
  110. package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
  111. package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
  112. package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
  113. package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
  114. package/playwright.config.ts +17 -0
  115. package/pnpm-lock.yaml +1588 -0
  116. package/pty-daemon.js +303 -0
  117. package/release-please-config.json +14 -0
  118. package/scripts/auto-deploy.sh +46 -0
  119. package/scripts/build-dmg.sh +121 -0
  120. package/scripts/build-ghostty-kit.sh +43 -0
  121. package/scripts/check-active-terminology.mjs +132 -0
  122. package/scripts/setup-ci-secrets.sh +80 -0
  123. package/scripts/sync-ghostty-web.sh +28 -0
  124. package/scripts/upload-testflight.sh +100 -0
  125. package/server.js +7074 -0
  126. package/src/adapters/agent-events.ts +246 -0
  127. package/src/adapters/claude-code.ts +158 -0
  128. package/src/adapters/codex.ts +210 -0
  129. package/src/adapters/generic-shell.ts +58 -0
  130. package/src/adapters/index.ts +15 -0
  131. package/src/adapters/registry.ts +99 -0
  132. package/src/adapters/types.ts +41 -0
  133. package/src/auth.ts +174 -0
  134. package/src/e2ee.ts +236 -0
  135. package/src/git-service.ts +168 -0
  136. package/src/message-buffer.ts +137 -0
  137. package/src/pty-daemon.ts +357 -0
  138. package/src/push.ts +127 -0
  139. package/src/renderers.ts +455 -0
  140. package/src/server.ts +2407 -0
  141. package/src/service.ts +226 -0
  142. package/src/session.ts +978 -0
  143. package/src/store.ts +1422 -0
  144. package/src/team.ts +123 -0
  145. package/src/tunnel.ts +126 -0
  146. package/src/types.d.ts +50 -0
  147. package/src/vt-tracker.ts +188 -0
  148. package/src/workspace-head.ts +144 -0
  149. package/src/workspace.ts +153 -0
  150. package/src/ws-handler.ts +1526 -0
  151. package/start.ps1 +83 -0
  152. package/tests/adapters.test.js +171 -0
  153. package/tests/auth.test.js +243 -0
  154. package/tests/codex-adapter.test.js +535 -0
  155. package/tests/durable-stream.test.js +153 -0
  156. package/tests/e2e/app.spec.js +530 -0
  157. package/tests/e2ee.test.js +325 -0
  158. package/tests/message-buffer.test.js +245 -0
  159. package/tests/message-routing.test.js +305 -0
  160. package/tests/pty-daemon.test.js +346 -0
  161. package/tests/push.test.js +281 -0
  162. package/tests/renderers.test.js +391 -0
  163. package/tests/search-shell.test.js +499 -0
  164. package/tests/server.test.js +882 -0
  165. package/tests/service.test.js +267 -0
  166. package/tests/store.test.js +369 -0
  167. package/tests/tunnel.test.js +67 -0
  168. package/tests/workspace-head.test.js +116 -0
  169. package/tests/workspace.test.js +417 -0
  170. package/tsconfig.backend.json +11 -0
  171. package/tsconfig.json +15 -0
  172. package/tui/client/client_test.go +125 -0
  173. package/tui/client/connection.go +342 -0
  174. package/tui/client/host_manager.go +141 -0
  175. package/tui/config/cache.go +81 -0
  176. package/tui/config/config.go +53 -0
  177. package/tui/config/config_test.go +89 -0
  178. package/tui/go.mod +32 -0
  179. package/tui/go.sum +50 -0
  180. package/tui/main.go +261 -0
  181. package/tui/tests/integration_test.go +283 -0
  182. package/tui/ui/model.go +310 -0
  183. 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
+ }