@wangyaoshen/remux 0.3.8-dev.bab6c95 → 0.3.9-dev.390cb29

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 (33) hide show
  1. package/.github/workflows/publish.yml +191 -17
  2. package/apps/ios/Remux.xcodeproj/project.pbxproj +21 -0
  3. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
  4. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
  5. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
  6. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
  7. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
  8. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
  9. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
  10. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
  11. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
  12. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
  13. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
  14. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
  15. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
  16. package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
  17. package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
  18. package/apps/ios/Sources/Remux/RootView.swift +2 -2
  19. package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +11 -4
  20. package/apps/macos/Sources/Remux/Views/ConnectionView.swift +4 -8
  21. package/package.json +1 -1
  22. package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +64 -0
  23. package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +88 -9
  24. package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +47 -8
  25. package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +81 -8
  26. package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +20 -1
  27. package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +16 -0
  28. package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +26 -0
  29. package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +41 -7
  30. package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +20 -2
  31. package/scripts/setup-ci-secrets.sh +80 -0
  32. package/scripts/upload-testflight.sh +100 -0
  33. package/tests/server.test.js +1 -1
@@ -55,37 +55,214 @@ jobs:
55
55
  tags: ${{ steps.meta.outputs.tags }}
56
56
  labels: ${{ steps.meta.outputs.labels }}
57
57
 
58
- # ── macOS app (.dmg) GitHub Release ────────────────
59
- macos:
58
+ # ── iOSTestFlight ─────────────────────────────────
59
+ ios:
60
60
  runs-on: macos-15
61
61
  permissions:
62
- contents: write
62
+ contents: read
63
63
  steps:
64
64
  - uses: actions/checkout@v4
65
65
  with:
66
66
  submodules: recursive
67
67
 
