tail-sim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tail-sim.js +34 -0
- package/package.json +26 -0
package/dist/tail-sim.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import{execSync as H,spawn as v}from"child_process";import{existsSync as m,mkdirSync as Gz,openSync as g,closeSync as n,readFileSync as I,unlinkSync as j,writeFileSync as Jz}from"fs";import{join as Kz,resolve as Qz}from"path";import{tmpdir as zz}from"os";import{join as _}from"path";import{readdirSync as Cz}from"fs";var U=_(zz(),"tail-sim"),kz=_(U,"server.json");function k(z){return _(U,`server-${z}.json`)}function q(){try{return Cz(U).filter((z)=>z.startsWith("server-")&&z.endsWith(".json")).map((z)=>_(U,z))}catch{return[]}}function F(){if(!m(U))Gz(U,{recursive:!0})}function R(z){if(z)return T(k(z));for(let G of q()){let C=T(G);if(C)return C}return null}function T(z){try{if(!m(z))return null;let G=JSON.parse(I(z,"utf-8"));try{return process.kill(G.pid,0),G}catch{return j(z),null}}catch{return null}}function y(){let z=[];for(let G of q()){let C=T(G);if(C)z.push(C)}return z}function A(z){F(),Jz(k(z.device),JSON.stringify(z,null,2))}function b(z){if(z)try{j(k(z))}catch{}else for(let G of q())try{j(G)}catch{}}function Vz(){let z=Qz(import.meta.dir,"../bin/tail-sim-bin");if(m(z))return z;throw Error(`tail-sim-bin binary not found. Run 'bun run build:swift' first.
|
|
4
|
+
Checked: ${z}`)}function p(){try{let z=H("xcrun simctl list devices booted -j",{encoding:"utf-8"}),G=JSON.parse(z);for(let C of Object.values(G.devices))for(let K of C)if(K.state==="Booted")return K.udid}catch{}return null}function D(z){try{let G=H("xcrun simctl list devices -j",{encoding:"utf-8"}),C=JSON.parse(G);for(let K of Object.values(C.devices))for(let V of K)if(V.udid===z)return V.name}catch{}return null}function E(z){if(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(z))return z;try{let G=H("xcrun simctl list devices -j",{encoding:"utf-8"}),C=JSON.parse(G);for(let K of Object.values(C.devices))for(let V of K)if(V.name.toLowerCase()===z.toLowerCase())return V.udid}catch{}console.error(`Could not resolve device: ${z}`),process.exit(1)}function O(z){try{let G=H("xcrun simctl list devices -j",{encoding:"utf-8"}),C=JSON.parse(G);for(let K of Object.values(C.devices))for(let V of K)if(V.udid===z)return V.state==="Booted"}catch{}return!1}function w(z){try{return process.kill(z,0),!0}catch{return!1}}function h(z){try{process.kill(z,"SIGTERM")}catch{return}let G=Date.now()+500;while(Date.now()<G)try{process.kill(z,0),Bun.sleepSync(25)}catch{return}try{process.kill(z,"SIGKILL")}catch{}let C=Date.now()+500;while(Date.now()<C)try{process.kill(z,0),Bun.sleepSync(25)}catch{return}}function Wz(z){try{let G=H(`lsof -ti tcp:${z}`,{encoding:"utf-8",stdio:"pipe"}).trim();if(G){let C=process.pid;for(let K of G.split(`
|
|
5
|
+
`)){let V=parseInt(K,10);if(V!==C)try{process.kill(V,"SIGKILL")}catch{}}Bun.sleepSync(100)}}catch{}}function u(z){if(O(z))return;try{H(`xcrun simctl boot ${z}`,{encoding:"utf-8",stdio:"pipe"})}catch(G){let C=(G.stderr??G.message??"").toLowerCase();if(C.includes("booted")||C.includes("current state"))return;throw Error(`Failed to boot device ${z}: ${G.stderr||G.message}`)}}function Yz(z){try{H(`xcrun simctl shutdown ${z}`,{encoding:"utf-8",stdio:"pipe"})}catch{}u(z)}async function c(z){let G=new Set(y().map((C)=>C.port));for(let C=z;C<z+100;C++){if(G.has(C))continue;try{return Bun.serve({port:C,fetch:()=>new Response("ok")}).stop(!0),C}catch{continue}}throw Error(`No available port found in range ${z}-${z+99}`)}async function S(z){if(!O(z)){u(z);let G=Date.now()+15000;while(!O(z)&&Date.now()<G)await new Promise((C)=>setTimeout(C,200));if(!O(z))console.error(`Device ${z} failed to boot within 15 seconds.`),process.exit(1)}}async function l(z,G,C,K){let V=!1;for(let Q=0;Q<30;Q++){if(!K())break;try{if((await fetch(`${G}/health`)).ok){V=!0;break}}catch{}await new Promise((Y)=>setTimeout(Y,100))}if(V){let Q=Date.now()+8000;while(Date.now()<Q){if(await new Promise((Y)=>setTimeout(Y,200)),!K()){V=!1;break}try{if(I(C,"utf-8").includes("Capture started"))break}catch{}}}let W="";try{W=I(C,"utf-8").trim()}catch{}return{ready:V,log:W}}async function Zz(z){let{helperPath:G,udid:C,port:K,host:V,logFile:W}=z,Q=`http://${V}:${K}`;F();let Y=g(W,"w"),J=v(G,[C,"--port",String(K)],{detached:!0,stdio:["ignore",Y,Y]});J.unref(),n(Y);let Z=J.pid,X=!1;J.once("exit",()=>{X=!0});let{ready:L,log:M}=await l(Z,Q,W,()=>!X&&w(Z));return{ready:L,pid:Z,exited:X||!w(Z),log:M}}async function $z(z){let{helperPath:G,udid:C,port:K,host:V,logFile:W}=z,Q=`http://${V}:${K}`;F();let Y=g(W,"w"),J=v(G,[C,"--port",String(K)],{detached:!1,stdio:["ignore",Y,Y]});n(Y);let Z=J.pid,X=!1;J.once("exit",()=>{X=!0});let{ready:L,log:M}=await l(Z,Q,W,()=>!X&&w(Z));return{ready:L,child:J,log:M}}async function o(z,G,C){await S(z);let K="127.0.0.1",V=Vz(),W=Kz(U,`server-${z}.log`),Q={helperPath:V,udid:z,port:G,host:K,logFile:W},Y="",J=!1,Z=3;for(let L=1;L<=Z;L++){if(Wz(G),C.detach){let $=await Zz(Q);if($.ready){let B={pid:$.pid,port:G,device:z,url:`http://${K}:${G}`,streamUrl:`http://${K}:${G}/stream.mjpeg`,wsUrl:`ws://${K}:${G}/ws`};return A(B),{pid:$.pid}}h($.pid),Y=$.log}else{let $=await $z(Q);if($.ready){let B={pid:$.child.pid,port:G,device:z,url:`http://${K}:${G}`,streamUrl:`http://${K}:${G}/stream.mjpeg`,wsUrl:`ws://${K}:${G}/ws`};return A(B),{pid:$.child.pid,child:$.child}}h($.child.pid),Y=$.log}if(Y.includes("framebufferSurface not available")&&!J)console.error("[tail-sim] Framebuffer unavailable \u2014 restarting simulator..."),J=!0,Yz(z),await S(z);else if(L<Z)await new Promise(($)=>setTimeout($,500))}let X=Y?`Helper failed:
|
|
6
|
+
${Y}`:"Helper process failed to start";console.error(X),process.exit(1)}async function Xz(z,G,C){let K=z.length>0?z.map(E):(()=>{let J=p();if(!J)console.error("No device specified and no booted simulator found."),process.exit(1);return[J]})(),V=new Map,W=[],Q=G;for(let J of K){let Z=R(J);if(Z){if(!C){let B=D(J)??J;if(K.length>1)console.error(`
|
|
7
|
+
==> ${B} (${J}) <==`);console.error(` Already running on port ${Z.port}`),console.error(` Stream: ${Z.streamUrl}`),console.error(` WebSocket: ${Z.wsUrl}`)}W.push(Z);continue}Q=await c(Q);let{pid:X,child:L}=await o(J,Q,{detach:!1});if(L)V.set(J,L);let M="127.0.0.1",$={pid:X,port:Q,device:J,url:`http://${M}:${Q}`,streamUrl:`http://${M}:${Q}/stream.mjpeg`,wsUrl:`ws://${M}:${Q}/ws`};if(W.push($),!C){let B=D(J)??J;if(K.length>1)console.error(`
|
|
8
|
+
==> ${B} (${J}) <==`);console.error(` Stream: ${$.streamUrl}`),console.error(` WebSocket: ${$.wsUrl}`),console.error(` Port: ${Q}`)}Q++}if(W.length===1){let J=W[0];console.log(JSON.stringify({url:J.url,streamUrl:J.streamUrl,wsUrl:J.wsUrl,port:J.port,device:J.device}))}else console.log(JSON.stringify({devices:W.map((J)=>({url:J.url,streamUrl:J.streamUrl,wsUrl:J.wsUrl,port:J.port,device:J.device}))}));if(V.size===0)return;for(let[J,Z]of V)Z.on("exit",(X)=>{if(!C)console.error(`[${J}] Helper exited (code ${X})`);if(b(J),V.delete(J),V.size===0)process.exit(X??1)});let Y=()=>{if(!C)console.error(`
|
|
9
|
+
[tail-sim] Shutting down...`);for(let[J,Z]of V){try{process.kill(Z.pid,"SIGTERM")}catch{}b(J)}process.exit(0)};process.on("SIGINT",Y),process.on("SIGTERM",Y),await new Promise(()=>{})}async function Nz(z,G,C){let K=z.length>0?z.map(E):(()=>{let Q=p();if(!Q)console.error("No device specified and no booted simulator found."),process.exit(1);return[Q]})(),V=[],W=G;for(let Q of K){let Y=R(Q);if(Y){V.push(Y);continue}W=await c(W),await o(Q,W,{detach:!0});let J="127.0.0.1";V.push({pid:R(Q).pid,port:W,device:Q,url:`http://${J}:${W}`,streamUrl:`http://${J}:${W}/stream.mjpeg`,wsUrl:`ws://${J}:${W}/ws`}),W++}if(V.length===1){let Q=V[0];console.log(JSON.stringify({url:Q.url,streamUrl:Q.streamUrl,wsUrl:Q.wsUrl,port:Q.port,device:Q.device}))}else console.log(JSON.stringify({devices:V.map((Q)=>({url:Q.url,streamUrl:Q.streamUrl,wsUrl:Q.wsUrl,port:Q.port,device:Q.device}))}))}function Lz(z){if(z){let C=E(z),K=R(C);if(!K)console.log(JSON.stringify({running:!1,device:C}));else console.log(JSON.stringify({running:!0,url:K.url,streamUrl:K.streamUrl,wsUrl:K.wsUrl,port:K.port,device:K.device,pid:K.pid}));return}let G=y();if(G.length===0)console.log(JSON.stringify({running:!1}));else if(G.length===1){let C=G[0];console.log(JSON.stringify({running:!0,url:C.url,streamUrl:C.streamUrl,wsUrl:C.wsUrl,port:C.port,device:C.device,pid:C.pid}))}else console.log(JSON.stringify({running:!0,streams:G.map((C)=>({url:C.url,streamUrl:C.streamUrl,wsUrl:C.wsUrl,port:C.port,device:C.device,pid:C.pid}))}))}function Mz(z){if(z){let G=E(z),C=R(G);if(!C){console.log(JSON.stringify({disconnected:!0,device:G}));return}try{process.kill(C.pid,"SIGTERM")}catch{}b(G),console.log(JSON.stringify({disconnected:!0,device:C.device}))}else{let G=y();if(G.length===0){console.log(JSON.stringify({disconnected:!0,devices:[]}));return}let C=[];for(let K of G){try{process.kill(K.pid,"SIGTERM")}catch{}C.push(K.device)}b(),console.log(JSON.stringify({disconnected:!0,devices:C}))}}async function Uz(z){let G,C=[];for(let Q=0;Q<z.length;Q++)if(z[Q]==="--device"||z[Q]==="-d")G=z[++Q];else C.push(z[Q]);let K=R(G);if(!K)console.error("No tail-sim server running. Run `tail-sim` first."),process.exit(1);let V=C[0];if(!V)console.error("Usage: tail-sim gesture '<json>'"),console.error(`Example: tail-sim gesture '{"type":"begin","x":0.5,"y":0.5}'`),process.exit(1);let W;try{W=JSON.parse(V)}catch{console.error("Invalid JSON:",V),process.exit(1)}return new Promise((Q,Y)=>{let J=new WebSocket(K.wsUrl);J.binaryType="arraybuffer",J.onopen=()=>{let Z=new TextEncoder().encode(JSON.stringify(W)),X=new Uint8Array(1+Z.length);X[0]=3,X.set(Z,1),J.send(X),setTimeout(()=>{J.close(),Q()},50)},J.onerror=()=>{console.error("Failed to connect to tail-sim server at",K.wsUrl),Y(Error("WebSocket connection failed"))}})}async function Bz(z){let G,C=[];for(let W=0;W<z.length;W++)if(z[W]==="--device"||z[W]==="-d")G=z[++W];else C.push(z[W]);let K=R(G);if(!K)console.error("No tail-sim server running. Run `tail-sim` first."),process.exit(1);let V=C[0]??"home";return new Promise((W,Q)=>{let Y=new WebSocket(K.wsUrl);Y.binaryType="arraybuffer",Y.onopen=()=>{let J=new TextEncoder().encode(JSON.stringify({button:V})),Z=new Uint8Array(1+J.length);Z[0]=4,Z.set(J,1),Y.send(Z),setTimeout(()=>{Y.close(),W()},50)},Y.onerror=()=>{console.error("Failed to connect to tail-sim server at",K.wsUrl),Q(Error("WebSocket connection failed"))}})}function a(){console.log(`
|
|
10
|
+
tail-sim - Stream iOS Simulator to the browser
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
tail-sim [device...] Start streaming (foreground, Ctrl+C to stop)
|
|
14
|
+
tail-sim gesture '<json>' [-d udid] Send a touch gesture
|
|
15
|
+
tail-sim button [name] [-d udid] Send a button press (default: home)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-p, --port <port> Starting port (default: 3100, auto-allocates)
|
|
19
|
+
-d, --detach Spawn helper and exit (daemon mode)
|
|
20
|
+
-q, --quiet Suppress human-readable output, JSON only
|
|
21
|
+
--list [device] List running streams
|
|
22
|
+
--kill [device] Kill running stream(s)
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
tail-sim Auto-detect booted sim, stream in foreground
|
|
27
|
+
tail-sim "iPhone 16 Pro" Stream a specific device
|
|
28
|
+
tail-sim "iPhone 16 Pro" "iPhone 15" Stream multiple devices
|
|
29
|
+
tail-sim --detach Start streaming in background (daemon)
|
|
30
|
+
tail-sim --list Show all running streams
|
|
31
|
+
tail-sim --kill Stop all streams
|
|
32
|
+
tail-sim gesture '{"type":"begin","x":0.5,"y":0.5}'
|
|
33
|
+
tail-sim button home -d <udid>
|
|
34
|
+
`)}var N=process.argv.slice(2);if(N[0]==="gesture")await Uz(N.slice(1)),process.exit(0);if(N[0]==="button")await Bz(N.slice(1)),process.exit(0);var x=3100,r=!1,f=!1,d=!1,s=!1,i=!1,P=[],t,e;for(let z=0;z<N.length;z++){let G=N[z];switch(G){case"--port":case"-p":x=parseInt(N[++z]??"3100",10);break;case"--detach":case"-d":r=!0;break;case"--quiet":case"-q":f=!0;break;case"--list":case"-l":if(d=!0,N[z+1]&&!N[z+1].startsWith("-"))t=N[++z];break;case"--kill":case"-k":if(s=!0,N[z+1]&&!N[z+1].startsWith("-"))e=N[++z];break;case"--help":case"-h":case"help":i=!0;break;default:if(!G.startsWith("-"))P.push(G);else console.error(`Unknown flag: ${G}`),a(),process.exit(1)}}if(i)a(),process.exit(0);if(d)Lz(t),process.exit(0);if(s)Mz(e),process.exit(0);if(r)await Nz(P,x,f);else await Xz(P,x,f);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tail-sim",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tail-sim": "dist/tail-sim.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/tail-sim.js"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
"./state": {
|
|
13
|
+
"import": "./src/state.ts",
|
|
14
|
+
"types": "./src/state.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build --compile --minify src/index.ts --outfile dist/tail-sim && bun build src/index.ts --outfile dist/tail-sim.js --target bun --minify",
|
|
19
|
+
"build:swift": "./build.sh",
|
|
20
|
+
"dev": "bun run src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "latest",
|
|
24
|
+
"typescript": "^5.7.0"
|
|
25
|
+
}
|
|
26
|
+
}
|