@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 +142 -0
- package/bin/setup.js +307 -0
- package/dist/android-device.d.ts +10 -0
- package/dist/android-device.d.ts.map +1 -0
- package/dist/android-device.js +161 -0
- package/dist/android-device.js.map +1 -0
- package/dist/device.d.ts +18 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +77 -0
- package/dist/device.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +1 -0
- package/dist/location.d.ts +9 -0
- package/dist/location.d.ts.map +1 -0
- package/dist/location.js +68 -0
- package/dist/location.js.map +1 -0
- package/package.json +30 -0
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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"}
|
package/dist/device.d.ts
ADDED
|
@@ -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"}
|