68
- - name: Install Zig
69
- uses: mlugg/setup-zig@v2
68
+ - name: Set up signing
69
+ env:
70
+ CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }}
71
+ CERTIFICATES_PASSWORD: ${{ secrets.APPLE_CERTIFICATES_PASSWORD }}
72
+ API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }}
73
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
74
+ run: |
75
+ # Create temporary keychain
76
+ KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
77
+ KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"
78
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
79
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
80
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
81
+
82
+ # Import certificates
83
+ echo "$CERTIFICATES_P12" | base64 --decode > "$RUNNER_TEMP/certs.p12"
84
+ security import "$RUNNER_TEMP/certs.p12" \
85
+ -P "$CERTIFICATES_PASSWORD" \
86
+ -A -t cert -f pkcs12 \
87
+ -k "$KEYCHAIN_PATH"
88
+ security set-key-partition-list -S apple-tool:,apple:,codesign: \
89
+ -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
90
+
91
+ # Add keychain to search list
92
+ security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
93
+
94
+ # Install Apple WWDR G3 intermediate cert
95
+ curl -sO https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer
96
+ sudo security add-certificates -k /Library/Keychains/System.keychain AppleWWDRCAG3.cer
97
+
98
+ # Set up API key
99
+ mkdir -p ~/.private_keys
100
+ echo "$API_KEY_P8" | base64 --decode > ~/.private_keys/AuthKey_${API_KEY_ID}.p8
101
+
102
+ # Verify
103
+ security find-identity -v -p codesigning "$KEYCHAIN_PATH"
104
+
105
+ - name: Archive
106
+ env:
107
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
108
+ API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
109
+ run: |
110
+ TAG="${GITHUB_REF#refs/tags/}"
111
+ VERSION="${TAG#v}"
112
+
113
+ xcodebuild archive \
114
+ -project apps/ios/Remux.xcodeproj \
115
+ -scheme Remux \
116
+ -destination 'generic/platform=iOS' \
117
+ -archivePath "$RUNNER_TEMP/Remux.xcarchive" \
118
+ -allowProvisioningUpdates \
119
+ -authenticationKeyPath ~/.private_keys/AuthKey_${API_KEY_ID}.p8 \
120
+ -authenticationKeyID "$API_KEY_ID" \
121
+ -authenticationKeyIssuerID "$API_ISSUER_ID" \
122
+ CURRENT_PROJECT_VERSION="$(git rev-list --count HEAD)" \
123
+ MARKETING_VERSION="$VERSION" \
124
+ -quiet
125
+
126
+ - name: Export IPA
127
+ env:
128
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
129
+ API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
130
+ run: |
131
+ cat > "$RUNNER_TEMP/ExportOptions.plist" << 'PLIST'
132
+ <?xml version="1.0" encoding="UTF-8"?>
133
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
134
+ <plist version="1.0">
135
+ <dict>
136
+ <key>method</key>
137
+ <string>app-store-connect</string>
138
+ <key>signingStyle</key>
139
+ <string>automatic</string>
140
+ <key>uploadSymbols</key>
141
+ <true/>
142
+ <key>manageAppVersionAndBuildNumber</key>
143
+ <true/>
144
+ </dict>
145
+ </plist>
146
+ PLIST
147
+
148
+ xcodebuild -exportArchive \
149
+ -archivePath "$RUNNER_TEMP/Remux.xcarchive" \
150
+ -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
151
+ -exportPath "$RUNNER_TEMP/export" \
152
+ -allowProvisioningUpdates \
153
+ -authenticationKeyPath ~/.private_keys/AuthKey_${API_KEY_ID}.p8 \
154
+ -authenticationKeyID "$API_KEY_ID" \
155
+ -authenticationKeyIssuerID "$API_ISSUER_ID" \
156
+ -quiet
157
+
158
+ - name: Upload to TestFlight
159
+ env:
160
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
161
+ API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
162
+ run: |
163
+ IPA=$(find "$RUNNER_TEMP/export" -name "*.ipa" | head -1)
164
+ xcrun altool --upload-app --type ios \
165
+ --file "$IPA" \
166
+ --apiKey "$API_KEY_ID" \
167
+ --apiIssuer "$API_ISSUER_ID"
168
+
169
+ - name: Set export compliance
170
+ run: |
171
+ pip3 install PyJWT cryptography
172
+ python3 << 'PYEOF'
173
+ import jwt, time, json, urllib.request, os
174
+
175
+ with open(os.path.expanduser(f"~/.private_keys/AuthKey_{os.environ['API_KEY_ID']}.p8")) as f:
176
+ key = f.read()
177
+ token = jwt.encode(
178
+ {"iss": os.environ["API_ISSUER_ID"], "iat": int(time.time()),
179
+ "exp": int(time.time()) + 1200, "aud": "appstoreconnect-v1"},
180
+ key, algorithm="ES256",
181
+ headers={"kid": os.environ["API_KEY_ID"], "typ": "JWT"})
182
+
183
+ # Find latest build
184
+ req = urllib.request.Request(
185
+ f"https://api.appstoreconnect.apple.com/v1/builds?filter[app]=6761521429&sort=-uploadedDate&limit=1",
186
+ headers={"Authorization": f"Bearer {token}"})
187
+ data = json.loads(urllib.request.urlopen(req).read())
188
+ build_id = data["data"][0]["id"]
189
+
190
+ # Set usesNonExemptEncryption = false
191
+ body = json.dumps({"data": {"type": "builds", "id": build_id,
192
+ "attributes": {"usesNonExemptEncryption": False}}}).encode()
193
+ req = urllib.request.Request(
194
+ f"https://api.appstoreconnect.apple.com/v1/builds/{build_id}",
195
+ data=body, method="PATCH",
196
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
197
+ urllib.request.urlopen(req)
198
+ print(f"✓ Export compliance set for build {build_id}")
199
+ PYEOF
200
+ env:
201
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
202
+ API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
203
+
204
+ - name: Clean up keychain
205
+ if: always()
206
+ run: security delete-keychain "$RUNNER_TEMP/signing.keychain-db" 2>/dev/null || true
207
+
208
+ # ── macOS app (.dmg) → signed + notarized + GitHub Release ──
209
+ # Runs on self-hosted runner (Mac Mini) which has Developer ID cert installed
210
+ macos:
211
+ runs-on: [self-hosted, remux-deploy]
212
+ permissions:
213
+ contents: write
214
+ steps:
215
+ - uses: actions/checkout@v4
70
216
  with:
71
- version: 0.15.2
217
+ submodules: recursive
72
218
 
73
- - name: Build GhosttyKit xcframework
219
+ - name: Verify signing identity
74
220
  run: |
75
- cd vendor/ghostty
76
- zig build -Demit-xcframework=true -Dxcframework-target=native -Doptimize=ReleaseFast
221
+ security find-identity -v -p codesigning | grep "Developer ID Application"
77
222
 
78
- - name: Build macOS app (release)
223
+ - name: Build GhosttyKit xcframework (if needed)
79
224
  run: |
80
- cd apps/macos
81
- swift build -c release
225
+ XCFW="vendor/ghostty/macos/GhosttyKit.xcframework"
226
+ if [ ! -d "$XCFW" ] || [ ! -f "$XCFW/Info.plist" ]; then
227
+ cd vendor/ghostty
228
+ zig build -Demit-xcframework=true -Dxcframework-target=native -Doptimize=ReleaseFast
229
+ fi
82
230
 
83
- - name: Create .app bundle and DMG
231
+ - name: Build, sign, and notarize DMG
232
+ env:
233
+ API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
234
+ API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
235
+ TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
84
236
  run: |
85
237
  TAG="${GITHUB_REF#refs/tags/}"
86
238
  VERSION="${TAG#v}"
239
+
240
+ # Build .app and DMG
87
241
  bash scripts/build-dmg.sh "$VERSION"
88
242
 
243
+ # Sign with Developer ID + hardened runtime + timestamp
244
+ codesign --force --deep --options runtime --timestamp \
245
+ --sign "Developer ID Application: Yaoshen Wang ($TEAM_ID)" \
246
+ "build/Remux.app"
247
+
248
+ # Rebuild DMG with signed app
249
+ DMG="build/Remux-${VERSION}-arm64.dmg"
250
+ rm -f "$DMG"
251
+ hdiutil create -volname "Remux" \
252
+ -srcfolder build/Remux.app \
253
+ -ov -format UDZO "$DMG"
254
+ codesign --force --timestamp \
255
+ --sign "Developer ID Application: Yaoshen Wang ($TEAM_ID)" "$DMG"
256
+
257
+ # Notarize
258
+ xcrun notarytool submit "$DMG" \
259
+ --key ~/.private_keys/AuthKey_${API_KEY_ID}.p8 \
260
+ --key-id "$API_KEY_ID" \
261
+ --issuer "$API_ISSUER_ID" \
262
+ --wait
263
+
264
+ xcrun stapler staple "$DMG"
265
+
89
266
  - name: Upload DMG to GitHub Release
90
267
  env:
91
268
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -93,13 +270,10 @@ jobs:
93
270
  TAG="${GITHUB_REF#refs/tags/}"
94
271
  VERSION="${TAG#v}"
95
272
  DMG="build/Remux-${VERSION}-arm64.dmg"
273
+
96
274
  if [ -f "$DMG" ]; then
97
275
  gh release upload "$TAG" "$DMG" --clobber
98
276
  echo "✓ Uploaded $DMG to release $TAG"
99
- else
100
- echo "⚠ DMG not found, uploading .app as zip"
101
- cd build && zip -r "Remux-${VERSION}-arm64.zip" Remux.app
102
- gh release upload "$TAG" "Remux-${VERSION}-arm64.zip" --clobber
103
277
  fi
104
278
 
105
279
  # ── Update Homebrew tap ─────────────────────────────
@@ -21,6 +21,7 @@
21
21
  ECAFCC766BD77CCD21821612 /* ManualConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3AE7922D1E43AE099D1D40 /* ManualConnectView.swift */; };
