@8bitbish/screenshot-service 1.0.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/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @8bitbish/screenshot-service
2
+
3
+ Take screenshots and fetch screen recordings from a connected iPhone or Android device, straight to your Mac as a `Buffer`. Works over USB or WiFi. No cloud, no Shortcuts, no tapping the phone.
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @8bitbish/screenshot-service
11
+ ```
12
+
13
+ ---
14
+
15
+ ## One-time machine setup
16
+
17
+ Each computer needs Python + pymobiledevice3 (for iOS) and/or ADB (for Android). Run the relevant setup once per machine:
18
+
19
+ ```bash
20
+ npx @8bitbish/screenshot-service setup-ios # iOS
21
+ npx @8bitbish/screenshot-service setup-android # Android
22
+ ```
23
+
24
+ These walk you through everything: Homebrew installs, device trust/pairing, and (for iOS) an optional passwordless-sudo grant for the tunnel daemon. **Installing the npm package itself is silent and side-effect-free** — no prompts on `npm install`.
25
+
26
+ Test it:
27
+
28
+ ```bash
29
+ npx @8bitbish/screenshot-service test-ios
30
+ npx @8bitbish/screenshot-service test-android
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Usage
36
+
37
+ ```typescript
38
+ import {
39
+ triggerPhoneScreenshot,
40
+ triggerAndroidScreenshot,
41
+ fetchLatestPhoneRecording,
42
+ fetchLatestAndroidRecording,
43
+ startTunnel,
44
+ } from '@8bitbish/screenshot-service'
45
+ import * as fs from 'fs'
46
+
47
+ // iOS — start tunnel once at app launch (no-op if already running)
48
+ await startTunnel()
49
+
50
+ const ios = await triggerPhoneScreenshot({ includeLocation: true })
51
+ fs.writeFileSync('iphone.png', ios.image)
52
+ // ios.device.{model, name, iosVersion}
53
+ // ios.foregroundApp.{bundleId, name, version} // null if undetected
54
+ // ios.location.{city, country, latitude, longitude} // null unless includeLocation
55
+
56
+ // Android — no tunnel needed
57
+ const android = await triggerAndroidScreenshot({ includeLocation: true })
58
+ fs.writeFileSync('android.png', android.image)
59
+ // android.device.{model, name, androidVersion, softwareVersion}
60
+ // same foregroundApp + location shape as iOS
61
+
62
+ // Fetch the latest video recording (user records on device, then call this)
63
+ const rec = await fetchLatestPhoneRecording({ waitForNew: true, waitTimeout: 120_000 })
64
+ fs.writeFileSync('recording.mov', rec.video)
65
+ ```
66
+
67
+ ---
68
+
69
+ ## API
70
+
71
+ ```typescript
72
+ triggerPhoneScreenshot(opts?: {
73
+ timeout?: number // default 30000
74
+ includeLocation?: boolean // default false
75
+ }): Promise<ScreenshotResult>
76
+
77
+ triggerAndroidScreenshot(opts?: {
78
+ timeout?: number
79
+ includeLocation?: boolean
80
+ }): Promise<AndroidScreenshotResult>
81
+
82
+ fetchLatestPhoneRecording(opts?: {
83
+ waitForNew?: boolean // poll until a new video appears (user records then stops)
84
+ waitTimeout?: number // default 300000 (5 min)
85
+ timeout?: number
86
+ includeLocation?: boolean
87
+ }): Promise<PhoneRecordingResult>
88
+
89
+ fetchLatestAndroidRecording(opts?: {
90
+ waitForNew?: boolean
91
+ waitTimeout?: number
92
+ timeout?: number
93
+ includeLocation?: boolean
94
+ }): Promise<AndroidRecordingResult>
95
+
96
+ // iOS tunnel control (Android needs no tunnel — ADB handles it)
97
+ startTunnel(): Promise<void> // requires sudo; no-op if already running
98
+ isTunnelRunning(): boolean
99
+ stopTunnel(): void
100
+ ```
101
+
102
+ All result objects share the shape:
103
+
104
+ ```typescript
105
+ {
106
+ image?: Buffer // present on screenshot results
107
+ video?: Buffer // present on recording results
108
+ device: { ...platform-specific fields }
109
+ foregroundApp: { bundleId, name, version } | null
110
+ capturedAt: Date
111
+ location: { city, country, latitude, longitude, source } | null
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## How it works
118
+
119
+ **iOS** — A background tunnel (`pymobiledevice3 remote tunneld`) connects Mac ↔ iPhone over Apple's RemoteXPC/RSD protocol (iOS 17+). The tunnel needs root; setup-ios offers a passwordless-sudo rule for just this command so you're not prompted every session. Screenshots use the DVT screenshot service; recordings are pulled directly from the Camera Roll via AFC.
120
+
121
+ **Android** — Uses ADB. Screenshots via `adb exec-out screencap -p`. Recordings are pulled from MediaStore via `adb pull`. Wireless connections auto-reconnect to the last known IP, so a Mac reboot doesn't break things (as long as Wireless Debugging is still toggled ON on the phone).
122
+
123
+ ---
124
+
125
+ ## Troubleshooting
126
+
127
+ | Symptom | Fix |
128
+ |---|---|
129
+ | `Python 3.11+ with pymobiledevice3 not found` | `npx @8bitbish/screenshot-service setup-ios` |
130
+ | `adb not found` | `npx @8bitbish/screenshot-service setup-android` |
131
+ | `No Android device found` after Mac reboot | The package auto-reconnects to the last wireless IP. If it still fails, check Settings → Developer Options → Wireless Debugging is ON on the phone. |
132
+ | iOS screenshot fails repeatedly | Make sure the iPhone is unlocked and trusted with this Mac |
133
+ | Tunnel dies after Mac sleeps | Next `triggerPhoneScreenshot()` call restarts it automatically |
134
+
135
+ ---
136
+
137
+ ## Requirements
138
+
139
+ - macOS (uses Homebrew, AFC, ADB)
140
+ - Node.js 18+
141
+ - iOS 17+ for iPhone support
142
+ - Android 10+ for Android support (Wireless Debugging requires Android 11+)
package/bin/setup.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { execSync, execFileSync, spawnSync } = require('child_process');
5
+ const { writeFileSync, unlinkSync, existsSync } = require('fs');
6
+ const { tmpdir, homedir } = require('os');
7
+ const path = require('path');
8
+ const readline = require('readline');
9
+
10
+ // ── ANSI colours ──────────────────────────────────────────────────────────────
11
+ const C = {
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ cyan: '\x1b[36m',
19
+ };
20
+
21
+ function log(s) { console.log(s); }
22
+ function ok(s) { console.log(C.green + '✓ ' + s + C.reset); }
23
+ function warn(s) { console.log(C.yellow + '⚠ ' + s + C.reset); }
24
+ function fail(s) { console.log(C.red + '✗ ' + s + C.reset); }
25
+ function step(n, t) { console.log('\n' + C.bold + C.cyan + 'Step ' + n + ':' + C.reset + ' ' + t + '\n'); }
26
+ function divider() { console.log(C.dim + '──────────────────────────────────────────────────────────────────' + C.reset); }
27
+
28
+ function prompt(question) {
29
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
+ return new Promise(resolve => rl.question(C.bold + question + C.reset + ' ', a => { rl.close(); resolve(a.trim()); }));
31
+ }
32
+
33
+ // ── Shared helpers ────────────────────────────────────────────────────────────
34
+ function which(bin) {
35
+ try { return execSync(`command -v ${bin}`, { encoding: 'utf8' }).trim(); } catch { return ''; }
36
+ }
37
+
38
+ function findPython() {
39
+ const candidates = [
40
+ '/opt/homebrew/bin/python3.14', '/opt/homebrew/bin/python3.13',
41
+ '/opt/homebrew/bin/python3.12', '/opt/homebrew/bin/python3.11',
42
+ '/usr/local/bin/python3.14', '/usr/local/bin/python3.13',
43
+ '/usr/local/bin/python3.12', '/usr/local/bin/python3.11',
44
+ 'python3',
45
+ ];
46
+ const probe = 'import sys, pymobiledevice3; sys.exit(0 if sys.version_info >= (3, 11) else 1)';
47
+ for (const p of candidates) {
48
+ try { execFileSync(p, ['-c', probe], { stdio: 'ignore' }); return p; } catch {}
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function findAdb() {
54
+ for (const p of ['/opt/homebrew/bin/adb', '/usr/local/bin/adb', 'adb']) {
55
+ try { execFileSync(p, ['version'], { stdio: 'ignore' }); return p; } catch {}
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function findBrewPython() {
61
+ // Find any Homebrew-installed python3.x (x ≥ 11), preferring newer
62
+ for (const v of ['3.14', '3.13', '3.12', '3.11']) {
63
+ for (const dir of ['/opt/homebrew/bin', '/usr/local/bin']) {
64
+ const p = path.join(dir, 'python' + v);
65
+ try { execFileSync(p, ['--version'], { stdio: 'ignore' }); return p; } catch {}
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // ── iOS setup ─────────────────────────────────────────────────────────────────
72
+ async function runSetupIos() {
73
+ console.log('');
74
+ console.log(C.bold + '📱 screenshot-service — iOS setup' + C.reset);
75
+ divider();
76
+
77
+ // Homebrew
78
+ step(1, 'Homebrew');
79
+ if (!which('brew')) {
80
+ fail('Homebrew not found. Install from https://brew.sh and run this again.');
81
+ process.exit(1);
82
+ }
83
+ ok('Homebrew found');
84
+
85
+ // Python 3.11+
86
+ step(2, 'Python 3.11+ with pymobiledevice3');
87
+ let python = findPython();
88
+ if (python) {
89
+ ok('Found ' + python);
90
+ } else {
91
+ let py = findBrewPython();
92
+ if (!py) {
93
+ log('Installing python@3.13 via Homebrew...');
94
+ execSync('brew install python@3.13', { stdio: 'inherit' });
95
+ py = findBrewPython();
96
+ }
97
+ if (!py) { fail('Python install failed.'); process.exit(1); }
98
+ log('Installing pymobiledevice3 into ' + py + '...');
99
+ // pip3.x ships alongside python3.x in Homebrew
100
+ const pip = py.replace(/python(3\.\d+)$/, 'pip$1');
101
+ execSync(`"${pip}" install --break-system-packages pymobiledevice3`, { stdio: 'inherit' });
102
+ python = findPython();
103
+ if (!python) { fail('pymobiledevice3 install failed.'); process.exit(1); }
104
+ ok('Installed: ' + python);
105
+ }
106
+
107
+ // Sudoers (opt-in)
108
+ step(3, 'Passwordless tunnel (optional)');
109
+ console.log(' The iPhone screenshot tunnel runs under sudo. You can either:');
110
+ console.log(' • ' + C.bold + 'Grant passwordless sudo' + C.reset + ' for this specific command (recommended for convenience)');
111
+ console.log(' • Skip — you will be prompted for your password once per terminal session');
112
+ console.log('');
113
+ const ans = await prompt('Grant passwordless sudo for the tunnel daemon only? [y/N]');
114
+ if (/^[yY]/.test(ans)) {
115
+ const username = execSync('whoami').toString().trim();
116
+ const rule = `${username} ALL=(ALL) NOPASSWD: ${python} -m pymobiledevice3 remote tunneld`;
117
+ const sudoersFile = '/etc/sudoers.d/screenshot-service';
118
+ const scriptPath = path.join(tmpdir(), 'screenshot-service-setup.sh');
119
+ writeFileSync(scriptPath, [
120
+ '#!/bin/bash',
121
+ `printf '%s\\n' '${rule}' > '${sudoersFile}'`,
122
+ `chmod 440 '${sudoersFile}'`,
123
+ ].join('\n'), { mode: 0o755 });
124
+ try {
125
+ execSync(`osascript -e 'do shell script "${scriptPath}" with administrator privileges'`, { stdio: 'pipe' });
126
+ ok('Passwordless tunnel configured');
127
+ } catch {
128
+ warn('Sudoers setup cancelled — you will be prompted for your password each session.');
129
+ } finally {
130
+ try { unlinkSync(scriptPath); } catch {}
131
+ }
132
+ } else {
133
+ log(C.dim + ' Skipped. You will be prompted for your Mac password the first time per session.' + C.reset);
134
+ }
135
+
136
+ // Device instructions
137
+ step(4, 'On your iPhone (one-time setup)');
138
+ console.log(' 1. Settings → search ' + C.yellow + '"Developer Mode"' + C.reset + ' → toggle ON → iPhone restarts → confirm');
139
+ console.log(' 2. Plug iPhone in via USB → tap ' + C.yellow + 'Trust' + C.reset + ' when prompted');
140
+ console.log(' 3. (Optional, for wireless) Finder → select iPhone → tick ' + C.yellow + '"Show this iPhone when on Wi-Fi"' + C.reset);
141
+ console.log('');
142
+ await prompt('Press Enter when done...');
143
+
144
+ divider();
145
+ console.log('');
146
+ console.log(C.bold + C.green + 'Done!' + C.reset + ' Test it with: ' + C.cyan + 'npx @8bitbish/screenshot-service test-ios' + C.reset);
147
+ console.log('');
148
+ }
149
+
150
+ // ── Android setup ─────────────────────────────────────────────────────────────
151
+ async function runSetupAndroid() {
152
+ console.log('');
153
+ console.log(C.bold + '📱 screenshot-service — Android setup' + C.reset);
154
+ divider();
155
+
156
+ // ADB
157
+ step(1, 'ADB (Android Debug Bridge)');
158
+ let adb = findAdb();
159
+ if (adb) {
160
+ const ver = execFileSync(adb, ['version']).toString().split('\n')[0];
161
+ ok('Found ' + adb + ' — ' + ver);
162
+ } else {
163
+ if (!which('brew')) {
164
+ fail('Homebrew not found. Install from https://brew.sh and run this again.');
165
+ process.exit(1);
166
+ }
167
+ log('Installing android-platform-tools via Homebrew...');
168
+ execSync('brew install android-platform-tools', { stdio: 'inherit' });
169
+ adb = findAdb();
170
+ if (!adb) { fail('adb install failed.'); process.exit(1); }
171
+ ok('Installed: ' + adb);
172
+ }
173
+
174
+ // Developer Options
175
+ step(2, 'Enable Developer Options on your Android phone');
176
+ console.log(' 1. Open ' + C.bold + 'Settings' + C.reset);
177
+ console.log(' 2. Scroll to ' + C.bold + 'About Phone' + C.reset);
178
+ console.log(' 3. Tap ' + C.bold + 'Build Number' + C.reset + ' seven times');
179
+ console.log(' 4. You should see ' + C.cyan + '"You are now a developer!"' + C.reset);
180
+ console.log('');
181
+ await prompt('Press Enter when done (skip if already enabled)...');
182
+
183
+ // Connection
184
+ step(3, 'Connection type');
185
+ console.log(' ' + C.bold + 'A) USB cable' + C.reset + ' — plug in once, works every time');
186
+ console.log(' ' + C.bold + 'B) Wireless' + C.reset + ' — one-time pairing, then no cable needed (Android 11+)');
187
+ console.log('');
188
+ const choice = await prompt('Choose [A/B]:');
189
+
190
+ if (/^[Aa]/.test(choice)) {
191
+ console.log('');
192
+ console.log(' Settings → Developer Options → turn on ' + C.bold + 'USB Debugging' + C.reset);
193
+ console.log(' Plug your phone into the Mac via USB. Tap ' + C.bold + 'Allow' + C.reset + ' on the phone.');
194
+ console.log('');
195
+ await prompt('Press Enter once connected...');
196
+ } else {
197
+ console.log('');
198
+ console.log(' Settings → Developer Options → turn on ' + C.bold + 'Wireless Debugging' + C.reset);
199
+ console.log(' Tap ' + C.bold + 'Pair device with pairing code' + C.reset + ' (note IP, port, and 6-digit code).');
200
+ console.log('');
201
+ const pairIp = await prompt(' IP address shown on phone (e.g. 192.168.1.50):');
202
+ const pairPort = await prompt(' Port shown on pairing screen (e.g. 39847):');
203
+ console.log('');
204
+ console.log(' Running: ' + C.cyan + `adb pair ${pairIp}:${pairPort}` + C.reset);
205
+ console.log(' ' + C.dim + '(enter the 6-digit code when prompted)' + C.reset);
206
+ console.log('');
207
+ spawnSync(adb, ['pair', `${pairIp}:${pairPort}`], { stdio: 'inherit' });
208
+
209
+ console.log('');
210
+ console.log(' Now go back to the main ' + C.bold + 'Wireless Debugging' + C.reset + ' screen (different port).');
211
+ const connIp = await prompt(' IP address (same as before, e.g. 192.168.1.50):');
212
+ console.log('');
213
+ log('Connecting...');
214
+ spawnSync(adb, ['connect', `${connIp}:5555`], { stdio: 'inherit' });
215
+ }
216
+
217
+ // Verify
218
+ console.log('');
219
+ const out = execFileSync(adb, ['devices']).toString();
220
+ if (!/\tdevice/.test(out)) {
221
+ fail('No authorised device found. Check the connection and try again.');
222
+ process.exit(1);
223
+ }
224
+ ok('Device connected');
225
+ out.split('\n').slice(1).filter(l => l.includes('\tdevice')).forEach(l => log(' ' + l));
226
+
227
+ divider();
228
+ console.log('');
229
+ console.log(C.bold + C.green + 'Done!' + C.reset + ' Test it with: ' + C.cyan + 'npx @8bitbish/screenshot-service test-android' + C.reset);
230
+ console.log('');
231
+ }
232
+
233
+ // ── Test commands ─────────────────────────────────────────────────────────────
234
+ async function runTestIos() {
235
+ const { triggerPhoneScreenshot } = require('..');
236
+ const fs = require('fs');
237
+ const out = path.join(homedir(), 'Desktop', 'iphone-screenshot.png');
238
+ console.log('Taking iPhone screenshot...');
239
+ try {
240
+ const r = await triggerPhoneScreenshot({ includeLocation: true });
241
+ fs.writeFileSync(out, r.image);
242
+ ok('Saved → Desktop/iphone-screenshot.png');
243
+ log(' Device : ' + r.device.name + ' (' + r.device.model + ')');
244
+ log(' iOS : ' + r.device.iosVersion);
245
+ if (r.foregroundApp) {
246
+ const v = r.foregroundApp.version ? ' v' + r.foregroundApp.version : '';
247
+ log(' App : ' + r.foregroundApp.name + v + ' (' + r.foregroundApp.bundleId + ')');
248
+ }
249
+ if (r.location) log(' Place : ' + r.location.city + ', ' + r.location.country);
250
+ spawnSync('open', [out]);
251
+ } catch (e) {
252
+ fail(e.message);
253
+ process.exit(1);
254
+ }
255
+ }
256
+
257
+ async function runTestAndroid() {
258
+ const { triggerAndroidScreenshot } = require('..');
259
+ const fs = require('fs');
260
+ const out = path.join(homedir(), 'Desktop', 'android-screenshot.png');
261
+ console.log('Taking Android screenshot...');
262
+ try {
263
+ const r = await triggerAndroidScreenshot({ includeLocation: true });
264
+ fs.writeFileSync(out, r.image);
265
+ ok('Saved → Desktop/android-screenshot.png');
266
+ log(' Device : ' + r.device.name + ' (' + r.device.model + ')');
267
+ log(' Software : ' + r.device.softwareVersion);
268
+ if (r.foregroundApp) {
269
+ const v = r.foregroundApp.version ? ' v' + r.foregroundApp.version : '';
270
+ log(' App : ' + r.foregroundApp.name + v + ' (' + r.foregroundApp.bundleId + ')');
271
+ }
272
+ if (r.location) log(' Place : ' + r.location.city + ', ' + r.location.country);
273
+ spawnSync('open', [out]);
274
+ } catch (e) {
275
+ fail(e.message);
276
+ process.exit(1);
277
+ }
278
+ }
279
+
280
+ // ── Entry ─────────────────────────────────────────────────────────────────────
281
+ function printHelp() {
282
+ console.log('');
283
+ console.log(C.bold + '@8bitbish/screenshot-service' + C.reset);
284
+ console.log('');
285
+ console.log('Usage: ' + C.cyan + 'npx @8bitbish/screenshot-service <command>' + C.reset);
286
+ console.log('');
287
+ console.log('Commands:');
288
+ console.log(' ' + C.bold + 'setup-ios' + C.reset + ' Install iOS deps (Python + pymobiledevice3) and guide device trust');
289
+ console.log(' ' + C.bold + 'setup-android' + C.reset + ' Install ADB and guide device pairing');
290
+ console.log(' ' + C.bold + 'test-ios' + C.reset + ' Take a test iPhone screenshot (saved to Desktop)');
291
+ console.log(' ' + C.bold + 'test-android' + C.reset + ' Take a test Android screenshot (saved to Desktop)');
292
+ console.log('');
293
+ }
294
+
295
+ (async () => {
296
+ const cmd = process.argv[2];
297
+ switch (cmd) {
298
+ case 'setup-ios': return runSetupIos();
299
+ case 'setup-android': return runSetupAndroid();
300
+ case 'test-ios': return runTestIos();
301
+ case 'test-android': return runTestAndroid();
302
+ default: printHelp();
303
+ }
304
+ })().catch(err => {
305
+ fail(err.message || String(err));
306
+ process.exit(1);
307
+ });
@@ -0,0 +1,10 @@
1
+ import type { ForegroundApp } from "./device";
2
+ export interface AndroidDeviceInfo {
3
+ model: string;
4
+ name: string;
5
+ androidVersion: string;
6
+ softwareVersion: string;
7
+ }
8
+ export declare function getAndroidDeviceInfo(adb: string): Promise<AndroidDeviceInfo>;
9
+ export declare function getAndroidForegroundApp(adb: string): Promise<ForegroundApp | null>;
10
+ //# sourceMappingURL=android-device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-device.d.ts","sourceRoot":"","sources":["../android-device.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;CACxB;AA4DD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwBlF;AAuDD,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA0BxF"}
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAndroidDeviceInfo = getAndroidDeviceInfo;
4
+ exports.getAndroidForegroundApp = getAndroidForegroundApp;
5
+ const child_process_1 = require("child_process");
6
+ const util_1 = require("util");
7
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
8
+ // Samsung model number prefix → marketing name
9
+ // Strip regional suffix (e.g. SM-S918B → SM-S918) then look up
10
+ const SAMSUNG_MODELS = {
11
+ // S25
12
+ "SM-S938": "Galaxy S25 Ultra", "SM-S936": "Galaxy S25+", "SM-S931": "Galaxy S25",
13
+ // S24
14
+ "SM-S928": "Galaxy S24 Ultra", "SM-S926": "Galaxy S24+", "SM-S921": "Galaxy S24",
15
+ "SM-S721": "Galaxy S24 FE",
16
+ // S23
17
+ "SM-S918": "Galaxy S23 Ultra", "SM-S916": "Galaxy S23+", "SM-S911": "Galaxy S23",
18
+ "SM-S711": "Galaxy S23 FE",
19
+ // S22
20
+ "SM-S908": "Galaxy S22 Ultra", "SM-S906": "Galaxy S22+", "SM-S901": "Galaxy S22",
21
+ // S21
22
+ "SM-G998": "Galaxy S21 Ultra", "SM-G996": "Galaxy S21+", "SM-G991": "Galaxy S21",
23
+ "SM-G990": "Galaxy S21 FE",
24
+ // S20
25
+ "SM-G988": "Galaxy S20 Ultra", "SM-G986": "Galaxy S20+", "SM-G981": "Galaxy S20",
26
+ "SM-G780": "Galaxy S20 FE",
27
+ // S10
28
+ "SM-G977": "Galaxy S10 5G", "SM-G975": "Galaxy S10+",
29
+ "SM-G973": "Galaxy S10", "SM-G970": "Galaxy S10e",
30
+ // Note
31
+ "SM-N986": "Galaxy Note 20 Ultra", "SM-N981": "Galaxy Note 20",
32
+ "SM-N976": "Galaxy Note 10+", "SM-N970": "Galaxy Note 10",
33
+ // Z Fold
34
+ "SM-F956": "Galaxy Z Fold 6", "SM-F946": "Galaxy Z Fold 5",
35
+ "SM-F936": "Galaxy Z Fold 4", "SM-F926": "Galaxy Z Fold 3",
36
+ // Z Flip
37
+ "SM-F741": "Galaxy Z Flip 6", "SM-F731": "Galaxy Z Flip 5",
38
+ "SM-F721": "Galaxy Z Flip 4", "SM-F711": "Galaxy Z Flip 3",
39
+ // A series
40
+ "SM-A556": "Galaxy A55 5G", "SM-A546": "Galaxy A54 5G",
41
+ "SM-A536": "Galaxy A53 5G", "SM-A526": "Galaxy A52 5G",
42
+ "SM-A346": "Galaxy A34 5G", "SM-A336": "Galaxy A33 5G",
43
+ "SM-A256": "Galaxy A25 5G", "SM-A156": "Galaxy A15 5G",
44
+ };
45
+ function samsungModelName(modelNumber) {
46
+ const base = modelNumber.match(/^(SM-[A-Z]\d{3})/)?.[1];
47
+ return base ? (SAMSUNG_MODELS[base] ?? null) : null;
48
+ }
49
+ function prop(adb, key) {
50
+ return execFileAsync(adb, ["shell", "getprop", key], { timeout: 5000 })
51
+ .then(r => r.stdout.trim())
52
+ .catch(() => "");
53
+ }
54
+ function formatOneUi(raw) {
55
+ const n = parseInt(raw, 10);
56
+ if (!n)
57
+ return null;
58
+ const major = Math.floor(n / 10000);
59
+ const minor = Math.floor((n % 10000) / 100);
60
+ const patch = n % 100;
61
+ return patch > 0 ? `One UI ${major}.${minor}.${patch}` : `One UI ${major}.${minor}`;
62
+ }
63
+ async function getAndroidDeviceInfo(adb) {
64
+ const [n1, n2, n3, n4, modelNumber, androidVersion, oneui, miui, emui] = await Promise.all([
65
+ prop(adb, "ro.product.marketname"), // Samsung (most firmware)
66
+ prop(adb, "ro.vendor.oem.model"), // Samsung alternative
67
+ prop(adb, "ro.product.vendor.marketname"), // Samsung alternative
68
+ prop(adb, "ro.config.marketing_name"), // Samsung alternative
69
+ prop(adb, "ro.product.model"), // raw model number fallback
70
+ prop(adb, "ro.build.version.release"),
71
+ prop(adb, "ro.build.version.oneui"), // Samsung: "80000" → One UI 8.0
72
+ prop(adb, "ro.miui.ui.version.name"), // Xiaomi: "MIUI 14"
73
+ prop(adb, "ro.build.version.emui"), // Huawei: "EmotionUI 12"
74
+ ]);
75
+ const name = n1 || n2 || n3 || n4 || samsungModelName(modelNumber) || modelNumber;
76
+ const model = modelNumber;
77
+ let softwareVersion;
78
+ if (oneui)
79
+ softwareVersion = formatOneUi(oneui) ?? `Android ${androidVersion}`;
80
+ else if (miui)
81
+ softwareVersion = miui;
82
+ else if (emui)
83
+ softwareVersion = emui;
84
+ else
85
+ softwareVersion = `Android ${androidVersion}`;
86
+ return { model, name, androidVersion, softwareVersion };
87
+ }
88
+ // Android 12+ doesn't expose app labels via ADB — use lookup for common apps,
89
+ // then fall back to formatting the package name into something readable
90
+ const KNOWN_APPS = {
91
+ "com.google.android.gm": "Gmail",
92
+ "com.google.android.apps.maps": "Google Maps",
93
+ "com.google.android.youtube": "YouTube",
94
+ "com.google.android.googlequicksearchbox": "Google",
95
+ "com.google.android.apps.photos": "Google Photos",
96
+ "com.google.android.keep": "Google Keep",
97
+ "com.google.android.apps.translate": "Google Translate",
98
+ "com.google.android.apps.messaging": "Messages",
99
+ "com.google.android.calendar": "Google Calendar",
100
+ "com.google.android.apps.chromecast.app": "Google Home",
101
+ "com.android.chrome": "Chrome",
102
+ "com.instagram.android": "Instagram",
103
+ "com.instagram.barcelona": "Threads",
104
+ "com.facebook.katana": "Facebook",
105
+ "com.facebook.orca": "Messenger",
106
+ "com.whatsapp": "WhatsApp",
107
+ "com.twitter.android": "X (Twitter)",
108
+ "com.snapchat.android": "Snapchat",
109
+ "com.tiktok.android": "TikTok",
110
+ "com.linkedin.android": "LinkedIn",
111
+ "com.Slack": "Slack",
112
+ "com.microsoft.teams": "Teams",
113
+ "com.microsoft.office.outlook": "Outlook",
114
+ "com.microsoft.office.word": "Word",
115
+ "com.microsoft.office.excel": "Excel",
116
+ "com.spotify.music": "Spotify",
117
+ "com.netflix.mediaclient": "Netflix",
118
+ "com.openai.chatgpt": "ChatGPT",
119
+ "com.amazon.mShop.android.shopping": "Amazon",
120
+ "com.ebay.mobile": "eBay",
121
+ "com.paypal.android.p2pmobile": "PayPal",
122
+ "com.ubercab": "Uber",
123
+ "com.ubercab.eats": "Uber Eats",
124
+ "com.deliveroo.orderapp": "Deliveroo",
125
+ "com.sec.android.app.camera": "Camera",
126
+ "com.sec.android.app.launcher": "Samsung Home",
127
+ "com.samsung.android.messaging": "Messages (Samsung)",
128
+ "com.samsung.android.calendar": "Calendar (Samsung)",
129
+ };
130
+ function resolveAppName(packageId) {
131
+ if (KNOWN_APPS[packageId])
132
+ return KNOWN_APPS[packageId];
133
+ // Heuristic: strip vendor prefix, skip generic segments, capitalise
134
+ const parts = packageId.split(".");
135
+ const skip = new Set(["android", "app", "mobile", "client", "com", "org", "net", "io"]);
136
+ const meaningful = parts.filter(p => !skip.has(p) && p.length > 2);
137
+ const segment = meaningful[meaningful.length - 1] ?? parts[parts.length - 1];
138
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
139
+ }
140
+ async function getAndroidForegroundApp(adb) {
141
+ try {
142
+ // "dumpsys window windows" omits focus data on Android 12+ — use base "dumpsys window"
143
+ const { stdout } = await execFileAsync(adb, ["shell", "dumpsys", "window"], { timeout: 8000, maxBuffer: 20 * 1024 * 1024 });
144
+ // mFocusedWindow gives the cleanest format; mCurrentFocus is the fallback
145
+ const packageId = stdout.match(/mFocusedWindow=([^\s/]+)\//)?.[1] ??
146
+ stdout.match(/mCurrentFocus=Window\{[^}]+\s+([^\s/}]+)\//)?.[1];
147
+ if (!packageId || packageId === "null")
148
+ return null;
149
+ const { stdout: pkgOut } = await execFileAsync(adb, ["shell", "dumpsys", "package", packageId], { timeout: 8000, maxBuffer: 50 * 1024 * 1024 });
150
+ const versionName = pkgOut.match(/versionName=(\S+)/)?.[1] ?? null;
151
+ const versionCode = pkgOut.match(/versionCode=(\d+)/)?.[1] ?? null;
152
+ const version = versionName && versionCode ? `${versionName} (${versionCode})`
153
+ : versionName ?? versionCode ?? null;
154
+ const name = resolveAppName(packageId);
155
+ return { bundleId: packageId, name, version };
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ //# sourceMappingURL=android-device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-device.js","sourceRoot":"","sources":["../android-device.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface DeviceInfo {
2
+ model: string;
3
+ name: string;
4
+ iosVersion: string;
5
+ }
6
+ export interface ForegroundApp {
7
+ bundleId: string;
8
+ name: string;
9
+ version: string | null;
10
+ }
11
+ export declare function getDeviceInfo(python: string): Promise<DeviceInfo>;
12
+ /**
13
+ * Returns the foreground app at time of call.
14
+ * Uses foregroundRunning flag from proclist; cross-references applist for version.
15
+ * Returns null if detection fails — this is best-effort.
16
+ */
17
+ export declare function getForegroundApp(python: string): Promise<ForegroundApp | null>;
18
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../device.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA+BD,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBvE;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAyBpF"}