import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {run,workspace,adbBinary} from './lib.mjs'; export function selectTransport(raw) { const lines=raw.split('\n').filter(l=>l.includes('usb:')&&l.includes('model:FiiO_JM21')); const valid=lines.filter(l=>/^\S+\s+device\s/.test(l)); if(valid.length>1)throw new Error('More than one USB JM21 is authorized. Disconnect the other player.'); if(valid.length!==1){ if(lines.some(l=>/\sunauthorized(?:\s|$)/.test(l)))throw new Error('Unlock the JM21 and accept Allow USB debugging for this laptop.'); throw new Error('Connect one unlocked FiiO JM21 by a USB data cable and enable USB debugging. Network devices are ignored.'); } const id=valid[0].match(/transport_id:(\d+)/)?.[1]; if(!id)throw new Error('ADB did not report a transport ID. Update Android platform-tools.'); return id; } export function connect(root=workspace()) { const binary=adbBinary(root),transport=selectTransport(run(binary,['devices','-l'])); const adb=(args,options={})=>run(binary,['-t',transport,...args],options); const model=adb(['shell','getprop','ro.product.model']).trim(); if(model!=='FiiO JM21')throw new Error('USB model changed; refusing to use '+model); return {transport,adb}; } const decode=s=>s.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,"'").replace(/</g,'<').replace(/>/g,'>'); export function dump(d) { const status=d.adb(['shell','uiautomator','dump','/sdcard/chloe-window.xml']); if(!status.includes('UI hierchary dumped'))throw new Error('UI was not idle. Use a fresh screenshot; do not reuse stale coordinates.'); const xml=d.adb(['shell','cat','/sdcard/chloe-window.xml']); return [...xml.matchAll(/]+)>/g)].map(m=>Object.fromEntries([...m[1].matchAll(/([\w-]+)="([^"]*)"/g)].map(a=>[a[1],decode(a[2])]))); } export function main(args) { const [action,...rest]=args,d=connect(); if(action==='screenshot'){ const bytes=d.adb(['exec-out','screencap','-p'],{encoding:null}); if(rest[0])fs.writeFileSync(path.resolve(rest[0]),bytes);else process.stdout.write(bytes); }else if(action==='dump')console.log(dump(d).filter(n=>n.text||n['content-desc']).map(n=>`${n.text||'@'+n['content-desc']} | ${n.bounds}`).join('\n')); else if(action==='tap'){ const n=dump(d).find(n=>n.text===rest[0]||n['content-desc']===rest[0]); if(!n)throw new Error('No visible control: '+rest[0]); const b=[...n.bounds.matchAll(/\d+/g)].map(x=>+x[0]); if(b.length!==4||b[2]<=b[0]||b[3]<=b[1])throw new Error('Control has no visible bounds.'); d.adb(['shell','input','tap',String(Math.round((b[0]+b[2])/2)),String(Math.round((b[1]+b[3])/2))]); }else if(['shell','push','pull'].includes(action))console.log(d.adb([action,...rest])); else throw new Error('Use screenshot [FILE], dump, tap LABEL, shell ARG..., push or pull. Use chloe.mjs install for signed updates.'); } if(process.argv[1]&&fs.realpathSync(process.argv[1])===fileURLToPath(import.meta.url)){try{main(process.argv.slice(2));}catch(e){console.error(e.message);process.exitCode=1;}}