22
22
  EE0A712F52F48C1AB61D7AEC /* RemuxKit in Frameworks */ = {isa = PBXBuildFile; productRef = EF63343AE24B477D7365EE17 /* RemuxKit */; };
23
23
  F136D905F6E9BA7F2C1816F2 /* InspectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED7C2F66BB76FB8DE51725E /* InspectView.swift */; };
24
+ AA1234560000000000000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA1234560000000000000002 /* Assets.xcassets */; };
24
25
  /* End PBXBuildFile section */
25
26
 
26
27
  /* Begin PBXFileReference section */
@@ -40,6 +41,7 @@
40
41
  D548B0C91D3AABA26641D898 /* ControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = "<group>"; };
41
42
  DBE379B0C119DDC9C8B324E5 /* NowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowView.swift; sourceTree = "<group>"; };
42
43
  DEEEA01EB80CE2C1601F6228 /* InspectCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectCache.swift; sourceTree = "<group>"; };
44
+ AA1234560000000000000002 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
43
45
  /* End PBXFileReference section */
44
46
 
45
47
  /* Begin PBXFrameworksBuildPhase section */
@@ -67,6 +69,7 @@
67
69
  0E70947CCFEAEFEDCCF14E35 /* Remux */ = {
68
70
  isa = PBXGroup;
69
71
  children = (
72
+ AA1234560000000000000002 /* Assets.xcassets */,
70
73
  48C3DD287E421F1077A8923A /* MainTabView.swift */,
71
74
  6C491FD55D84AC2BED5C5EBD /* Remux.entitlements */,
72
75
  662FEE0E78ADAADEA731106C /* RemuxiOSApp.swift */,
@@ -174,6 +177,7 @@
174
177
  buildPhases = (
175
178
  204B591A381758166DA82963 /* Sources */,
176
179
  03BDC461530EDD09CDB7FF29 /* Frameworks */,
180
+ AA1234560000000000000003 /* Resources */,
177
181
  );
178
182
  buildRules = (
179
183
  );
@@ -224,6 +228,17 @@
224
228
  };
225
229
  /* End PBXProject section */
226
230
 
231
+ /* Begin PBXResourcesBuildPhase section */
232
+ AA1234560000000000000003 /* Resources */ = {
233
+ isa = PBXResourcesBuildPhase;
234
+ buildActionMask = 2147483647;
235
+ files = (
236
+ AA1234560000000000000001 /* Assets.xcassets in Resources */,
237
+ );
238
+ runOnlyForDeploymentPostprocessing = 0;
239
+ };
240
+ /* End PBXResourcesBuildPhase section */
241
+
227
242
  /* Begin PBXSourcesBuildPhase section */
228
243
  204B591A381758166DA82963 /* Sources */ = {
229
244
  isa = PBXSourcesBuildPhase;
@@ -378,6 +393,7 @@
378
393
  CODE_SIGN_ENTITLEMENTS = Sources/Remux/Remux.entitlements;
379
394
  CODE_SIGN_IDENTITY = "iPhone Developer";
380
395
  GENERATE_INFOPLIST_FILE = YES;
396
+ INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
381
397
  INFOPLIST_KEY_NSCameraUsageDescription = "Remux uses the camera to scan QR codes for server pairing.";
382
398
  INFOPLIST_KEY_NSFaceIDUsageDescription = "Remux uses Face ID to protect your terminal sessions.";
383
399
  INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -387,6 +403,8 @@
387
403
  "$(inherited)",
388
404
  "@executable_path/Frameworks",
389
405
  );
406
+ CURRENT_PROJECT_VERSION = 1;
407
+ MARKETING_VERSION = 0.3.8;
390
408
  PRODUCT_BUNDLE_IDENTIFIER = com.remux.ios;
391
409
  SDKROOT = iphoneos;
392
410
  TARGETED_DEVICE_FAMILY = "1,2";
@@ -400,6 +418,7 @@
400
418
  CODE_SIGN_ENTITLEMENTS = Sources/Remux/Remux.entitlements;
401
419
  CODE_SIGN_IDENTITY = "iPhone Developer";
402
420
  GENERATE_INFOPLIST_FILE = YES;
421
+ INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
403
422
  INFOPLIST_KEY_NSCameraUsageDescription = "Remux uses the camera to scan QR codes for server pairing.";
404
423
  INFOPLIST_KEY_NSFaceIDUsageDescription = "Remux uses Face ID to protect your terminal sessions.";
405
424
  INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -409,6 +428,8 @@
409
428
  "$(inherited)",
410
429
  "@executable_path/Frameworks",
411
430
  );
