mobile-debug-mcp 0.24.0 → 0.24.2
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/dist/interact/index.js +120 -15
- package/dist/manage/android.js +51 -8
- package/dist/manage/ios.js +27 -3
- package/dist/server/common.js +4 -2
- package/dist/server/tool-handlers.js +19 -2
- package/dist/system/index.js +53 -2
- package/docs/CHANGELOG.md +8 -0
- package/docs/tools/interact.md +73 -7
- package/docs/tools/manage.md +16 -1
- package/docs/tools/system.md +23 -1
- package/package.json +1 -1
- package/src/interact/index.ts +121 -15
- package/src/manage/android.ts +51 -7
- package/src/manage/ios.ts +27 -3
- package/src/server/common.ts +8 -2
- package/src/server/tool-handlers.ts +19 -2
- package/src/system/index.ts +57 -2
- package/src/types.ts +36 -0
- package/test/unit/interact/expect_tools.test.ts +29 -2
- package/test/unit/interact/wait_for_screen_change.test.ts +7 -3
- package/test/unit/interact/wait_for_ui_selector_matching.test.ts +19 -0
- package/test/unit/manage/install.test.ts +94 -1
- package/test/unit/server/response_shapes.test.ts +20 -3
- package/test/unit/system/get_system_status.test.ts +2 -0
- package/test/unit/system/system_status.test.ts +6 -0
package/dist/interact/index.js
CHANGED
|
@@ -437,6 +437,12 @@ export class ToolsInteract {
|
|
|
437
437
|
}
|
|
438
438
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }) {
|
|
439
439
|
const overallStart = Date.now();
|
|
440
|
+
const requestedIndex = typeof match?.index === 'number' ? match.index : null;
|
|
441
|
+
const requested = {
|
|
442
|
+
selector: selector ?? {},
|
|
443
|
+
condition,
|
|
444
|
+
match: requestedIndex === null ? null : { index: requestedIndex }
|
|
445
|
+
};
|
|
440
446
|
// Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
|
|
441
447
|
const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
|
|
442
448
|
const hasResId = typeof selector?.resource_id === 'string' && selector.resource_id.trim().length > 0;
|
|
@@ -448,22 +454,27 @@ export class ToolsInteract {
|
|
|
448
454
|
code: 'INVALID_SELECTOR',
|
|
449
455
|
message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
|
|
450
456
|
},
|
|
451
|
-
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
|
|
457
|
+
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 },
|
|
458
|
+
requested,
|
|
459
|
+
observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null }
|
|
452
460
|
};
|
|
453
461
|
}
|
|
454
462
|
// Validate condition
|
|
455
463
|
if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) {
|
|
456
|
-
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
|
|
464
|
+
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
|
|
457
465
|
}
|
|
458
466
|
// Platform check
|
|
459
467
|
if (platform && !['android', 'ios'].includes(platform)) {
|
|
460
|
-
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } };
|
|
468
|
+
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } };
|
|
461
469
|
}
|
|
462
470
|
const effectivePoll = Math.max(50, Math.min(poll_interval_ms || 300, 2000));
|
|
463
471
|
const maxAttempts = (retry && retry.max_attempts) ? Math.max(1, retry.max_attempts) : 1;
|
|
464
472
|
const backoff = (retry && retry.backoff_ms) ? Math.max(0, retry.backoff_ms) : 0;
|
|
465
473
|
let attempts = 0;
|
|
466
474
|
let totalPollCount = 0;
|
|
475
|
+
let lastMatchedCount = 0;
|
|
476
|
+
let lastMatchedElement = null;
|
|
477
|
+
let lastConditionSatisfied = false;
|
|
467
478
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
468
479
|
const normalize = ToolsInteract._normalize;
|
|
469
480
|
const containsFlag = !!selector?.contains;
|
|
@@ -573,18 +584,27 @@ export class ToolsInteract {
|
|
|
573
584
|
else
|
|
574
585
|
conditionMet = false;
|
|
575
586
|
}
|
|
587
|
+
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
588
|
+
const resolvedDeviceId = tree?.device?.id || deviceId;
|
|
589
|
+
lastMatchedCount = matchedCount;
|
|
590
|
+
lastConditionSatisfied = conditionMet;
|
|
591
|
+
lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
576
592
|
if (conditionMet) {
|
|
577
593
|
const now = Date.now();
|
|
578
594
|
const latency_ms = now - overallStart;
|
|
579
|
-
|
|
580
|
-
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android');
|
|
581
|
-
const resolvedDeviceId = tree?.device?.id || deviceId;
|
|
582
|
-
const outEl = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null;
|
|
595
|
+
const outEl = lastMatchedElement;
|
|
583
596
|
return {
|
|
584
597
|
status: 'success',
|
|
585
598
|
matched: matchedCount,
|
|
586
599
|
element: outEl,
|
|
587
|
-
metrics: { latency_ms, poll_count: totalPollCount, attempts }
|
|
600
|
+
metrics: { latency_ms, poll_count: totalPollCount, attempts },
|
|
601
|
+
requested,
|
|
602
|
+
observed: {
|
|
603
|
+
matched_count: matchedCount,
|
|
604
|
+
condition_satisfied: true,
|
|
605
|
+
selected_index: outEl?.index ?? null,
|
|
606
|
+
last_matched_element: outEl
|
|
607
|
+
}
|
|
588
608
|
};
|
|
589
609
|
}
|
|
590
610
|
}
|
|
@@ -603,16 +623,38 @@ export class ToolsInteract {
|
|
|
603
623
|
}
|
|
604
624
|
// Final failure for this call
|
|
605
625
|
const elapsed = Date.now() - overallStart;
|
|
626
|
+
const observed = {
|
|
627
|
+
matched_count: lastMatchedCount,
|
|
628
|
+
condition_satisfied: lastConditionSatisfied,
|
|
629
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
630
|
+
last_matched_element: lastMatchedElement
|
|
631
|
+
};
|
|
632
|
+
const matchNote = requestedIndex !== null && lastMatchedCount <= requestedIndex
|
|
633
|
+
? ` requested match.index=${requestedIndex} but observed ${lastMatchedCount} match(es)`
|
|
634
|
+
: ` observed ${lastMatchedCount} match(es)`;
|
|
606
635
|
return {
|
|
607
636
|
status: 'timeout',
|
|
608
|
-
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout` },
|
|
609
|
-
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts }
|
|
637
|
+
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
638
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
639
|
+
requested,
|
|
640
|
+
observed
|
|
610
641
|
};
|
|
611
642
|
}
|
|
612
643
|
}
|
|
613
644
|
catch (err) {
|
|
614
645
|
const elapsed = Date.now() - overallStart;
|
|
615
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
status: 'timeout',
|
|
648
|
+
error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
|
|
649
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
650
|
+
requested,
|
|
651
|
+
observed: {
|
|
652
|
+
matched_count: lastMatchedCount,
|
|
653
|
+
condition_satisfied: false,
|
|
654
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
655
|
+
last_matched_element: lastMatchedElement
|
|
656
|
+
}
|
|
657
|
+
};
|
|
616
658
|
}
|
|
617
659
|
}
|
|
618
660
|
// Helper: normalize various log objects into plain message strings for comparison
|
|
@@ -635,10 +677,12 @@ export class ToolsInteract {
|
|
|
635
677
|
static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
|
|
636
678
|
const start = Date.now();
|
|
637
679
|
let lastFingerprint = null;
|
|
680
|
+
let lastActivity = null;
|
|
638
681
|
while (Date.now() - start < timeoutMs) {
|
|
639
682
|
try {
|
|
640
683
|
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
641
684
|
const fp = res?.fingerprint ?? null;
|
|
685
|
+
lastActivity = res?.activity ?? lastActivity;
|
|
642
686
|
if (fp === null || fp === undefined) {
|
|
643
687
|
lastFingerprint = null;
|
|
644
688
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
@@ -651,8 +695,18 @@ export class ToolsInteract {
|
|
|
651
695
|
try {
|
|
652
696
|
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
653
697
|
const confirmFp = confirmRes?.fingerprint ?? null;
|
|
698
|
+
lastActivity = confirmRes?.activity ?? lastActivity;
|
|
654
699
|
if (confirmFp === fp) {
|
|
655
|
-
return {
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
previousFingerprint,
|
|
703
|
+
newFingerprint: fp,
|
|
704
|
+
elapsedMs: Date.now() - start,
|
|
705
|
+
observed_screen: {
|
|
706
|
+
fingerprint: fp,
|
|
707
|
+
activity: lastActivity
|
|
708
|
+
}
|
|
709
|
+
};
|
|
656
710
|
}
|
|
657
711
|
lastFingerprint = confirmFp;
|
|
658
712
|
continue;
|
|
@@ -668,7 +722,17 @@ export class ToolsInteract {
|
|
|
668
722
|
}
|
|
669
723
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
670
724
|
}
|
|
671
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
reason: 'timeout',
|
|
728
|
+
previousFingerprint,
|
|
729
|
+
lastFingerprint,
|
|
730
|
+
elapsedMs: Date.now() - start,
|
|
731
|
+
observed_screen: {
|
|
732
|
+
fingerprint: lastFingerprint,
|
|
733
|
+
activity: lastActivity
|
|
734
|
+
}
|
|
735
|
+
};
|
|
672
736
|
}
|
|
673
737
|
static async expectScreenHandler({ platform, fingerprint, screen, deviceId }) {
|
|
674
738
|
const observedFingerprint = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
@@ -691,16 +755,26 @@ export class ToolsInteract {
|
|
|
691
755
|
screen: screen ?? null
|
|
692
756
|
};
|
|
693
757
|
let success = false;
|
|
758
|
+
let basis = 'none';
|
|
759
|
+
let reason = 'No fingerprint or screen expectation provided';
|
|
694
760
|
if (fingerprint) {
|
|
761
|
+
basis = 'fingerprint';
|
|
695
762
|
success = observedScreen.fingerprint === fingerprint;
|
|
763
|
+
reason = success
|
|
764
|
+
? `observed fingerprint matches expected fingerprint ${fingerprint}`
|
|
765
|
+
: `expected fingerprint ${fingerprint} but observed ${observedScreen.fingerprint ?? 'null'}`;
|
|
696
766
|
}
|
|
697
767
|
else if (screen) {
|
|
768
|
+
basis = 'screen';
|
|
698
769
|
const candidates = new Set();
|
|
699
770
|
if (observedScreen.screen)
|
|
700
771
|
candidates.add(observedScreen.screen);
|
|
701
772
|
if (observedScreenLabel)
|
|
702
773
|
candidates.add(observedScreenLabel);
|
|
703
774
|
success = candidates.has(screen);
|
|
775
|
+
reason = success
|
|
776
|
+
? `observed screen matches expected screen ${screen}`
|
|
777
|
+
: `expected screen ${screen} but observed ${observedScreenLabel ?? observedScreen.screen ?? 'null'}`;
|
|
704
778
|
}
|
|
705
779
|
return {
|
|
706
780
|
success,
|
|
@@ -709,7 +783,12 @@ export class ToolsInteract {
|
|
|
709
783
|
screen: observedScreenLabel
|
|
710
784
|
},
|
|
711
785
|
expected_screen: expectedScreen,
|
|
712
|
-
confidence: success ? 1 : 0
|
|
786
|
+
confidence: success ? 1 : 0,
|
|
787
|
+
comparison: {
|
|
788
|
+
basis,
|
|
789
|
+
matched: success,
|
|
790
|
+
reason
|
|
791
|
+
}
|
|
713
792
|
};
|
|
714
793
|
}
|
|
715
794
|
static async expectElementVisibleHandler({ selector, element_id, timeout_ms = 5000, poll_interval_ms = 300, platform, deviceId }) {
|
|
@@ -726,6 +805,7 @@ export class ToolsInteract {
|
|
|
726
805
|
success: true,
|
|
727
806
|
selector,
|
|
728
807
|
element_id: result.element.elementId ?? element_id ?? null,
|
|
808
|
+
expected_condition: 'visible',
|
|
729
809
|
element: {
|
|
730
810
|
elementId: result.element.elementId ?? null,
|
|
731
811
|
text: result.element.text ?? null,
|
|
@@ -734,7 +814,23 @@ export class ToolsInteract {
|
|
|
734
814
|
class: result.element.class ?? null,
|
|
735
815
|
bounds: result.element.bounds ?? null,
|
|
736
816
|
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
737
|
-
}
|
|
817
|
+
},
|
|
818
|
+
observed: {
|
|
819
|
+
status: result.status,
|
|
820
|
+
matched_count: typeof result.matched === 'number' ? result.matched : result?.observed?.matched_count ?? null,
|
|
821
|
+
condition_satisfied: true,
|
|
822
|
+
selected_index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
823
|
+
last_matched_element: {
|
|
824
|
+
elementId: result.element.elementId ?? null,
|
|
825
|
+
text: result.element.text ?? null,
|
|
826
|
+
resource_id: result.element.resource_id ?? null,
|
|
827
|
+
accessibility_id: result.element.accessibility_id ?? null,
|
|
828
|
+
class: result.element.class ?? null,
|
|
829
|
+
bounds: result.element.bounds ?? null,
|
|
830
|
+
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
reason: 'selector is visible'
|
|
738
834
|
};
|
|
739
835
|
}
|
|
740
836
|
const errorCode = result?.error?.code === 'INTERNAL_ERROR' ? 'UNKNOWN' : 'TIMEOUT';
|
|
@@ -742,6 +838,15 @@ export class ToolsInteract {
|
|
|
742
838
|
success: false,
|
|
743
839
|
selector,
|
|
744
840
|
element_id: element_id ?? null,
|
|
841
|
+
expected_condition: 'visible',
|
|
842
|
+
observed: {
|
|
843
|
+
status: result?.status,
|
|
844
|
+
matched_count: result?.observed?.matched_count,
|
|
845
|
+
condition_satisfied: result?.observed?.condition_satisfied ?? false,
|
|
846
|
+
selected_index: result?.observed?.selected_index ?? null,
|
|
847
|
+
last_matched_element: result?.observed?.last_matched_element ?? null
|
|
848
|
+
},
|
|
849
|
+
reason: result?.error?.message ?? 'selector is not visible',
|
|
745
850
|
failure_code: errorCode,
|
|
746
851
|
retryable: errorCode === 'TIMEOUT'
|
|
747
852
|
};
|
package/dist/manage/android.js
CHANGED
|
@@ -5,7 +5,11 @@ import { existsSync } from 'fs';
|
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
|
+
import { AndroidObserve } from '../observe/android.js';
|
|
8
9
|
export class AndroidManage {
|
|
10
|
+
isTestOnlyInstallFailure(output) {
|
|
11
|
+
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
|
|
12
|
+
}
|
|
9
13
|
async build(projectPath, _variant) {
|
|
10
14
|
void _variant;
|
|
11
15
|
try {
|
|
@@ -93,6 +97,13 @@ export class AndroidManage {
|
|
|
93
97
|
if (res.code === 0) {
|
|
94
98
|
return { device: deviceInfo, installed: true, output: res.stdout };
|
|
95
99
|
}
|
|
100
|
+
const installOutput = `${res.stdout}\n${res.stderr}`.trim();
|
|
101
|
+
if (this.isTestOnlyInstallFailure(installOutput)) {
|
|
102
|
+
const retryRes = await spawnAdb(['install', '-r', '-t', apkToInstall], deviceId);
|
|
103
|
+
if (retryRes.code === 0) {
|
|
104
|
+
return { device: deviceInfo, installed: true, output: retryRes.stdout };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
96
107
|
}
|
|
97
108
|
catch (e) {
|
|
98
109
|
console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
|
|
@@ -100,12 +111,25 @@ export class AndroidManage {
|
|
|
100
111
|
const basename = path.basename(apkToInstall);
|
|
101
112
|
const remotePath = `/data/local/tmp/${basename}`;
|
|
102
113
|
await execAdb(['push', apkToInstall, remotePath], deviceId);
|
|
103
|
-
|
|
114
|
+
let finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
|
|
104
115
|
try {
|
|
105
|
-
|
|
116
|
+
if (finalPmRes.code === 0) {
|
|
117
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
|
|
118
|
+
}
|
|
119
|
+
if (this.isTestOnlyInstallFailure(`${finalPmRes.stdout}\n${finalPmRes.stderr}`)) {
|
|
120
|
+
finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', '-t', remotePath], deviceId);
|
|
121
|
+
if (finalPmRes.code === 0) {
|
|
122
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
try {
|
|
129
|
+
await execAdb(['shell', 'rm', remotePath], deviceId);
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
106
132
|
}
|
|
107
|
-
catch { }
|
|
108
|
-
return { device: deviceInfo, installed: true, output: pmOut };
|
|
109
133
|
}
|
|
110
134
|
catch (e) {
|
|
111
135
|
// gather diagnostics for attempted adb operations
|
|
@@ -121,8 +145,21 @@ export class AndroidManage {
|
|
|
121
145
|
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
122
146
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
123
147
|
try {
|
|
124
|
-
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
125
|
-
|
|
148
|
+
const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
149
|
+
const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null);
|
|
150
|
+
return {
|
|
151
|
+
device: deviceInfo,
|
|
152
|
+
appStarted: true,
|
|
153
|
+
launchTimeMs: 1000,
|
|
154
|
+
output,
|
|
155
|
+
observedApp: {
|
|
156
|
+
appId,
|
|
157
|
+
package: current?.package ?? null,
|
|
158
|
+
activity: current?.activity ?? null,
|
|
159
|
+
screen: current?.shortActivity ?? current?.activity ?? null,
|
|
160
|
+
matchedTarget: current ? current.package === appId : null
|
|
161
|
+
}
|
|
162
|
+
};
|
|
126
163
|
}
|
|
127
164
|
catch (e) {
|
|
128
165
|
const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
@@ -142,12 +179,18 @@ export class AndroidManage {
|
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
181
|
async restartApp(appId, deviceId) {
|
|
145
|
-
await this.terminateApp(appId, deviceId);
|
|
182
|
+
const terminateResult = await this.terminateApp(appId, deviceId);
|
|
146
183
|
const startResult = await this.startApp(appId, deviceId);
|
|
147
184
|
return {
|
|
148
185
|
device: startResult.device,
|
|
149
186
|
appRestarted: startResult.appStarted,
|
|
150
|
-
launchTimeMs: startResult.launchTimeMs
|
|
187
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
188
|
+
output: startResult.output,
|
|
189
|
+
observedApp: startResult.observedApp,
|
|
190
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
191
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
192
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
193
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
151
194
|
};
|
|
152
195
|
}
|
|
153
196
|
async resetAppData(appId, deviceId) {
|
package/dist/manage/ios.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import { spawn, spawnSync } from "child_process";
|
|
3
3
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js";
|
|
4
|
+
import { iOSObserve } from "../observe/ios.js";
|
|
4
5
|
import path from "path";
|
|
5
6
|
export class iOSManage {
|
|
6
7
|
async build(projectPath, optsOrVariant) {
|
|
@@ -294,7 +295,20 @@ export class iOSManage {
|
|
|
294
295
|
try {
|
|
295
296
|
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
296
297
|
const device = await getIOSDeviceMetadata(deviceId);
|
|
297
|
-
|
|
298
|
+
const fingerprint = await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null);
|
|
299
|
+
const pidMatch = result.output.match(/:\s*(\d+)\s*$/);
|
|
300
|
+
return {
|
|
301
|
+
device,
|
|
302
|
+
appStarted: !!result.output,
|
|
303
|
+
launchTimeMs: 1000,
|
|
304
|
+
output: result.output,
|
|
305
|
+
observedApp: {
|
|
306
|
+
appId: bundleId,
|
|
307
|
+
pid: pidMatch ? Number(pidMatch[1]) : null,
|
|
308
|
+
screen: fingerprint?.activity ?? null,
|
|
309
|
+
matchedTarget: null
|
|
310
|
+
}
|
|
311
|
+
};
|
|
298
312
|
}
|
|
299
313
|
catch (e) {
|
|
300
314
|
const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
|
|
@@ -316,9 +330,19 @@ export class iOSManage {
|
|
|
316
330
|
}
|
|
317
331
|
}
|
|
318
332
|
async restartApp(bundleId, deviceId = "booted") {
|
|
319
|
-
await this.terminateApp(bundleId, deviceId);
|
|
333
|
+
const terminateResult = await this.terminateApp(bundleId, deviceId);
|
|
320
334
|
const startResult = await this.startApp(bundleId, deviceId);
|
|
321
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
device: startResult.device,
|
|
337
|
+
appRestarted: startResult.appStarted,
|
|
338
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
339
|
+
output: startResult.output,
|
|
340
|
+
observedApp: startResult.observedApp,
|
|
341
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
342
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
343
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
344
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
345
|
+
};
|
|
322
346
|
}
|
|
323
347
|
async resetAppData(bundleId, deviceId = "booted") {
|
|
324
348
|
validateBundleId(bundleId);
|
package/dist/server/common.js
CHANGED
|
@@ -48,12 +48,13 @@ export function inferScrollFailure(message) {
|
|
|
48
48
|
return { failureCode: 'TIMEOUT', retryable: true };
|
|
49
49
|
return { failureCode: 'UNKNOWN', retryable: false };
|
|
50
50
|
}
|
|
51
|
-
export function buildActionExecutionResult({ actionType, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure }) {
|
|
51
|
+
export function buildActionExecutionResult({ actionType, device, selector, resolved, success, uiFingerprintBefore, uiFingerprintAfter, failure, details }) {
|
|
52
52
|
const timestamp = Date.now();
|
|
53
53
|
return {
|
|
54
54
|
action_id: nextActionId(actionType, timestamp),
|
|
55
55
|
timestamp,
|
|
56
56
|
action_type: actionType,
|
|
57
|
+
...(device ? { device } : {}),
|
|
57
58
|
target: {
|
|
58
59
|
selector,
|
|
59
60
|
resolved: normalizeResolvedTarget(resolved)
|
|
@@ -61,6 +62,7 @@ export function buildActionExecutionResult({ actionType, selector, resolved, suc
|
|
|
61
62
|
success,
|
|
62
63
|
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
|
|
63
64
|
ui_fingerprint_before: uiFingerprintBefore,
|
|
64
|
-
ui_fingerprint_after: uiFingerprintAfter
|
|
65
|
+
ui_fingerprint_after: uiFingerprintAfter,
|
|
66
|
+
...(details ? { details } : {})
|
|
65
67
|
};
|
|
66
68
|
}
|
|
@@ -13,11 +13,19 @@ async function handleStartApp(args) {
|
|
|
13
13
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
14
14
|
return wrapResponse(buildActionExecutionResult({
|
|
15
15
|
actionType: 'start_app',
|
|
16
|
+
device: res.device,
|
|
16
17
|
selector: { appId },
|
|
17
18
|
success: !!res.appStarted,
|
|
18
19
|
uiFingerprintBefore,
|
|
19
20
|
uiFingerprintAfter,
|
|
20
|
-
failure: res.appStarted ? undefined : inferGenericFailure(res.error)
|
|
21
|
+
failure: res.appStarted ? undefined : inferGenericFailure(res.error),
|
|
22
|
+
details: {
|
|
23
|
+
launch_time_ms: res.launchTimeMs,
|
|
24
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
25
|
+
...(res.device ? { device_id: res.device.id } : {}),
|
|
26
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
27
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
28
|
+
}
|
|
21
29
|
}));
|
|
22
30
|
}
|
|
23
31
|
async function handleTerminateApp(args) {
|
|
@@ -34,11 +42,20 @@ async function handleRestartApp(args) {
|
|
|
34
42
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId);
|
|
35
43
|
return wrapResponse(buildActionExecutionResult({
|
|
36
44
|
actionType: 'restart_app',
|
|
45
|
+
device: res.device,
|
|
37
46
|
selector: { appId },
|
|
38
47
|
success: !!res.appRestarted,
|
|
39
48
|
uiFingerprintBefore,
|
|
40
49
|
uiFingerprintAfter,
|
|
41
|
-
failure: res.appRestarted ? undefined : inferGenericFailure(res.error)
|
|
50
|
+
failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
|
|
51
|
+
details: {
|
|
52
|
+
launch_time_ms: res.launchTimeMs,
|
|
53
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
54
|
+
...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
|
|
55
|
+
...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
|
|
56
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
57
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
58
|
+
}
|
|
42
59
|
}));
|
|
43
60
|
}
|
|
44
61
|
async function handleResetAppData(args) {
|
package/dist/system/index.js
CHANGED
|
@@ -8,8 +8,30 @@ export async function getSystemStatus() {
|
|
|
8
8
|
const gradle = await checkGradle();
|
|
9
9
|
const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
|
|
10
10
|
const success = issues.length === 0;
|
|
11
|
+
const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue));
|
|
12
|
+
const iosReady = ios.iosAvailable && ios.iosDevices > 0;
|
|
13
|
+
const gradleReady = (gradle.issues || []).length === 0;
|
|
14
|
+
const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked');
|
|
15
|
+
const androidSummary = !android.adbAvailable
|
|
16
|
+
? 'ADB unavailable'
|
|
17
|
+
: android.devices === 0
|
|
18
|
+
? 'ADB available but no Android devices connected'
|
|
19
|
+
: android.logsAvailable
|
|
20
|
+
? `${android.devices} Android device(s) connected; log access available`
|
|
21
|
+
: `${android.devices} Android device(s) connected; log access unavailable`;
|
|
22
|
+
const iosSummary = !ios.iosAvailable
|
|
23
|
+
? 'xcrun unavailable'
|
|
24
|
+
: ios.iosDevices === 0
|
|
25
|
+
? 'xcrun available but no iOS simulators booted'
|
|
26
|
+
: `${ios.iosDevices} iOS simulator(s) booted`;
|
|
27
|
+
const gradleSummary = !gradle.gradleJavaHome
|
|
28
|
+
? 'No explicit Gradle JDK override detected'
|
|
29
|
+
: gradleReady
|
|
30
|
+
? `Gradle JDK configured at ${gradle.gradleJavaHome}`
|
|
31
|
+
: `Gradle JDK override invalid: ${gradle.gradleJavaHome}`;
|
|
11
32
|
return {
|
|
12
33
|
success,
|
|
34
|
+
status: overallStatus,
|
|
13
35
|
adbAvailable: android.adbAvailable,
|
|
14
36
|
adbVersion: android.adbVersion,
|
|
15
37
|
devices: android.devices,
|
|
@@ -23,10 +45,39 @@ export async function getSystemStatus() {
|
|
|
23
45
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
24
46
|
gradleValid: gradle.gradleValid,
|
|
25
47
|
gradleFilesChecked: gradle.filesChecked,
|
|
26
|
-
gradleSuggestedFixes: gradle.suggestedFixes
|
|
48
|
+
gradleSuggestedFixes: gradle.suggestedFixes,
|
|
49
|
+
summary: {
|
|
50
|
+
overall: overallStatus,
|
|
51
|
+
android: {
|
|
52
|
+
ready: androidReady,
|
|
53
|
+
summary: androidSummary,
|
|
54
|
+
blockers: android.issues
|
|
55
|
+
},
|
|
56
|
+
ios: {
|
|
57
|
+
ready: iosReady,
|
|
58
|
+
summary: iosSummary,
|
|
59
|
+
blockers: ios.issues
|
|
60
|
+
},
|
|
61
|
+
gradle: {
|
|
62
|
+
ready: gradleReady,
|
|
63
|
+
summary: gradleSummary,
|
|
64
|
+
blockers: gradle.issues || [],
|
|
65
|
+
suggestedFixes: gradle.suggestedFixes || []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
27
68
|
};
|
|
28
69
|
}
|
|
29
70
|
catch (e) {
|
|
30
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
status: 'blocked',
|
|
74
|
+
issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))],
|
|
75
|
+
summary: {
|
|
76
|
+
overall: 'blocked',
|
|
77
|
+
android: { ready: false, summary: 'Android status unavailable', blockers: [] },
|
|
78
|
+
ios: { ready: false, summary: 'iOS status unavailable', blockers: [] },
|
|
79
|
+
gradle: { ready: false, summary: 'Gradle status unavailable', blockers: [], suggestedFixes: [] }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
31
82
|
}
|
|
32
83
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.24.2]
|
|
6
|
+
- Fixed Android install issue
|
|
7
|
+
- Updated tools to have more detailed responses
|
|
8
|
+
|
|
9
|
+
## [0.24.1]
|
|
10
|
+
- Fixed Android install issue
|
|
11
|
+
- Updated tools to have more detailed responses
|
|
12
|
+
|
|
5
13
|
## [0.24.0]
|
|
6
14
|
- Improved execution loop
|
|
7
15
|
|