@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
@@ -0,0 +1,142 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ /// Keychain wrapper for storing remux credentials.
5
+ /// Supports multiple servers via kSecAttrAccount differentiation.
6
+ public struct KeychainStore: Sendable {
7
+
8
+ private static let service = "com.remux.credentials"
9
+
10
+ public init() {}
11
+
12
+ // MARK: - Resume Token
13
+
14
+ public func saveResumeToken(_ token: String, forServer server: String) throws {
15
+ try save(key: "resume_token", value: token, account: server)
16
+ }
17
+
18
+ public func loadResumeToken(forServer server: String) -> String? {
19
+ load(key: "resume_token", account: server)
20
+ }
21
+
22
+ public func deleteResumeToken(forServer server: String) {
23
+ delete(key: "resume_token", account: server)
24
+ }
25
+
26
+ // MARK: - Device ID
27
+
28
+ public func saveDeviceId(_ id: String, forServer server: String) throws {
29
+ try save(key: "device_id", value: id, account: server)
30
+ }
31
+
32
+ public func loadDeviceId(forServer server: String) -> String? {
33
+ load(key: "device_id", account: server)
34
+ }
35
+
36
+ // MARK: - Server Token (manual token auth)
37
+
38
+ public func saveServerToken(_ token: String, forServer server: String) throws {
39
+ try save(key: "server_token", value: token, account: server)
40
+ }
41
+
42
+ public func loadServerToken(forServer server: String) -> String? {
43
+ load(key: "server_token", account: server)
44
+ }
45
+
46
+ public func deleteServerToken(forServer server: String) {
47
+ delete(key: "server_token", account: server)
48
+ }
49
+
50
+ // MARK: - Server List
51
+
52
+ /// Returns all server URLs that have stored credentials.
53
+ public func savedServers() -> [String] {
54
+ let query: [String: Any] = [
55
+ kSecClass as String: kSecClassGenericPassword,
56
+ kSecAttrService as String: Self.service,
57
+ kSecAttrLabel as String: "resume_token",
58
+ kSecMatchLimit as String: kSecMatchLimitAll,
59
+ kSecReturnAttributes as String: true,
60
+ ]
61
+
62
+ var result: AnyObject?
63
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
64
+ guard status == errSecSuccess, let items = result as? [[String: Any]] else {
65
+ return []
66
+ }
67
+
68
+ return items.compactMap { $0[kSecAttrAccount as String] as? String }
69
+ }
70
+
71
+ /// Delete all credentials for a server.
72
+ public func deleteAll(forServer server: String) {
73
+ delete(key: "resume_token", account: server)
74
+ delete(key: "device_id", account: server)
75
+ delete(key: "server_token", account: server)
76
+ }
77
+
78
+ // MARK: - Low-level helpers
79
+
80
+ private func save(key: String, value: String, account: String) throws {
81
+ let data = Data(value.utf8)
82
+
83
+ // Try update first
84
+ let updateQuery: [String: Any] = [
85
+ kSecClass as String: kSecClassGenericPassword,
86
+ kSecAttrService as String: Self.service,
87
+ kSecAttrLabel as String: key,
88
+ kSecAttrAccount as String: account,
89
+ ]
90
+ let updateAttrs: [String: Any] = [
91
+ kSecValueData as String: data,
92
+ ]
93
+
94
+ let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttrs as CFDictionary)
95
+ if updateStatus == errSecSuccess { return }
96
+
97
+ // Item doesn't exist — add it
98
+ let addQuery: [String: Any] = [
99
+ kSecClass as String: kSecClassGenericPassword,
100
+ kSecAttrService as String: Self.service,
101
+ kSecAttrLabel as String: key,
102
+ kSecAttrAccount as String: account,
103
+ kSecValueData as String: data,
104
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
105
+ ]
106
+
107
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
108
+ guard addStatus == errSecSuccess else {
109
+ throw KeychainError.saveFailed(status: addStatus)
110
+ }
111
+ }
112
+
113
+ private func load(key: String, account: String) -> String? {
114
+ let query: [String: Any] = [
115
+ kSecClass as String: kSecClassGenericPassword,
116
+ kSecAttrService as String: Self.service,
117
+ kSecAttrLabel as String: key,
118
+ kSecAttrAccount as String: account,
119
+ kSecReturnData as String: true,
120
+ kSecMatchLimit as String: kSecMatchLimitOne,
121
+ ]
122
+
123
+ var result: AnyObject?
124
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
125
+ guard status == errSecSuccess, let data = result as? Data else { return nil }
126
+ return String(data: data, encoding: .utf8)
127
+ }
128
+
129
+ private func delete(key: String, account: String) {
130
+ let query: [String: Any] = [
131
+ kSecClass as String: kSecClassGenericPassword,
132
+ kSecAttrService as String: Self.service,
133
+ kSecAttrLabel as String: key,
134
+ kSecAttrAccount as String: account,
135
+ ]
136
+ SecItemDelete(query as CFDictionary)
137
+ }
138
+ }
139
+
140
+ public enum KeychainError: Error {
141
+ case saveFailed(status: OSStatus)
142
+ }
@@ -0,0 +1,145 @@
1
+ import Foundation
2
+ import WebKit
3
+
4
+ /// Bridge between Swift and ghostty-web running in WKWebView.
5
+ /// Used by iOS (and macOS WKWebView fallback if needed).
6
+ /// Communication:
7
+ /// Swift → JS: webView.evaluateJavaScript("terminal.write(...)")
8
+ /// JS → Swift: webkit.messageHandlers.remux.postMessage({type, data})
9
+ @MainActor
10
+ public final class GhosttyBridge: NSObject, WKScriptMessageHandler {
11
+
12
+ public let webView: WKWebView
13
+
14
+ public var onInput: ((String) -> Void)?
15
+ public var onResize: ((Int, Int) -> Void)?
16
+ public var onReady: (() -> Void)?
17
+ public var onBell: (() -> Void)?
18
+
19
+ public init(frame: CGRect = .zero) {
20
+ let config = WKWebViewConfiguration()
21
+ config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
22
+
23
+ // Disable zoom on iOS
24
+ let viewportScript = WKUserScript(
25
+ source: "var meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; document.head.appendChild(meta);",
26
+ injectionTime: .atDocumentEnd,
27
+ forMainFrameOnly: true
28
+ )
29
+ config.userContentController.addUserScript(viewportScript)
30
+
31
+ webView = WKWebView(frame: frame, configuration: config)
32
+
33
+ #if os(iOS)
34
+ webView.scrollView.isScrollEnabled = false
35
+ webView.scrollView.bounces = false
36
+ #endif
37
+
38
+ super.init()
39
+
40
+ config.userContentController.add(self, name: "remux")
41
+ loadTerminalHTML()
42
+ }
43
+
44
+ // MARK: - Swift → JS
45
+
46
+ /// Write PTY data to the terminal (base64 encoded to avoid escaping issues)
47
+ public func writeToTerminal(data: Data) {
48
+ let base64 = data.base64EncodedString()
49
+ // Use callAsyncJavaScript with arguments to prevent JS injection
50
+ webView.callAsyncJavaScript(
51
+ "window.terminalBridge.write(b64)",
52
+ arguments: ["b64": base64],
53
+ in: nil,
54
+ in: WKContentWorld.page,
55
+ completionHandler: nil
56
+ )
57
+ }
58
+
59
+ /// Write string data to the terminal
60
+ public func writeString(_ string: String) {
61
+ if let data = string.data(using: .utf8) {
62
+ writeToTerminal(data: data)
63
+ }
64
+ }
65
+
66
+ /// Resize the terminal
67
+ public func resize(cols: Int, rows: Int) {
68
+ webView.callAsyncJavaScript(
69
+ "window.terminalBridge.resize(c, r)",
70
+ arguments: ["c": cols, "r": rows],
71
+ in: nil,
72
+ in: WKContentWorld.page,
73
+ completionHandler: nil
74
+ )
75
+ }
76
+
77
+ /// Set terminal theme
78
+ public func setTheme(_ config: [String: String]) {
79
+ if let data = try? JSONSerialization.data(withJSONObject: config),
80
+ let json = String(data: data, encoding: .utf8) {
81
+ webView.evaluateJavaScript("window.terminalBridge.setTheme(\(json))") { _, _ in }
82
+ }
83
+ }
84
+
85
+ // MARK: - JS → Swift (WKScriptMessageHandler)
86
+
87
+ public func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
88
+ guard let body = message.body as? [String: Any],
89
+ let type = body["type"] as? String else { return }
90
+
91
+ switch type {
92
+ case "input":
93
+ if let data = body["data"] as? String {
94
+ onInput?(data)
95
+ }
96
+ case "resize":
97
+ if let cols = body["cols"] as? Int, let rows = body["rows"] as? Int {
98
+ onResize?(cols, rows)
99
+ }
100
+ case "ready":
101
+ onReady?()
102
+ case "bell":
103
+ onBell?()
104
+ default:
105
+ break
106
+ }
107
+ }
108
+
109
+ // MARK: - HTML loading
110
+
111
+ private func loadTerminalHTML() {
112
+ // Load from main bundle (app must include ghostty-terminal.html)
113
+ if let resourceURL = Bundle.main.url(forResource: "ghostty-terminal", withExtension: "html") {
114
+ webView.loadFileURL(resourceURL, allowingReadAccessTo: resourceURL.deletingLastPathComponent())
115
+ } else {
116
+ // Fallback: inline HTML for development/testing
117
+ let html = Self.fallbackHTML
118
+ webView.loadHTMLString(html, baseURL: nil)
119
+ }
120
+ }
121
+
122
+ /// Minimal fallback HTML when bundle resources are not yet available
123
+ private static let fallbackHTML = """
124
+ <!DOCTYPE html>
125
+ <html>
126
+ <head>
127
+ <meta charset="utf-8">
128
+ <style>
129
+ body { margin: 0; background: #1a1a2e; color: #e0e0e0; font-family: monospace; padding: 20px; }
130
+ </style>
131
+ </head>
132
+ <body>
133
+ <p>ghostty-web resources not found. Run scripts/sync-ghostty-web.sh to bundle them.</p>
134
+ <script>
135
+ window.terminalBridge = {
136
+ write: function(b64) {},
137
+ resize: function(c, r) {},
138
+ setTheme: function(t) {}
139
+ };
140
+ window.webkit.messageHandlers.remux.postMessage({type: 'ready', cols: 80, rows: 24});
141
+ </script>
142
+ </body>
143
+ </html>
144
+ """
145
+ }
@@ -0,0 +1,35 @@
1
+ import SwiftUI
2
+ import WebKit
3
+
4
+ /// SwiftUI view wrapping ghostty-web in WKWebView.
5
+ /// Used by iOS. macOS uses GhosttyNativeTerminalView (libghostty) instead.
6
+ #if os(iOS)
7
+ public struct GhosttyTerminalView: UIViewRepresentable {
8
+ let bridge: GhosttyBridge
9
+
10
+ public init(bridge: GhosttyBridge) {
11
+ self.bridge = bridge
12
+ }
13
+
14
+ public func makeUIView(context: Context) -> WKWebView {
15
+ bridge.webView
16
+ }
17
+
18
+ public func updateUIView(_ uiView: WKWebView, context: Context) {}
19
+ }
20
+ #elseif os(macOS)
21
+ /// macOS fallback using WKWebView (prefer GhosttyNativeTerminalView for production)
22
+ public struct GhosttyTerminalView: NSViewRepresentable {
23
+ let bridge: GhosttyBridge
24
+
25
+ public init(bridge: GhosttyBridge) {
26
+ self.bridge = bridge
27
+ }
28
+
29
+ public func makeNSView(context: Context) -> WKWebView {
30
+ bridge.webView
31
+ }
32
+
33
+ public func updateNSView(_ nsView: WKWebView, context: Context) {}
34
+ }
35
+ #endif
@@ -0,0 +1,91 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; }
9
+ #terminal { width: 100%; height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="terminal"></div>
14
+ <script src="ghostty-web.js"></script>
15
+ <script>
16
+ // ghostty-web terminal bridge for RemuxKit WKWebView integration
17
+ (function() {
18
+ let terminal = null;
19
+
20
+ function init() {
21
+ if (typeof GhosttyWeb === 'undefined') {
22
+ // ghostty-web not loaded yet — show placeholder
23
+ document.getElementById('terminal').textContent = 'Loading terminal...';
24
+ setTimeout(init, 100);
25
+ return;
26
+ }
27
+
28
+ const container = document.getElementById('terminal');
29
+ terminal = new GhosttyWeb.Terminal(container, {
30
+ // ghostty-web options
31
+ });
32
+
33
+ // Forward input to Swift
34
+ terminal.onData(function(data) {
35
+ window.webkit.messageHandlers.remux.postMessage({
36
+ type: 'input',
37
+ data: data
38
+ });
39
+ });
40
+
41
+ // Forward bell to Swift
42
+ terminal.onBell(function() {
43
+ window.webkit.messageHandlers.remux.postMessage({ type: 'bell' });
44
+ });
45
+
46
+ // Watch for container resize
47
+ const resizeObserver = new ResizeObserver(function() {
48
+ if (terminal && terminal.cols && terminal.rows) {
49
+ window.webkit.messageHandlers.remux.postMessage({
50
+ type: 'resize',
51
+ cols: terminal.cols,
52
+ rows: terminal.rows
53
+ });
54
+ }
55
+ });
56
+ resizeObserver.observe(container);
57
+
58
+ // Notify Swift that terminal is ready
59
+ window.webkit.messageHandlers.remux.postMessage({
60
+ type: 'ready',
61
+ cols: terminal.cols || 80,
62
+ rows: terminal.rows || 24
63
+ });
64
+ }
65
+
66
+ // Bridge object exposed to Swift
67
+ window.terminalBridge = {
68
+ write: function(base64) {
69
+ if (!terminal) return;
70
+ const raw = atob(base64);
71
+ const bytes = new Uint8Array(raw.length);
72
+ for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
73
+ terminal.write(bytes);
74
+ },
75
+ resize: function(cols, rows) {
76
+ if (!terminal) return;
77
+ terminal.resize(cols, rows);
78
+ },
79
+ setTheme: function(config) {
80
+ if (!terminal) return;
81
+ // Apply theme config to terminal
82
+ if (config.background) document.body.style.background = config.background;
83
+ // ghostty-web theme API TBD based on actual ghostty-web version
84
+ }
85
+ };
86
+
87
+ init();
88
+ })();
89
+ </script>
90
+ </body>
91
+ </html>
@@ -0,0 +1,74 @@
1
+ import Testing
2
+ import Foundation
3
+ @testable import RemuxKit
4
+
5
+ @Suite("Connection Integration")
6
+ struct ConnectionIntegrationTests {
7
+
8
+ @Test("Connect to local remux server and receive state")
9
+ func connectAndReceiveState() async throws {
10
+ let url = URL(string: "http://localhost:8767")!
11
+
12
+ let conn = RemuxConnection(
13
+ serverURL: url,
14
+ credential: .token("test123"),
15
+ cols: 80,
16
+ rows: 24
17
+ )
18
+
19
+ let result: (auth: Bool, state: Bool) = await withCheckedContinuation { continuation in
20
+ final class TestDelegate: RemuxConnectionDelegate, @unchecked Sendable {
21
+ var authed = false
22
+ var gotState = false
23
+ var continuation: CheckedContinuation<(auth: Bool, state: Bool), Never>?
24
+ var timer: DispatchWorkItem?
25
+
26
+ func connectionDidChangeStatus(_ status: ConnectionStatus) {}
27
+ func connectionDidReceiveData(_ data: Data) {}
28
+
29
+ func connectionDidAuthenticate(capabilities: ProtocolCapabilities) {
30
+ authed = true
31
+ }
32
+
33
+ func connectionDidFailAuth(reason: String) {
34
+ finish()
35
+ }
36
+
37
+ func connectionDidReceiveMessage(_ message: String) {
38
+ if message.contains("\"type\":\"state\"") {
39
+ gotState = true
40
+ finish()
41
+ }
42
+ }
43
+
44
+ func finish() {
45
+ timer?.cancel()
46
+ let c = continuation
47
+ continuation = nil
48
+ c?.resume(returning: (auth: authed, state: gotState))
49
+ }
50
+ }
51
+
52
+ let delegate = TestDelegate()
53
+ delegate.continuation = continuation
54
+
55
+ // Timeout after 5s
56
+ let timeout = DispatchWorkItem { delegate.finish() }
57
+ delegate.timer = timeout
58
+ DispatchQueue.global().asyncAfter(deadline: .now() + 5, execute: timeout)
59
+
60
+ conn.delegate = delegate
61
+ conn.connect()
62
+ }
63
+
64
+ conn.disconnect()
65
+
66
+ if !result.auth {
67
+ print("⚠️ Server not running at localhost:8767, skipping")
68
+ return
69
+ }
70
+
71
+ #expect(result.auth == true)
72
+ #expect(result.state == true)
73
+ }
74
+ }
@@ -0,0 +1,81 @@
1
+ import Testing
2
+ import Foundation
3
+ import Security
4
+ @testable import RemuxKit
5
+
6
+ /// Keychain tests require macOS Keychain access (not available in headless CI).
7
+ /// Tests are conditionally enabled.
8
+ @Suite("KeychainStore")
9
+ struct KeychainStoreTests {
10
+
11
+ let store = KeychainStore()
12
+ let testServer = "test-server-\(UUID().uuidString)"
13
+
14
+ static var keychainAvailable: Bool {
15
+ // CI environments (GitHub Actions, etc.) have partially-broken Keychain:
16
+ // SecItemAdd may succeed but the real store operations fail with -25299.
17
+ // Skip Keychain tests entirely in CI.
18
+ if ProcessInfo.processInfo.environment["CI"] != nil { return false }
19
+
20
+ let query: [String: Any] = [
21
+ kSecClass as String: kSecClassGenericPassword,
22
+ kSecAttrService as String: "com.remux.test-probe",
23
+ kSecAttrAccount as String: UUID().uuidString,
24
+ kSecValueData as String: Data("probe".utf8),
25
+ ]
26
+ let status = SecItemAdd(query as CFDictionary, nil)
27
+ if status == errSecSuccess {
28
+ SecItemDelete(query as CFDictionary)
29
+ return true
30
+ }
31
+ return false
32
+ }
33
+
34
+ @Test("Save and load server token", .enabled(if: keychainAvailable))
35
+ func saveAndLoadToken() throws {
36
+ try store.saveServerToken("abc123", forServer: testServer)
37
+ defer { store.deleteServerToken(forServer: testServer) }
38
+ let loaded = store.loadServerToken(forServer: testServer)
39
+ #expect(loaded == "abc123")
40
+ }
41
+
42
+ @Test("Save and load resume token", .enabled(if: keychainAvailable))
43
+ func saveAndLoadResumeToken() throws {
44
+ try store.saveResumeToken("resume-xyz", forServer: testServer)
45
+ defer { store.deleteResumeToken(forServer: testServer) }
46
+ let loaded = store.loadResumeToken(forServer: testServer)
47
+ #expect(loaded == "resume-xyz")
48
+ }
49
+
50
+ @Test("Load nonexistent key returns nil")
51
+ func loadMissing() {
52
+ let result = store.loadServerToken(forServer: "nonexistent-\(UUID().uuidString)")
53
+ #expect(result == nil)
54
+ }
55
+
56
+ @Test("Delete removes key", .enabled(if: keychainAvailable))
57
+ func deleteKey() throws {
58
+ try store.saveServerToken("temp", forServer: testServer)
59
+ store.deleteServerToken(forServer: testServer)
60
+ let loaded = store.loadServerToken(forServer: testServer)
61
+ #expect(loaded == nil)
62
+ }
63
+
64
+ @Test("Update existing key", .enabled(if: keychainAvailable))
65
+ func updateKey() throws {
66
+ try store.saveServerToken("v1", forServer: testServer)
67
+ try store.saveServerToken("v2", forServer: testServer)
68
+ defer { store.deleteServerToken(forServer: testServer) }
69
+ let loaded = store.loadServerToken(forServer: testServer)
70
+ #expect(loaded == "v2")
71
+ }
72
+
73
+ @Test("Delete all for server", .enabled(if: keychainAvailable))
74
+ func deleteAll() throws {
75
+ try store.saveServerToken("tok", forServer: testServer)
76
+ try store.saveDeviceId("dev", forServer: testServer)
77
+ store.deleteAll(forServer: testServer)
78
+ #expect(store.loadServerToken(forServer: testServer) == nil)
79
+ #expect(store.loadDeviceId(forServer: testServer) == nil)
80
+ }
81
+ }