431
+ CURRENT_PROJECT_VERSION = 1;
432
+ MARKETING_VERSION = 0.3.8;
412
433
  PRODUCT_BUNDLE_IDENTIFIER = com.remux.ios;
413
434
  SDKROOT = iphoneos;
414
435
  TARGETED_DEVICE_FAMILY = "1,2";
@@ -0,0 +1,23 @@
1
+ {
2
+ "images" : [
3
+ { "filename" : "icon_40x40.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" },
4
+ { "filename" : "icon_60x60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" },
5
+ { "filename" : "icon_58x58.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" },
6
+ { "filename" : "icon_87x87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" },
7
+ { "filename" : "icon_80x80.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" },
8
+ { "filename" : "icon_120x120.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" },
9
+ { "filename" : "icon_120x120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" },
10
+ { "filename" : "icon_180x180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" },
11
+ { "filename" : "icon_20x20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" },
12
+ { "filename" : "icon_40x40.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" },
13
+ { "filename" : "icon_29x29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" },
14
+ { "filename" : "icon_58x58.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" },
15
+ { "filename" : "icon_40x40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" },
16
+ { "filename" : "icon_80x80.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" },
17
+ { "filename" : "icon_76x76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" },
18
+ { "filename" : "icon_152x152.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" },
19
+ { "filename" : "icon_167x167.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" },
20
+ { "filename" : "icon_1024x1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }
21
+ ],
22
+ "info" : { "author" : "xcode", "version" : 1 }
23
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "info" : {
3
+ "author" : "xcode",
4
+ "version" : 1
5
+ }
6
+ }
@@ -96,9 +96,9 @@ struct RootView: View {
96
96
  private func tryAutoConnect() {
97
97
  let servers = keychain.savedServers()
98
98
  if let server = servers.first,
99
- let token = keychain.loadResumeToken(forServer: server) ?? keychain.loadServerToken(forServer: server),
99
+ let credential = keychain.preferredCredential(forServer: server),
100
100
  let url = URL(string: server) {
101
- state.connect(url: url, credential: .token(token))
101
+ state.connect(url: url, credential: credential)
102
102
  }
103
103
  }
104
104
  }
@@ -65,13 +65,13 @@ struct MeView: View {
65
65
  VStack(alignment: .leading) {
66
66
  Text(device.name ?? device.id.prefix(8).description)
67
67
  .font(.subheadline)
68
- Text(device.trustLevel)
68
+ Text(device.trust)
69
69
  .font(.caption)
70
- .foregroundStyle(device.trustLevel == "trusted" ? .green : .orange)
70
+ .foregroundStyle(device.trust == "trusted" ? .green : .orange)
71
71
  }
72
72
  Spacer()
73
73
  if let lastSeen = device.lastSeen {
74
- Text(lastSeen.prefix(10).description)
74
+ Text(Self.lastSeenFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(lastSeen) / 1000)))
75
75
  .font(.caption2)
76
76
  .foregroundStyle(.tertiary)
77
77
  }
