kg-ios-usb-app-info 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.
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ findDebugServers,
5
+ getUiTree,
6
+ debugScan
7
+ } from '../lib/kg-ios-usb-debug.mjs';
8
+
9
+ const VERSION = '0.1.0';
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args[0];
13
+
14
+ function getArgValue(name) {
15
+ const index = args.indexOf(name);
16
+ if (index < 0) return undefined;
17
+
18
+ const value = args[index + 1];
19
+ if (!value || value.startsWith('--')) {
20
+ return undefined;
21
+ }
22
+
23
+ return value;
24
+ }
25
+
26
+ function hasArg(name) {
27
+ return args.includes(name);
28
+ }
29
+
30
+ function parseInteger(value, fallback) {
31
+ if (!value) return fallback;
32
+
33
+ const number = Number(value);
34
+ if (!Number.isInteger(number)) {
35
+ return fallback;
36
+ }
37
+
38
+ return number;
39
+ }
40
+
41
+ function parsePorts(value) {
42
+ if (!value) return undefined;
43
+
44
+ const ports = [];
45
+
46
+ for (const part of value.split(',')) {
47
+ const item = part.trim();
48
+ if (!item) continue;
49
+
50
+ if (item.includes('-')) {
51
+ const [startText, endText] = item.split('-');
52
+ const start = Number(startText);
53
+ const end = Number(endText);
54
+
55
+ if (
56
+ Number.isInteger(start) &&
57
+ Number.isInteger(end) &&
58
+ start > 0 &&
59
+ end >= start
60
+ ) {
61
+ for (let port = start; port <= end; port++) {
62
+ ports.push(port);
63
+ }
64
+ }
65
+
66
+ continue;
67
+ }
68
+
69
+ const port = Number(item);
70
+ if (Number.isInteger(port) && port > 0) {
71
+ ports.push(port);
72
+ }
73
+ }
74
+
75
+ return ports.length ? [...new Set(ports)] : undefined;
76
+ }
77
+
78
+ function printJson(value, pretty = true) {
79
+ console.log(JSON.stringify(value, null, pretty ? 2 : 0));
80
+ }
81
+
82
+ function printHelp() {
83
+ console.log(`
84
+ kg-ios-usb-app-info
85
+
86
+ Usage:
87
+ kg-ios-usb-app-info list
88
+ kg-ios-usb-app-info tree
89
+ kg-ios-usb-app-info tree --bundle-id com.xxx.app
90
+ kg-ios-usb-app-info tree --raw
91
+ kg-ios-usb-app-info scan
92
+ kg-ios-usb-app-info help
93
+
94
+ Commands:
95
+ list List iOS Debug UI Servers over USB
96
+ tree Read current iOS app UI hierarchy over USB
97
+ scan Diagnose USB devices and debug ports
98
+ help Show help
99
+ version Show version
100
+
101
+ Options:
102
+ --bundle-id <id> Specify app bundleId when multiple apps are found
103
+ --ports <ports> Specify ports, e.g. 18080 or 18080,18081 or 18080-18100
104
+ --server-name <name> Specify debug server name, default KGDebugUIServer
105
+ --concurrency <n> Specify scan concurrency, default from lib
106
+ --compact Output compact JSON
107
+ --raw For tree command, output only raw UI tree JSON
108
+
109
+ Environment:
110
+ KG_IOS_USB_PORTS Default ports, e.g. 18080,18081 or 18080-18100
111
+ KG_IOS_USB_SERVER_NAME Default server name
112
+ KG_IOS_USB_DEBUG_TOKEN Debug token sent as X-Debug-Token
113
+ KG_IOS_USB_CONNECT_TIMEOUT_MS USB connect timeout
114
+ KG_IOS_USB_READ_TIMEOUT_MS HTTP read timeout
115
+ KG_IOS_USB_TREE_TIMEOUT_MS UI tree read timeout
116
+
117
+ Examples:
118
+ kg-ios-usb-app-info list
119
+ kg-ios-usb-app-info tree
120
+ kg-ios-usb-app-info tree --bundle-id com.kugou.tingshu
121
+ kg-ios-usb-app-info tree --ports 18080 --raw
122
+ kg-ios-usb-app-info scan --ports 18080-18100
123
+ `);
124
+ }
125
+
126
+ function printVersion() {
127
+ console.log(`kg-ios-usb-app-info ${VERSION}`);
128
+ }
129
+
130
+ function buildOptions() {
131
+ const bundleId = getArgValue('--bundle-id');
132
+ const ports = parsePorts(getArgValue('--ports'));
133
+ const serverName = getArgValue('--server-name');
134
+ const concurrency = parseInteger(getArgValue('--concurrency'), undefined);
135
+
136
+ const options = {};
137
+
138
+ if (bundleId) {
139
+ options.bundleId = bundleId;
140
+ }
141
+
142
+ if (ports) {
143
+ options.ports = ports;
144
+ }
145
+
146
+ if (serverName) {
147
+ options.serverName = serverName;
148
+ }
149
+
150
+ if (concurrency) {
151
+ options.concurrency = concurrency;
152
+ }
153
+
154
+ return options;
155
+ }
156
+
157
+ async function handleList(cliOptions) {
158
+ const servers = await findDebugServers(cliOptions.queryOptions);
159
+
160
+ printJson({
161
+ ok: true,
162
+ count: servers.length,
163
+ servers
164
+ }, cliOptions.pretty);
165
+ }
166
+
167
+ async function handleTree(cliOptions) {
168
+ const result = await getUiTree(cliOptions.queryOptions);
169
+
170
+ if (cliOptions.raw) {
171
+ printJson(result.tree, cliOptions.pretty);
172
+ return;
173
+ }
174
+
175
+ printJson({
176
+ ok: true,
177
+ target: result.target,
178
+ tree: result.tree
179
+ }, cliOptions.pretty);
180
+ }
181
+
182
+ async function handleScan(cliOptions) {
183
+ const result = await debugScan(cliOptions.queryOptions);
184
+
185
+ printJson({
186
+ ok: true,
187
+ ...result
188
+ }, cliOptions.pretty);
189
+ }
190
+
191
+ async function main() {
192
+ const pretty = !hasArg('--compact');
193
+ const raw = hasArg('--raw');
194
+
195
+ const cliOptions = {
196
+ pretty,
197
+ raw,
198
+ queryOptions: buildOptions()
199
+ };
200
+
201
+ if (
202
+ !command ||
203
+ command === 'help' ||
204
+ command === '--help' ||
205
+ command === '-h'
206
+ ) {
207
+ printHelp();
208
+ return;
209
+ }
210
+
211
+ if (
212
+ command === 'version' ||
213
+ command === '--version' ||
214
+ command === '-v'
215
+ ) {
216
+ printVersion();
217
+ return;
218
+ }
219
+
220
+ if (command === 'list') {
221
+ await handleList(cliOptions);
222
+ return;
223
+ }
224
+
225
+ if (command === 'tree') {
226
+ await handleTree(cliOptions);
227
+ return;
228
+ }
229
+
230
+ if (command === 'scan') {
231
+ await handleScan(cliOptions);
232
+ return;
233
+ }
234
+
235
+ throw new Error(`Unknown command: ${command}`);
236
+ }
237
+
238
+ function flushStdout() {
239
+ return new Promise(resolve => {
240
+ process.stdout.write('', resolve);
241
+ });
242
+ }
243
+
244
+ main()
245
+ .then(async () => {
246
+ await flushStdout();
247
+ process.exit(0);
248
+ })
249
+ .catch(async error => {
250
+ printJson({
251
+ ok: false,
252
+ error: error?.message || String(error)
253
+ }, true);
254
+
255
+ await flushStdout();
256
+ process.exit(1);
257
+ });
258
+
259
+
260
+
@@ -0,0 +1,565 @@
1
+ import { UsbmuxClient } from 'usbmux-client';
2
+
3
+ const DEFAULT_SERVER_NAME = process.env.KG_IOS_USB_SERVER_NAME || 'KGDebugUIServer';
4
+
5
+ // 默认扫描 18080~18100
6
+ // 也可以通过环境变量指定:
7
+ // KG_IOS_USB_PORTS=18080
8
+ // KG_IOS_USB_PORTS=18080,18081,18090-18100
9
+ const DEFAULT_PORTS = parsePorts(process.env.KG_IOS_USB_PORTS) ||
10
+ Array.from({ length: 21 }, (_, i) => 18080 + i);
11
+
12
+ const DEFAULT_CONNECT_TIMEOUT_MS = Number(process.env.KG_IOS_USB_CONNECT_TIMEOUT_MS || 1200);
13
+ const DEFAULT_READ_TIMEOUT_MS = Number(process.env.KG_IOS_USB_READ_TIMEOUT_MS || 2000);
14
+ const TREE_READ_TIMEOUT_MS = Number(process.env.KG_IOS_USB_TREE_TIMEOUT_MS || 15000);
15
+ const MAX_RESPONSE_BYTES = Number(process.env.KG_IOS_USB_MAX_RESPONSE_BYTES || 50 * 1024 * 1024);
16
+
17
+ function parsePorts(value) {
18
+ if (!value) return null;
19
+
20
+ const ports = [];
21
+
22
+ for (const part of value.split(',')) {
23
+ const item = part.trim();
24
+ if (!item) continue;
25
+
26
+ if (item.includes('-')) {
27
+ const [startText, endText] = item.split('-');
28
+ const start = Number(startText);
29
+ const end = Number(endText);
30
+
31
+ if (
32
+ Number.isInteger(start) &&
33
+ Number.isInteger(end) &&
34
+ start > 0 &&
35
+ end >= start
36
+ ) {
37
+ for (let port = start; port <= end; port++) {
38
+ ports.push(port);
39
+ }
40
+ }
41
+
42
+ continue;
43
+ }
44
+
45
+ const port = Number(item);
46
+ if (Number.isInteger(port) && port > 0) {
47
+ ports.push(port);
48
+ }
49
+ }
50
+
51
+ return ports.length ? [...new Set(ports)] : null;
52
+ }
53
+
54
+ function normalizeDevices(devices) {
55
+ if (Array.isArray(devices)) {
56
+ return devices.map((raw, index) => {
57
+ const id = raw.DeviceID ?? raw.deviceId ?? raw.id ?? index;
58
+ return {
59
+ id,
60
+ raw
61
+ };
62
+ });
63
+ }
64
+
65
+ return Object.entries(devices || {}).map(([id, raw]) => ({
66
+ id,
67
+ raw
68
+ }));
69
+ }
70
+
71
+ function createTimeoutError(label, timeoutMs) {
72
+ return new Error(`${label} timeout after ${timeoutMs}ms`);
73
+ }
74
+
75
+ function withTimeout(promise, timeoutMs, label) {
76
+ let timer;
77
+
78
+ const timeoutPromise = new Promise((_, reject) => {
79
+ timer = setTimeout(() => {
80
+ reject(createTimeoutError(label, timeoutMs));
81
+ }, timeoutMs);
82
+
83
+ if (typeof timer.unref === 'function') {
84
+ timer.unref();
85
+ }
86
+ });
87
+
88
+ return Promise.race([promise, timeoutPromise]).finally(() => {
89
+ clearTimeout(timer);
90
+ });
91
+ }
92
+
93
+ function safeDestroySocket(socket) {
94
+ if (!socket) return;
95
+
96
+ try {
97
+ socket.destroy();
98
+ } catch {
99
+ // ignore
100
+ }
101
+ }
102
+
103
+ function parseHeaderLines(headerText) {
104
+ const lines = headerText.split('\r\n');
105
+ const statusLine = lines.shift() || '';
106
+
107
+ const headers = {};
108
+ for (const line of lines) {
109
+ const index = line.indexOf(':');
110
+ if (index < 0) continue;
111
+
112
+ const key = line.slice(0, index).trim().toLowerCase();
113
+ const value = line.slice(index + 1).trim();
114
+
115
+ if (!key) continue;
116
+ headers[key] = value;
117
+ }
118
+
119
+ const statusMatch = statusLine.match(/^HTTP\/\d(?:\.\d)?\s+(\d+)/i);
120
+ const statusCode = statusMatch ? Number(statusMatch[1]) : 0;
121
+
122
+ return {
123
+ statusLine,
124
+ statusCode,
125
+ headers
126
+ };
127
+ }
128
+
129
+ function decodeChunkedBody(buffer) {
130
+ let offset = 0;
131
+ const chunks = [];
132
+
133
+ while (offset < buffer.length) {
134
+ const lineEnd = buffer.indexOf(Buffer.from('\r\n'), offset);
135
+ if (lineEnd < 0) break;
136
+
137
+ const sizeLine = buffer.subarray(offset, lineEnd).toString('utf8').trim();
138
+ const semicolonIndex = sizeLine.indexOf(';');
139
+ const sizeText = semicolonIndex >= 0 ? sizeLine.slice(0, semicolonIndex) : sizeLine;
140
+ const size = Number.parseInt(sizeText, 16);
141
+
142
+ if (!Number.isFinite(size)) break;
143
+
144
+ offset = lineEnd + 2;
145
+
146
+ if (size === 0) {
147
+ break;
148
+ }
149
+
150
+ const chunkEnd = offset + size;
151
+ if (chunkEnd > buffer.length) break;
152
+
153
+ chunks.push(buffer.subarray(offset, chunkEnd));
154
+ offset = chunkEnd + 2;
155
+ }
156
+
157
+ return Buffer.concat(chunks);
158
+ }
159
+
160
+ function parseHttpResponse(buffer) {
161
+ const headerEnd = buffer.indexOf(Buffer.from('\r\n\r\n'));
162
+
163
+ if (headerEnd < 0) {
164
+ return {
165
+ statusCode: 0,
166
+ statusLine: '',
167
+ headers: {},
168
+ rawHeaders: '',
169
+ bodyBuffer: buffer,
170
+ body: buffer.toString('utf8')
171
+ };
172
+ }
173
+
174
+ const rawHeaders = buffer.subarray(0, headerEnd).toString('utf8');
175
+ const parsed = parseHeaderLines(rawHeaders);
176
+
177
+ let bodyBuffer = buffer.subarray(headerEnd + 4);
178
+
179
+ const transferEncoding = parsed.headers['transfer-encoding'] || '';
180
+ if (transferEncoding.toLowerCase().includes('chunked')) {
181
+ bodyBuffer = decodeChunkedBody(bodyBuffer);
182
+ }
183
+
184
+ return {
185
+ statusCode: parsed.statusCode,
186
+ statusLine: parsed.statusLine,
187
+ headers: parsed.headers,
188
+ rawHeaders,
189
+ bodyBuffer,
190
+ body: bodyBuffer.toString('utf8')
191
+ };
192
+ }
193
+
194
+ function hasCompleteHttpResponse(buffer) {
195
+ const headerEnd = buffer.indexOf(Buffer.from('\r\n\r\n'));
196
+ if (headerEnd < 0) return false;
197
+
198
+ const headerText = buffer.subarray(0, headerEnd).toString('utf8');
199
+ const { headers } = parseHeaderLines(headerText);
200
+
201
+ const contentLengthText = headers['content-length'];
202
+ if (contentLengthText) {
203
+ const contentLength = Number(contentLengthText);
204
+ if (Number.isFinite(contentLength)) {
205
+ const totalLength = headerEnd + 4 + contentLength;
206
+ return buffer.length >= totalLength;
207
+ }
208
+ }
209
+
210
+ const transferEncoding = headers['transfer-encoding'] || '';
211
+ if (transferEncoding.toLowerCase().includes('chunked')) {
212
+ return buffer.includes(Buffer.from('\r\n0\r\n\r\n'), headerEnd + 4);
213
+ }
214
+
215
+ return false;
216
+ }
217
+
218
+ function readHttpResponse(socket, timeoutMs = DEFAULT_READ_TIMEOUT_MS) {
219
+ return new Promise((resolve, reject) => {
220
+ let buffer = Buffer.alloc(0);
221
+ let finished = false;
222
+ let timer = null;
223
+
224
+ function resetTimer() {
225
+ clearTimeout(timer);
226
+
227
+ timer = setTimeout(() => {
228
+ if (buffer.length > 0) {
229
+ finish(null, buffer);
230
+ } else {
231
+ finish(createTimeoutError('HTTP read', timeoutMs));
232
+ }
233
+ }, timeoutMs);
234
+
235
+ if (typeof timer.unref === 'function') {
236
+ timer.unref();
237
+ }
238
+ }
239
+
240
+ function cleanup() {
241
+ clearTimeout(timer);
242
+ socket.off('data', onData);
243
+ socket.off('end', onEnd);
244
+ socket.off('close', onClose);
245
+ socket.off('error', onError);
246
+ }
247
+
248
+ function finish(error, data) {
249
+ if (finished) return;
250
+ finished = true;
251
+
252
+ cleanup();
253
+
254
+ if (error) {
255
+ reject(error);
256
+ } else {
257
+ resolve(data || buffer);
258
+ }
259
+ }
260
+
261
+ function onData(chunk) {
262
+ buffer = Buffer.concat([buffer, chunk]);
263
+
264
+ if (buffer.length > MAX_RESPONSE_BYTES) {
265
+ finish(new Error(`HTTP response too large: ${buffer.length} bytes`));
266
+ return;
267
+ }
268
+
269
+ if (hasCompleteHttpResponse(buffer)) {
270
+ finish(null, buffer);
271
+ return;
272
+ }
273
+
274
+ resetTimer();
275
+ }
276
+
277
+ function onEnd() {
278
+ finish(null, buffer);
279
+ }
280
+
281
+ function onClose() {
282
+ if (buffer.length > 0) {
283
+ finish(null, buffer);
284
+ } else {
285
+ finish(new Error('socket closed without response'));
286
+ }
287
+ }
288
+
289
+ function onError(error) {
290
+ finish(error);
291
+ }
292
+
293
+ socket.on('data', onData);
294
+ socket.on('end', onEnd);
295
+ socket.on('close', onClose);
296
+ socket.on('error', onError);
297
+
298
+ resetTimer();
299
+ });
300
+ }
301
+
302
+ async function requestDeviceHttp(client, deviceId, port, path, options = {}) {
303
+ const {
304
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
305
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MS,
306
+ method = 'GET'
307
+ } = options;
308
+
309
+ let socket;
310
+
311
+ try {
312
+ socket = await withTimeout(
313
+ client.createDeviceTunnel(deviceId, port),
314
+ connectTimeoutMs,
315
+ `connect device=${deviceId} port=${port}`
316
+ );
317
+
318
+ const headers = [
319
+ `${method} ${path} HTTP/1.1`,
320
+ 'Host: 127.0.0.1',
321
+ 'Accept: application/json',
322
+ 'User-Agent: kg-ios-usb-app-info/0.1.0',
323
+ 'Connection: close'
324
+ ];
325
+
326
+ const token = process.env.KG_IOS_USB_DEBUG_TOKEN;
327
+ if (token) {
328
+ headers.push(`X-Debug-Token: ${token}`);
329
+ }
330
+
331
+ headers.push('', '');
332
+
333
+ socket.write(headers.join('\r\n'));
334
+
335
+ const responseBuffer = await readHttpResponse(socket, readTimeoutMs);
336
+ return parseHttpResponse(responseBuffer);
337
+ } finally {
338
+ safeDestroySocket(socket);
339
+ }
340
+ }
341
+
342
+ async function requestDeviceJson(client, deviceId, port, path, options = {}) {
343
+ const response = await requestDeviceHttp(client, deviceId, port, path, options);
344
+
345
+ if (response.statusCode && (response.statusCode < 200 || response.statusCode >= 300)) {
346
+ throw new Error(
347
+ `HTTP ${response.statusCode} from device=${deviceId} port=${port} path=${path}: ` +
348
+ response.body.slice(0, 300)
349
+ );
350
+ }
351
+
352
+ try {
353
+ return JSON.parse(response.body);
354
+ } catch (error) {
355
+ throw new Error(
356
+ `Invalid JSON from device=${deviceId} port=${port} path=${path}: ` +
357
+ `${error.message}; body=${response.body.slice(0, 300)}`
358
+ );
359
+ }
360
+ }
361
+
362
+ async function getDevices() {
363
+ const client = new UsbmuxClient();
364
+
365
+ const rawDevices = await withTimeout(
366
+ client.getDevices(),
367
+ 3000,
368
+ 'get USB devices'
369
+ );
370
+
371
+ return {
372
+ client,
373
+ devices: normalizeDevices(rawDevices),
374
+ rawDevices
375
+ };
376
+ }
377
+
378
+ async function mapWithConcurrency(items, concurrency, mapper) {
379
+ const results = new Array(items.length);
380
+ let nextIndex = 0;
381
+
382
+ async function worker() {
383
+ while (nextIndex < items.length) {
384
+ const currentIndex = nextIndex;
385
+ nextIndex += 1;
386
+
387
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
388
+ }
389
+ }
390
+
391
+ const workerCount = Math.min(concurrency, items.length);
392
+ await Promise.all(Array.from({ length: workerCount }, worker));
393
+
394
+ return results;
395
+ }
396
+
397
+ function buildCandidates(devices, ports) {
398
+ const candidates = [];
399
+
400
+ for (const device of devices) {
401
+ for (const port of ports) {
402
+ candidates.push({
403
+ device,
404
+ port
405
+ });
406
+ }
407
+ }
408
+
409
+ return candidates;
410
+ }
411
+
412
+ export async function findDebugServers(options = {}) {
413
+ const {
414
+ bundleId,
415
+ ports = DEFAULT_PORTS,
416
+ serverName = DEFAULT_SERVER_NAME,
417
+ concurrency = 6
418
+ } = options;
419
+
420
+ const { client, devices } = await getDevices();
421
+ const candidates = buildCandidates(devices, ports);
422
+
423
+ const results = await mapWithConcurrency(
424
+ candidates,
425
+ concurrency,
426
+ async ({ device, port }) => {
427
+ try {
428
+ const json = await requestDeviceJson(
429
+ client,
430
+ device.id,
431
+ port,
432
+ '/debug/ping',
433
+ {
434
+ connectTimeoutMs: DEFAULT_CONNECT_TIMEOUT_MS,
435
+ readTimeoutMs: DEFAULT_READ_TIMEOUT_MS
436
+ }
437
+ );
438
+
439
+ if (json?.server !== serverName) {
440
+ return null;
441
+ }
442
+
443
+ if (bundleId && json.bundleId !== bundleId) {
444
+ return null;
445
+ }
446
+
447
+ return {
448
+ deviceId: device.id,
449
+ port,
450
+ bundleId: json.bundleId || '',
451
+ appName: json.appName || '',
452
+ server: json.server || '',
453
+ ping: json,
454
+ device: device.raw
455
+ };
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ );
461
+
462
+ return results.filter(Boolean);
463
+ }
464
+
465
+ async function getSingleDebugServer(options = {}) {
466
+ const { bundleId } = options;
467
+ const servers = await findDebugServers(options);
468
+
469
+ if (servers.length === 0) {
470
+ throw new Error(
471
+ bundleId
472
+ ? `没有找到 bundleId=${bundleId} 的 iOS Debug UI Server`
473
+ : '没有找到 iOS Debug UI Server。请确认 USB 已连接、iPhone 已信任电脑、App 在前台、GCDWebServer 已启动,并且 /debug/ping 可访问。'
474
+ );
475
+ }
476
+
477
+ if (servers.length > 1 && !bundleId) {
478
+ throw new Error(
479
+ '找到多个 Debug UI Server,请使用 --bundle-id 指定:\n' +
480
+ servers
481
+ .map(server => {
482
+ return `${server.bundleId} app=${server.appName} device=${server.deviceId} port=${server.port}`;
483
+ })
484
+ .join('\n')
485
+ );
486
+ }
487
+
488
+ return servers[0];
489
+ }
490
+
491
+ export async function getUiTree(options = {}) {
492
+ const target = await getSingleDebugServer(options);
493
+ const client = new UsbmuxClient();
494
+
495
+ const tree = await requestDeviceJson(
496
+ client,
497
+ target.deviceId,
498
+ target.port,
499
+ '/debug/ui/tree',
500
+ {
501
+ connectTimeoutMs: DEFAULT_CONNECT_TIMEOUT_MS,
502
+ readTimeoutMs: TREE_READ_TIMEOUT_MS
503
+ }
504
+ );
505
+
506
+ return {
507
+ target,
508
+ tree
509
+ };
510
+ }
511
+
512
+ export async function debugScan(options = {}) {
513
+ const {
514
+ ports = DEFAULT_PORTS,
515
+ concurrency = 6
516
+ } = options;
517
+
518
+ const { client, devices, rawDevices } = await getDevices();
519
+ const candidates = buildCandidates(devices, ports);
520
+
521
+ const results = await mapWithConcurrency(
522
+ candidates,
523
+ concurrency,
524
+ async ({ device, port }) => {
525
+ try {
526
+ const response = await requestDeviceHttp(
527
+ client,
528
+ device.id,
529
+ port,
530
+ '/debug/ping',
531
+ {
532
+ connectTimeoutMs: DEFAULT_CONNECT_TIMEOUT_MS,
533
+ readTimeoutMs: DEFAULT_READ_TIMEOUT_MS
534
+ }
535
+ );
536
+
537
+ return {
538
+ deviceId: device.id,
539
+ port,
540
+ ok: true,
541
+ statusCode: response.statusCode,
542
+ headers: response.headers,
543
+ body: response.body
544
+ };
545
+ } catch (error) {
546
+ return {
547
+ deviceId: device.id,
548
+ port,
549
+ ok: false,
550
+ error: error?.message || String(error)
551
+ };
552
+ }
553
+ }
554
+ );
555
+
556
+ return {
557
+ rawDevices,
558
+ devices,
559
+ ports,
560
+ results
561
+ };
562
+ }
563
+
564
+
565
+
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "kg-ios-usb-app-info",
3
+ "version": "0.1.0",
4
+ "description": "Read iOS app UI hierarchy and app info through USB for AI tools",
5
+ "type": "module",
6
+ "bin": {
7
+ "kg-ios-usb-app-info": "bin/kg-ios-usb-app-info.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib"
12
+ ],
13
+ "dependencies": {
14
+ "usbmux-client": "^1.0.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "license": "MIT"
20
+ }
21
+
22
+