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.
Files changed (2) hide show
  1. package/dist/tail-sim.js +34 -0
  2. package/package.json +26 -0
@@ -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
+ }