@@ -90,7 +90,7 @@ struct MeView: View {
90
90
  HStack {
91
91
  Text("Version")
92
92
  Spacer()
93
- Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.3.5")
93
+ Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.3.9")
94
94
  .foregroundStyle(.secondary)
95
95
  }
96
96
  }
@@ -112,6 +112,13 @@ struct MeView: View {
112
112
  default: "questionmark.circle"
113
113
  }
114
114
  }
115
+
116
+ private static let lastSeenFormatter: DateFormatter = {
117
+ let formatter = DateFormatter()
118
+ formatter.dateStyle = .short
119
+ formatter.timeStyle = .short
120
+ return formatter
121
+ }()
115
122
  }
116
123
 
117
124
  extension ConnectionStatus {
@@ -53,9 +53,10 @@ struct ConnectionView: View {
53
53
  serverURL = server
54
54
  if let savedToken = keychain.loadServerToken(forServer: server) {
55
55
  token = savedToken
56
- connect()
57
- } else if let resumeToken = keychain.loadResumeToken(forServer: server) {
58
- connectWithResumeToken(server: server, resumeToken: resumeToken)
56
+ }
57
+ if let credential = keychain.preferredCredential(forServer: server),
58
+ let url = URL(string: server) {
59
+ state.connect(url: url, credential: credential)
59
60
  }
60
61
  }
61
62
  .buttonStyle(.plain)
@@ -76,9 +77,4 @@ struct ConnectionView: View {
76
77
  try? keychain.saveServerToken(token, forServer: serverURL)
77
78
  state.connect(url: url, credential: .token(token))
78
79
  }
79
-
80
- private func connectWithResumeToken(server: String, resumeToken: String) {
81
- guard let url = URL(string: server) else { return }
82
- state.connect(url: url, credential: .resumeToken(resumeToken))
83
- }
84
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wangyaoshen/remux",
3
- "version": "0.3.8-dev.bab6c95",
3
+ "version": "0.3.9-dev.390cb29",
4
4
  "description": "Remote terminal workspace — powered by ghostty-web",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,3 +1,5 @@
1
+ import Foundation
2
+
1
3
  public struct ProtocolCapabilities: Codable, Equatable, Sendable {
2
4
  public let envelope: Bool
3
5
  public let inspectV2: Bool
@@ -67,6 +69,58 @@ public struct WorkspaceState: Codable, Equatable, Sendable {
67
69
  public let activeTabIndex: Int
68
70
  }
69
71
 
72
+ public struct WorkspaceSessionSummary: Codable, Equatable, Sendable {
73
+ public let name: String
74
+ public let tabs: [WorkspaceSessionTab]
75
+ public let createdAt: Int
76
+ }
77
+
78
+ public struct WorkspaceSessionTab: Codable, Equatable, Sendable {
79
+ public let id: Int
80
+ public let title: String
81
+ public let ended: Bool
82
+ public let clients: Int
83
+ public let restored: Bool
84
+ }
85
+
86
+ public struct ConnectedClientInfo: Codable, Equatable, Sendable {
87
+ public let clientId: String
88
+ public let role: String
89
+ public let session: String?
90
+ public let tabId: Int?
91
+ }
92
+
93
+ public struct WorkspaceSnapshot: Codable, Equatable, Sendable {
94
+ public let sessions: [WorkspaceSessionSummary]
95
+ public let clients: [ConnectedClientInfo]
96
+
97
+ public init(sessions: [WorkspaceSessionSummary], clients: [ConnectedClientInfo]) {
98
+ self.sessions = sessions
99
+ self.clients = clients
100
+ }
101
+ }
102
+
103
+ public struct AttachedPayload: Codable, Equatable, Sendable {
104
+ public let tabId: Int
105
+ public let session: String
106
+ public let clientId: String
107
+ public let role: String
108
+ }
109
+
110
+ public struct ServerInspectMeta: Codable, Equatable, Sendable {
111
+ public let session: String
112
+ public let tabId: Int?
113
+ public let tabTitle: String
114
+ public let cols: Int
115
+ public let rows: Int
116
+ public let timestamp: Int
117
+ }
118
+
119
+ public struct ServerInspectResult: Codable, Equatable, Sendable {
120
+ public let text: String
121
+ public let meta: ServerInspectMeta
122
+ }
123
+
70
124
  public struct InspectHighlight: Codable, Equatable, Sendable {
71
125
  public let start: Int
72
126
  public let end: Int
@@ -177,6 +231,16 @@ public struct LegacyWorkspaceState: Codable, Equatable, Sendable {
177
231
  public let activeTabIndex: Int
178
232
  }
179
233
 
234
+ public struct CurrentWorkspaceStatePayload: Codable, Equatable, Sendable {
235
+ public let sessions: [WorkspaceSessionSummary]
236
+ public let clients: [ConnectedClientInfo]
237
+ }
238
+
239
+ public struct BootstrapPayload: Codable, Equatable, Sendable {
240
+ public let sessions: [WorkspaceSessionSummary]
241
+ public let clients: [ConnectedClientInfo]
242
+ }
243
+
180
244
  public struct LegacyInspectRequest: Codable, Equatable, Sendable {
181
245
  public let type: String
182
246
  public let scope: String