serve-sim-sjchmiela 0.1.44 → 0.1.45

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.
@@ -85,8 +85,17 @@ final class WebRTCPublisher {
85
85
  config.sdpSemantics = .unifiedPlan
86
86
  config.bundlePolicy = .maxBundle
87
87
  config.rtcpMuxPolicy = .require
88
+ config.candidateNetworkPolicy = .all
88
89
  config.continualGatheringPolicy = .gatherOnce
89
90
  config.iceServers = iceServers(from: request.iceServers)
91
+ if hasCredentialedTurnServer(request.iceServers) {
92
+ config.iceTransportPolicy = .relay
93
+ print("[webrtc] ICE transport policy: relay")
94
+ } else {
95
+ config.iceTransportPolicy = .all
96
+ print("[webrtc] ICE transport policy: all")
97
+ }
98
+ print("[webrtc] ICE servers: \(iceServerSummary(request.iceServers))")
90
99
 
91
100
  let constraints = LKRTCMediaConstraints(
92
101
  mandatoryConstraints: nil,
@@ -131,6 +140,12 @@ final class WebRTCPublisher {
131
140
  }
132
141
  session.waitForIceGathering {
133
142
  let local = peerConnection.localDescription ?? answer
143
+ let candidateCounts = self.iceCandidateCounts(in: local.sdp)
144
+ if self.hasCredentialedTurnServer(request.iceServers), candidateCounts["relay", default: 0] == 0 {
145
+ print("[webrtc] WARNING: no relay ICE candidates gathered for credentialed TURN offer; counts=\(candidateCounts)")
146
+ } else {
147
+ print("[webrtc] ICE candidates gathered: \(candidateCounts)")
148
+ }
134
149
  completion(.success(WebRTCAnswerPayload(
135
150
  type: LKRTCSessionDescription.string(for: local.type),
136
151
  sdp: local.sdp
@@ -172,13 +187,58 @@ final class WebRTCPublisher {
172
187
  WebRTCIceServerPayload(urls: ["stun:stun.l.google.com:19302"], username: nil, credential: nil),
173
188
  WebRTCIceServerPayload(urls: ["stun:stun1.l.google.com:19302"], username: nil, credential: nil),
174
189
  ]
175
- return servers.map { server in
176
- LKRTCIceServer(
177
- urlStrings: server.urls,
178
- username: server.username,
179
- credential: server.credential
180
- )
190
+ return servers.flatMap { server in
191
+ server.urls.map { url in
192
+ LKRTCIceServer(
193
+ urlStrings: [url],
194
+ username: server.username,
195
+ credential: server.credential
196
+ )
197
+ }
198
+ }
199
+ }
200
+
201
+ private func hasCredentialedTurnServer(_ payload: [WebRTCIceServerPayload]?) -> Bool {
202
+ (payload ?? []).contains { server in
203
+ guard
204
+ let username = server.username, !username.isEmpty,
205
+ let credential = server.credential, !credential.isEmpty
206
+ else {
207
+ return false
208
+ }
209
+ return server.urls.contains { $0.lowercased().hasPrefix("turn:") || $0.lowercased().hasPrefix("turns:") }
210
+ }
211
+ }
212
+
213
+ private func iceServerSummary(_ payload: [WebRTCIceServerPayload]?) -> String {
214
+ let servers = payload ?? []
215
+ let stunUrls = servers.flatMap { server in
216
+ server.urls.filter { $0.lowercased().hasPrefix("stun:") }
217
+ }.count
218
+ let turnUrls = servers.flatMap { server in
219
+ server.urls.filter { $0.lowercased().hasPrefix("turn:") || $0.lowercased().hasPrefix("turns:") }
220
+ }.count
221
+ let credentialedTurnServers = servers.filter { server in
222
+ let hasCredentials = !(server.username ?? "").isEmpty && !(server.credential ?? "").isEmpty
223
+ return hasCredentials && server.urls.contains {
224
+ $0.lowercased().hasPrefix("turn:") || $0.lowercased().hasPrefix("turns:")
225
+ }
226
+ }.count
227
+ return "servers=\(servers.count) stunUrls=\(stunUrls) turnUrls=\(turnUrls) credentialedTurnServers=\(credentialedTurnServers)"
228
+ }
229
+
230
+ private func iceCandidateCounts(in sdp: String) -> [String: Int] {
231
+ var counts: [String: Int] = [:]
232
+ for line in sdp.split(separator: "\n") {
233
+ guard line.hasPrefix("a=candidate:") else { continue }
234
+ let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" })
235
+ if let typeIndex = parts.firstIndex(of: "typ"), parts.indices.contains(parts.index(after: typeIndex)) {
236
+ counts[String(parts[parts.index(after: typeIndex)]), default: 0] += 1
237
+ } else {
238
+ counts["unknown", default: 0] += 1
239
+ }
181
240
  }
241
+ return counts
182
242
  }
183
243
 
184
244
  private func applyVideoCodecPreference(_ codec: String?, to transceiver: LKRTCRtpTransceiver) {
@@ -251,14 +311,32 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
251
311
  print("[webrtc] ICE connection state: \(newState.rawValue)")
252
312
  }
253
313
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didChange newState: LKRTCIceGatheringState) {
314
+ print("[webrtc] ICE gathering state: \(newState.rawValue)")
254
315
  if newState == .complete {
255
316
  let completion = onIceGatheringComplete
256
317
  onIceGatheringComplete = nil
257
318
  completion?()
258
319
  }
259
320
  }
260
- func peerConnection(_ peerConnection: LKRTCPeerConnection, didGenerate candidate: LKRTCIceCandidate) {}
321
+ func peerConnection(_ peerConnection: LKRTCPeerConnection, didGenerate candidate: LKRTCIceCandidate) {
322
+ print("[webrtc] ICE candidate gathered: \(candidateSummary(candidate))")
323
+ }
261
324
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didRemove candidates: [LKRTCIceCandidate]) {}
325
+ func peerConnection(
326
+ _ peerConnection: LKRTCPeerConnection,
327
+ didChangeLocalCandidate local: LKRTCIceCandidate,
328
+ remoteCandidate remote: LKRTCIceCandidate,
329
+ lastReceivedMs: Int32,
330
+ changeReason: String
331
+ ) {
332
+ print("[webrtc] ICE selected pair: local=\(candidateSummary(local)) remote=\(candidateSummary(remote)) reason=\(changeReason) lastReceivedMs=\(lastReceivedMs)")
333
+ }
334
+ func peerConnection(
335
+ _ peerConnection: LKRTCPeerConnection,
336
+ didFailToGatherIceCandidate event: LKRTCIceCandidateErrorEvent
337
+ ) {
338
+ print("[webrtc] ICE candidate error: url=\(event.url) code=\(event.errorCode) text=\(event.errorText)")
339
+ }
262
340
  func peerConnection(_ peerConnection: LKRTCPeerConnection, didOpen dataChannel: LKRTCDataChannel) {
263
341
  print("[webrtc] viewer opened data channel: \(dataChannel.label)")
264
342
  dataChannel.delegate = self
@@ -269,4 +347,19 @@ private final class WebRTCSessionDelegate: NSObject, LKRTCPeerConnectionDelegate
269
347
  func dataChannel(_ dataChannel: LKRTCDataChannel, didReceiveMessageWith buffer: LKRTCDataBuffer) {
270
348
  onInput(buffer.data)
271
349
  }
350
+
351
+ private func candidateSummary(_ candidate: LKRTCIceCandidate) -> String {
352
+ let parts = candidate.sdp.split(whereSeparator: { $0 == " " || $0 == "\t" })
353
+ let protocolName = parts.indices.contains(2) ? String(parts[2]).lowercased() : "?"
354
+ let address = parts.indices.contains(4) ? String(parts[4]) : "?"
355
+ let port = parts.indices.contains(5) ? String(parts[5]) : "?"
356
+ let type: String
357
+ if let typeIndex = parts.firstIndex(of: "typ"), parts.indices.contains(parts.index(after: typeIndex)) {
358
+ type = String(parts[parts.index(after: typeIndex)])
359
+ } else {
360
+ type = "unknown"
361
+ }
362
+ let server = candidate.serverUrl?.isEmpty == false ? " server=\(candidate.serverUrl!)" : ""
363
+ return "type=\(type) protocol=\(protocolName) address=\(address) port=\(port)\(server)"
364
+ }
272
365
  }
Binary file
package/dist/serve-sim.js CHANGED
@@ -139,7 +139,7 @@ Usage:
139
139
  Permissions: ${D4().join(", ")}`),process.exit(1);let Y=J.device?o(J.device):Q$();if(!Y)console.error("No booted simulator. Boot one or pass -d <udid|name>."),process.exit(1);if(J.verb==="list"){let X={udid:Y,bundleId:J.bundleId??null,tcc:u7(Y,J.bundleId),location:c7(Y,J.bundleId),notifications:d7(Y,J.bundleId)};console.log(JSON.stringify(X,null,Q?0:2)),process.exit(0)}let G=J.bundleId;try{if(J.permission==="all")for(let X of D4())x5(Y,"reset",X,void 0,G);else x5(Y,J.verb,J.permission,J.value,G)}catch(X){console.error(X?.message??String(X)),process.exit(1)}if(Q)console.log(JSON.stringify({udid:Y,verb:J.verb,permission:J.permission,value:J.value??null,bundleId:G}));else{let X=J.value?` (${J.value})`:"";console.log(`\uD83D\uDD10 ${J.verb} ${J.permission}${X} for ${G} on ${Y}`)}process.exit(0)}b4();g0();import{spawn as bZ}from"child_process";import{randomBytes as vZ}from"crypto";var hZ=/https:\/\/[a-z0-9-]+\.trycloudflare\.com/,Y6=30000,J6="cloudflared not found on PATH. Install it with `brew install cloudflared` (macOS) or see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/.";function G6($){return`${$.toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40)||"serve-sim"}-${vZ(4).toString("hex")}`}function X6($,Q){if((Q?.provider??"cloudflare")==="ngrok")return gZ($,{timeoutMs:Q?.timeoutMs,domain:Q?.domain,label:Q?.label});return uZ($,{timeoutMs:Q?.timeoutMs,protocol:Q?.protocol})}function uZ($,Q){let Z=Q?.timeoutMs??Y6,J=Q?.protocol;return new Promise((Y,G)=>{let X=["tunnel","--no-autoupdate",...J?["--protocol",J]:[],"--url",`http://localhost:${$}`],V;try{V=bZ("cloudflared",X,{stdio:["ignore","pipe","pipe"]})}catch(O){G(O.code==="ENOENT"?Error(J6):O);return}let z=!1,K="",W=()=>{clearTimeout(j),V.stdout?.off("data",H),V.stderr?.off("data",H),V.off("error",L),V.off("exit",T)},H=(O)=>{if(K+=typeof O==="string"?O:O.toString(),K.length>65536)K=K.slice(-32768);let q=K.match(hZ);if(q&&!z)z=!0,W(),Y({url:q[0],pid:V.pid,child:V,stop:()=>{try{V.kill("SIGTERM")}catch{}}})},L=(O)=>{if(z)return;z=!0,W();try{V.kill()}catch{}G(O.code==="ENOENT"?Error(J6):O)},T=(O)=>{if(z)return;z=!0,W();let q=K.split(`
140
140
  `).slice(-5).join(`
141
141
  `).trim();G(Error(`cloudflared exited (code ${O}) before producing a URL`+(q?`:
142
- ${q}`:"")))},j=setTimeout(()=>{if(z)return;z=!0,W();try{V.kill()}catch{}G(Error(`cloudflared did not produce a URL within ${Z}ms`))},Z);V.stdout?.on("data",H),V.stderr?.on("data",H),V.on("error",L),V.on("exit",T)})}async function gZ($,Q){let Z=Q?.timeoutMs??Y6,J=await cZ(),Y=Q?.domain?lZ(Q.domain,Q.label):void 0,G={addr:$};if(process.env.NGROK_AUTHTOKEN)G.authtoken=process.env.NGROK_AUTHTOKEN;if(Y)G.domain=Y;let X=await mZ(J(G),Z,`ngrok did not produce a URL within ${Z}ms`,(z)=>{z.close()}),V=X.url();if(!V)throw await X.close().catch(()=>{}),Error("ngrok started but did not return a URL");return{url:V,stop:()=>{X.close().catch(()=>{})}}}async function cZ(){try{let $=await import("@ngrok/ngrok"),Q=$.forward??$.default?.forward;if(typeof Q!=="function")throw Error("@ngrok/ngrok does not export forward()");return Q}catch($){let Q=$ instanceof Error?$.message:String($);throw Error(`Unable to load @ngrok/ngrok: ${Q}. Run \`bun install\` or install the package before using --tunnel-provider ngrok.`)}}function lZ($,Q){let Z=$.replace(/^https?:\/\//i,"").replace(/^\*\./,"").replace(/\/+$/,"");if(!Q)return Z;return`${Q}.${Z}`}function mZ($,Q,Z,J){let Y=!1;return new Promise((G,X)=>{let V=setTimeout(()=>{Y=!0,X(Error(Z))},Q);$.then((z)=>{if(clearTimeout(V),Y){J?.(z);return}G(z)},(z)=>{clearTimeout(V),X(z)})})}var D$=x0(import.meta.url);function E3(){return"0.1.44"}function G4($,Q){let Z=Number($);if(!Number.isInteger(Z)||Z<=0)throw new D0(`${Q} must be a positive integer.`);return Z}function T3($){let Q=Number($);if(!Number.isFinite(Q)||Q<0||Q>1)throw new D0("--stream-quality must be a number between 0 and 1.");return Q}var x9=new Set;function j3($){return x9.add($),$}function O3(){for(let $ of x9)try{$.stop()}catch{}x9.clear()}process.on("exit",O3);function d8(){if(!i(M$))y9(M$,{recursive:!0})}function e($){if($)return S9(X0($));for(let Q of P0()){let Z=S9(Q);if(Z)return Z}return null}var X4={at:0,booted:null};function D3(){let $=Date.now();if(X4.booted&&$-X4.at<1000)return X4.booted;try{let Q=s("xcrun simctl list devices booted -j",{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:3000}),Z=JSON.parse(Q),J=new Set;for(let Y of Object.values(Z.devices))for(let G of Y)if(G.state==="Booted")J.add(G.udid);return X4={at:$,booted:J},J}catch{return null}}function S9($){try{if(!i($))return R$("state file missing %s",$),null;let Q=JSON.parse(W$($,"utf-8"));try{process.kill(Q.pid,0)}catch{return R$("helper pid %d dead, removing stale state %s",Q.pid,$),s$($),null}let Z=D3();if(Z&&!Z.has(Q.device)){R$("helper pid %d bound to non-booted device %s — killing stale helper",Q.pid,Q.device),console.error(`[serve-sim] Helper pid ${Q.pid} is bound to device ${Q.device} which is no longer booted — killing stale helper.`);try{process.kill(Q.pid,"SIGTERM")}catch{}try{s$($)}catch{}return null}return R$("state ok pid=%d device=%s port=%d",Q.pid,Q.device,Q.port),Q}catch(Q){return R$("readStateFile threw for %s: %o",$,Q),null}}function E0(){let $=[];for(let Q of P0()){let Z=S9(Q);if(Z)$.push(Z)}return $}function A3($){d8(),v9(X0($.device),JSON.stringify($,null,2)),R$("wrote state pid=%d device=%s port=%d",$.pid,$.device,$.port)}function v$($){if($){R$("clearState device=%s",$);try{s$(X0($))}catch{}}else{R$("clearState (all)");for(let Q of P0())try{s$(Q)}catch{}}}function h9(){try{let $=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Q=JSON.parse($),Z=Object.keys(Q.devices).filter((J)=>/SimRuntime\.iOS-/i.test(J)).sort((J,Y)=>{let G=(J.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number),X=(Y.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number);return(X[0]??0)-(G[0]??0)||(X[1]??0)-(G[1]??0)});for(let J of Z){let G=(Q.devices[J]??[]).find((X)=>X.isAvailable!==!1&&/^iPhone\b/i.test(X.name));if(G)return{udid:G.udid,name:G.name}}}catch{}return null}function l8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.name}catch{}return null}function n8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.state==="Booted"}catch{}return!1}function z4($){try{return process.kill($,0),!0}catch{return!1}}function i8($){try{process.kill($,"SIGTERM")}catch{return}let Q=Date.now()+500;while(Date.now()<Q)try{process.kill($,0),q$(25)}catch{return}try{process.kill($,"SIGKILL")}catch{}let Z=Date.now()+500;while(Date.now()<Z)try{process.kill($,0),q$(25)}catch{return}}function q3($){if(!n8($))try{s(`xcrun simctl boot ${$}`,{encoding:"utf-8",stdio:"pipe"})}catch(Q){let Z=(Q.stderr??Q.message??"").toLowerCase();if(!Z.includes("booted")&&!Z.includes("current state"))throw Error(`Failed to boot device ${$}: ${Q.stderr||Q.message}`)}try{s("open -ga Simulator",{encoding:"utf-8",stdio:"pipe",timeout:3000})}catch{}}function C3(){let $=F3();for(let Q of Object.values($))for(let Z of Q??[])if(Z.family==="IPv4"&&!Z.internal)return Z.address;return null}async function o8($){let Q=new Set(E0().map((Z)=>Z.port));for(let Z=$;Z<$+100;Z++){if(Q.has(Z))continue;if(await C5(Z))return Z}throw Error(`No available port found in range ${$}-${$+99}`)}async function P3($){q3($);try{s(`xcrun simctl bootstatus ${$} -b`,{encoding:"utf-8",stdio:"pipe",timeout:60000})}catch(Q){if(!n8($))console.error(`Device ${$} failed to reach booted state: ${Q.stderr||Q.message}`),process.exit(1)}}function I3($){if(process.argv[0]&&/(^|\/)serve-sim$/.test(process.argv[0]))return{command:process.argv[0],args:$};return{command:process.argv[0],args:[process.argv[1],...$]}}async function w3($,Q=150000){let Z=Date.now();while(Date.now()-Z<Q){let J=e($);if(J)return J;await new Promise((Y)=>setTimeout(Y,200))}return null}async function a8($,Q,Z){g4("startHelper udid=%s port=%d detach=%s",$,Q,Z.detach);let J="127.0.0.1";d8(),v$($),I5(Q);let Y=J$(M$,`server-${$}.log`),G=f9(Y,"w"),{command:X,args:V}=I3([$,"--port",String(Q),"--host",J]),z=m8(X,V,{detached:Z.detach,stdio:["ignore",G,G]});if(b9(G),Z.detach)z.unref();let K=await w3($);if(!K){if(z.pid)i8(z.pid);let W="";try{W=W$(Y,"utf-8").trim()}catch{}console.error(W?`Preview server failed:
142
+ ${q}`:"")))},j=setTimeout(()=>{if(z)return;z=!0,W();try{V.kill()}catch{}G(Error(`cloudflared did not produce a URL within ${Z}ms`))},Z);V.stdout?.on("data",H),V.stderr?.on("data",H),V.on("error",L),V.on("exit",T)})}async function gZ($,Q){let Z=Q?.timeoutMs??Y6,J=await cZ(),Y=Q?.domain?lZ(Q.domain,Q.label):void 0,G={addr:$};if(process.env.NGROK_AUTHTOKEN)G.authtoken=process.env.NGROK_AUTHTOKEN;if(Y)G.domain=Y;let X=await mZ(J(G),Z,`ngrok did not produce a URL within ${Z}ms`,(z)=>{z.close()}),V=X.url();if(!V)throw await X.close().catch(()=>{}),Error("ngrok started but did not return a URL");return{url:V,stop:()=>{X.close().catch(()=>{})}}}async function cZ(){try{let $=await import("@ngrok/ngrok"),Q=$.forward??$.default?.forward;if(typeof Q!=="function")throw Error("@ngrok/ngrok does not export forward()");return Q}catch($){let Q=$ instanceof Error?$.message:String($);throw Error(`Unable to load @ngrok/ngrok: ${Q}. Run \`bun install\` or install the package before using --tunnel-provider ngrok.`)}}function lZ($,Q){let Z=$.replace(/^https?:\/\//i,"").replace(/^\*\./,"").replace(/\/+$/,"");if(!Q)return Z;return`${Q}.${Z}`}function mZ($,Q,Z,J){let Y=!1;return new Promise((G,X)=>{let V=setTimeout(()=>{Y=!0,X(Error(Z))},Q);$.then((z)=>{if(clearTimeout(V),Y){J?.(z);return}G(z)},(z)=>{clearTimeout(V),X(z)})})}var D$=x0(import.meta.url);function E3(){return"0.1.45"}function G4($,Q){let Z=Number($);if(!Number.isInteger(Z)||Z<=0)throw new D0(`${Q} must be a positive integer.`);return Z}function T3($){let Q=Number($);if(!Number.isFinite(Q)||Q<0||Q>1)throw new D0("--stream-quality must be a number between 0 and 1.");return Q}var x9=new Set;function j3($){return x9.add($),$}function O3(){for(let $ of x9)try{$.stop()}catch{}x9.clear()}process.on("exit",O3);function d8(){if(!i(M$))y9(M$,{recursive:!0})}function e($){if($)return S9(X0($));for(let Q of P0()){let Z=S9(Q);if(Z)return Z}return null}var X4={at:0,booted:null};function D3(){let $=Date.now();if(X4.booted&&$-X4.at<1000)return X4.booted;try{let Q=s("xcrun simctl list devices booted -j",{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:3000}),Z=JSON.parse(Q),J=new Set;for(let Y of Object.values(Z.devices))for(let G of Y)if(G.state==="Booted")J.add(G.udid);return X4={at:$,booted:J},J}catch{return null}}function S9($){try{if(!i($))return R$("state file missing %s",$),null;let Q=JSON.parse(W$($,"utf-8"));try{process.kill(Q.pid,0)}catch{return R$("helper pid %d dead, removing stale state %s",Q.pid,$),s$($),null}let Z=D3();if(Z&&!Z.has(Q.device)){R$("helper pid %d bound to non-booted device %s — killing stale helper",Q.pid,Q.device),console.error(`[serve-sim] Helper pid ${Q.pid} is bound to device ${Q.device} which is no longer booted — killing stale helper.`);try{process.kill(Q.pid,"SIGTERM")}catch{}try{s$($)}catch{}return null}return R$("state ok pid=%d device=%s port=%d",Q.pid,Q.device,Q.port),Q}catch(Q){return R$("readStateFile threw for %s: %o",$,Q),null}}function E0(){let $=[];for(let Q of P0()){let Z=S9(Q);if(Z)$.push(Z)}return $}function A3($){d8(),v9(X0($.device),JSON.stringify($,null,2)),R$("wrote state pid=%d device=%s port=%d",$.pid,$.device,$.port)}function v$($){if($){R$("clearState device=%s",$);try{s$(X0($))}catch{}}else{R$("clearState (all)");for(let Q of P0())try{s$(Q)}catch{}}}function h9(){try{let $=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Q=JSON.parse($),Z=Object.keys(Q.devices).filter((J)=>/SimRuntime\.iOS-/i.test(J)).sort((J,Y)=>{let G=(J.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number),X=(Y.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number);return(X[0]??0)-(G[0]??0)||(X[1]??0)-(G[1]??0)});for(let J of Z){let G=(Q.devices[J]??[]).find((X)=>X.isAvailable!==!1&&/^iPhone\b/i.test(X.name));if(G)return{udid:G.udid,name:G.name}}}catch{}return null}function l8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.name}catch{}return null}function n8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.state==="Booted"}catch{}return!1}function z4($){try{return process.kill($,0),!0}catch{return!1}}function i8($){try{process.kill($,"SIGTERM")}catch{return}let Q=Date.now()+500;while(Date.now()<Q)try{process.kill($,0),q$(25)}catch{return}try{process.kill($,"SIGKILL")}catch{}let Z=Date.now()+500;while(Date.now()<Z)try{process.kill($,0),q$(25)}catch{return}}function q3($){if(!n8($))try{s(`xcrun simctl boot ${$}`,{encoding:"utf-8",stdio:"pipe"})}catch(Q){let Z=(Q.stderr??Q.message??"").toLowerCase();if(!Z.includes("booted")&&!Z.includes("current state"))throw Error(`Failed to boot device ${$}: ${Q.stderr||Q.message}`)}try{s("open -ga Simulator",{encoding:"utf-8",stdio:"pipe",timeout:3000})}catch{}}function C3(){let $=F3();for(let Q of Object.values($))for(let Z of Q??[])if(Z.family==="IPv4"&&!Z.internal)return Z.address;return null}async function o8($){let Q=new Set(E0().map((Z)=>Z.port));for(let Z=$;Z<$+100;Z++){if(Q.has(Z))continue;if(await C5(Z))return Z}throw Error(`No available port found in range ${$}-${$+99}`)}async function P3($){q3($);try{s(`xcrun simctl bootstatus ${$} -b`,{encoding:"utf-8",stdio:"pipe",timeout:60000})}catch(Q){if(!n8($))console.error(`Device ${$} failed to reach booted state: ${Q.stderr||Q.message}`),process.exit(1)}}function I3($){if(process.argv[0]&&/(^|\/)serve-sim$/.test(process.argv[0]))return{command:process.argv[0],args:$};return{command:process.argv[0],args:[process.argv[1],...$]}}async function w3($,Q=150000){let Z=Date.now();while(Date.now()-Z<Q){let J=e($);if(J)return J;await new Promise((Y)=>setTimeout(Y,200))}return null}async function a8($,Q,Z){g4("startHelper udid=%s port=%d detach=%s",$,Q,Z.detach);let J="127.0.0.1";d8(),v$($),I5(Q);let Y=J$(M$,`server-${$}.log`),G=f9(Y,"w"),{command:X,args:V}=I3([$,"--port",String(Q),"--host",J]),z=m8(X,V,{detached:Z.detach,stdio:["ignore",G,G]});if(b9(G),Z.detach)z.unref();let K=await w3($);if(!K){if(z.pid)i8(z.pid);let W="";try{W=W$(Y,"utf-8").trim()}catch{}console.error(W?`Preview server failed:
143
143
  ${W}`:"Preview server failed to start"),process.exit(1)}return Z.detach?{pid:K.pid}:{pid:K.pid,child:z}}async function x3($,Q,Z){u4("follow devices=%o startPort=%d",$,Q);let J=$.length>0?$.map(o):(()=>{let K=Q$();if(K)return[K];let W=h9();if(!W)console.error("No device specified and no available iOS simulator found."),process.exit(1);if(!Z)console.log(`No booted simulator — booting ${W.name}...`);return[W.udid]})(),Y=new Map,G=[],X=Q;for(let K of J){let W=e(K);if(W){if(!Z){let T=l8(K)??K;if(J.length>1)console.log(`
144
144
  ==> ${T} (${K}) <==`);console.log(` Already running on port ${W.port}`),console.log(` Stream: ${W.streamUrl}`),console.log(` WebSocket: ${W.wsUrl}`)}G.push(W);continue}X=await o8(X);let{child:H}=await a8(K,X,{detach:!1});if(H)Y.set(K,H);let L=e(K)??p$(K,X,"/","127.0.0.1");if(G.push(L),!Z){let T=l8(K)??K;if(J.length>1)console.log(`
145
145
  ==> ${T} (${K}) <==`);console.log(` Stream: ${L.streamUrl}`),console.log(` WebSocket: ${L.wsUrl}`),console.log(` Port: ${X}`)}X++}if(G.length===1){let K=G[0];console.log(JSON.stringify({url:K.url,streamUrl:K.streamUrl,wsUrl:K.wsUrl,port:K.port,device:K.device}))}else console.log(JSON.stringify({devices:G.map((K)=>({url:K.url,streamUrl:K.streamUrl,wsUrl:K.wsUrl,port:K.port,device:K.device}))}));if(Y.size===0)return;let V=!1,z=(K)=>{if(V)return;if(V=!0,!Z)console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serve-sim-sjchmiela",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Stanislaw Chmiela",