mobile-debug-mcp 0.12.1 → 0.12.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/README.md +1 -1
- package/dist/android/diagnostics.js +1 -24
- package/dist/android/manage.js +1 -1
- package/dist/ios/manage.js +38 -9
- package/dist/tools/interact.js +1 -1
- package/dist/tools/manage.js +1 -1
- package/dist/tools/observe.js +1 -1
- package/dist/utils/diagnostics.js +24 -0
- package/dist/utils/resolve-device.js +62 -0
- package/package.json +1 -1
- package/src/android/manage.ts +1 -1
- package/src/ios/manage.ts +40 -10
- package/src/tools/interact.ts +1 -1
- package/src/tools/manage.ts +1 -1
- package/src/tools/observe.ts +1 -1
- package/src/utils/diagnostics.ts +24 -0
- package/src/{resolve-device.ts → utils/resolve-device.ts} +3 -11
- package/src/android/diagnostics.ts +0 -23
package/README.md
CHANGED
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { getAdbCmd } from './utils.js';
|
|
3
|
-
import { makeEnvSnapshot } from '../utils/diagnostics.js';
|
|
4
|
-
export function execAdbWithDiagnostics(args, deviceId) {
|
|
5
|
-
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args;
|
|
6
|
-
const timeout = 120000;
|
|
7
|
-
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout });
|
|
8
|
-
const runResult = {
|
|
9
|
-
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
10
|
-
stdout: res.stdout || '',
|
|
11
|
-
stderr: res.stderr || '',
|
|
12
|
-
envSnapshot: makeEnvSnapshot(['PATH', 'ADB_PATH', 'HOME', 'JAVA_HOME']),
|
|
13
|
-
command: getAdbCmd(),
|
|
14
|
-
args: adbArgs,
|
|
15
|
-
suggestedFixes: []
|
|
16
|
-
};
|
|
17
|
-
if (res.status !== 0) {
|
|
18
|
-
if ((runResult.stderr || '').includes('device not found'))
|
|
19
|
-
runResult.suggestedFixes.push('Ensure device is connected and adb is authorized (adb devices)');
|
|
20
|
-
if ((runResult.stderr || '').includes('No such file or directory'))
|
|
21
|
-
runResult.suggestedFixes.push('Verify ADB_PATH or that adb is installed');
|
|
22
|
-
}
|
|
23
|
-
return { runResult };
|
|
24
|
-
}
|
|
1
|
+
export { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
package/dist/android/manage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from './utils.js';
|
|
6
|
-
import { execAdbWithDiagnostics } from '
|
|
6
|
+
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
8
|
export class AndroidManage {
|
|
9
9
|
async build(projectPath, _variant) {
|
package/dist/ios/manage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
|
-
import { spawn } from "child_process";
|
|
2
|
+
import { spawn, spawnSync } from "child_process";
|
|
3
3
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "./utils.js";
|
|
4
4
|
import path from "path";
|
|
5
5
|
export class iOSManage {
|
|
@@ -34,10 +34,12 @@ export class iOSManage {
|
|
|
34
34
|
catch { }
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Resolve projectPath to an absolute path to avoid cwd-relative resolution issues
|
|
38
|
+
const absProjectPath = path.resolve(projectPath);
|
|
39
|
+
const projectInfo = await findProject(absProjectPath, 3);
|
|
38
40
|
if (!projectInfo)
|
|
39
41
|
return { error: 'No Xcode project or workspace found' };
|
|
40
|
-
const projectRootDir = projectInfo.dir ||
|
|
42
|
+
const projectRootDir = projectInfo.dir || absProjectPath;
|
|
41
43
|
const workspace = projectInfo.workspace;
|
|
42
44
|
const proj = projectInfo.proj;
|
|
43
45
|
// Determine destination: prefer explicit env var, otherwise use booted simulator UDID
|
|
@@ -50,15 +52,38 @@ export class iOSManage {
|
|
|
50
52
|
}
|
|
51
53
|
catch { }
|
|
52
54
|
}
|
|
55
|
+
// Determine xcode command early so it can be used when detecting schemes
|
|
56
|
+
const xcodeCmd = process.env.XCODEBUILD_PATH || 'xcodebuild';
|
|
57
|
+
// Determine available schemes by querying xcodebuild -list rather than guessing
|
|
58
|
+
async function detectScheme(xcodeCmdInner, workspacePath, projectPathFull, cwd) {
|
|
59
|
+
try {
|
|
60
|
+
const args = workspacePath ? ['-list', '-workspace', workspacePath] : ['-list', '-project', projectPathFull];
|
|
61
|
+
// Run xcodebuild directly to list schemes
|
|
62
|
+
const res = spawnSync(xcodeCmdInner, args, { cwd: cwd || projectRootDir, encoding: 'utf8', timeout: 20000 });
|
|
63
|
+
const out = res.stdout || '';
|
|
64
|
+
const schemesMatch = out.match(/Schemes:\s*\n([\s\S]*?)(?:\n\n|$)/m);
|
|
65
|
+
if (schemesMatch) {
|
|
66
|
+
const block = schemesMatch[1];
|
|
67
|
+
const schemes = block.split(/\n/).map(s => s.trim()).filter(Boolean);
|
|
68
|
+
if (schemes.length)
|
|
69
|
+
return schemes[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
53
75
|
let buildArgs;
|
|
76
|
+
let chosenScheme = null;
|
|
54
77
|
if (workspace) {
|
|
55
78
|
const workspacePath = path.join(projectRootDir, workspace);
|
|
56
|
-
|
|
79
|
+
chosenScheme = await detectScheme(xcodeCmd, workspacePath, undefined, projectRootDir);
|
|
80
|
+
const scheme = chosenScheme || workspace.replace(/\.xcworkspace$/, '');
|
|
57
81
|
buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
|
|
58
82
|
}
|
|
59
83
|
else {
|
|
60
84
|
const projectPathFull = path.join(projectRootDir, proj);
|
|
61
|
-
|
|
85
|
+
chosenScheme = await detectScheme(xcodeCmd, undefined, projectPathFull, projectRootDir);
|
|
86
|
+
const scheme = chosenScheme || proj.replace(/\.xcodeproj$/, '');
|
|
62
87
|
buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
|
|
63
88
|
}
|
|
64
89
|
// If we have a destination UDID, add an explicit destination to avoid xcodebuild picking an ambiguous target
|
|
@@ -71,7 +96,6 @@ export class iOSManage {
|
|
|
71
96
|
await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => { });
|
|
72
97
|
await fs.mkdir(resultsDir, { recursive: true }).catch(() => { });
|
|
73
98
|
// Skip specifying -resultBundlePath to avoid platform-specific collisions; rely on stdout/stderr logs
|
|
74
|
-
const xcodeCmd = process.env.XCODEBUILD_PATH || 'xcodebuild';
|
|
75
99
|
const XCODEBUILD_TIMEOUT = parseInt(process.env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
|
|
76
100
|
const MAX_RETRIES = parseInt(process.env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
|
|
77
101
|
const tries = MAX_RETRIES + 1;
|
|
@@ -81,7 +105,7 @@ export class iOSManage {
|
|
|
81
105
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
82
106
|
// Run xcodebuild with a watchdog
|
|
83
107
|
const res = await new Promise((resolve) => {
|
|
84
|
-
const proc = spawn(xcodeCmd, buildArgs, { cwd:
|
|
108
|
+
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir });
|
|
85
109
|
let stdout = '';
|
|
86
110
|
let stderr = '';
|
|
87
111
|
proc.stdout?.on('data', d => stdout += d.toString());
|
|
@@ -112,6 +136,9 @@ export class iOSManage {
|
|
|
112
136
|
}
|
|
113
137
|
// record the failure for reporting
|
|
114
138
|
lastErr = new Error(res.stderr || `xcodebuild failed with code ${res.code}`);
|
|
139
|
+
lastErr.code = res.code;
|
|
140
|
+
lastErr.exitCode = res.code;
|
|
141
|
+
lastErr.killedByWatchdog = !!res.killedByWatchdog;
|
|
115
142
|
// write logs for diagnostics (helpful whether killed or not)
|
|
116
143
|
try {
|
|
117
144
|
await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stdout.log`), res.stdout).catch(() => { });
|
|
@@ -127,8 +154,10 @@ export class iOSManage {
|
|
|
127
154
|
break;
|
|
128
155
|
}
|
|
129
156
|
if (lastErr) {
|
|
130
|
-
// Include diagnostics and result bundle path when available
|
|
131
|
-
|
|
157
|
+
// Include diagnostics and result bundle path when available; provide structured info useful for agents
|
|
158
|
+
const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
|
|
159
|
+
const envSnapshot = { PATH: process.env.PATH };
|
|
160
|
+
return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: lastErr.code || null, invokedCommand, cwd: projectRootDir, envSnapshot } };
|
|
132
161
|
}
|
|
133
162
|
// Try to locate built .app. First search project tree, then DerivedData if necessary
|
|
134
163
|
const built = await findAppBundle(projectPath);
|
package/dist/tools/interact.js
CHANGED
package/dist/tools/manage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolveTargetDevice, listDevices } from '../resolve-device.js';
|
|
3
|
+
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js';
|
|
4
4
|
import { AndroidManage } from '../android/manage.js';
|
|
5
5
|
import { iOSManage } from '../ios/manage.js';
|
|
6
6
|
export class ToolsManage {
|
package/dist/tools/observe.js
CHANGED
|
@@ -23,3 +23,27 @@ export class DiagnosticError extends Error {
|
|
|
23
23
|
this.runResult = runResult;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
// Exec ADB with diagnostics — moved from src/android/diagnostics.ts
|
|
27
|
+
import { spawnSync } from 'child_process';
|
|
28
|
+
import { getAdbCmd } from '../android/utils.js';
|
|
29
|
+
export function execAdbWithDiagnostics(args, deviceId) {
|
|
30
|
+
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args;
|
|
31
|
+
const timeout = 120000;
|
|
32
|
+
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout });
|
|
33
|
+
const runResult = {
|
|
34
|
+
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
35
|
+
stdout: res.stdout || '',
|
|
36
|
+
stderr: res.stderr || '',
|
|
37
|
+
envSnapshot: makeEnvSnapshot(['PATH', 'ADB_PATH', 'HOME', 'JAVA_HOME']),
|
|
38
|
+
command: getAdbCmd(),
|
|
39
|
+
args: adbArgs,
|
|
40
|
+
suggestedFixes: []
|
|
41
|
+
};
|
|
42
|
+
if (res.status !== 0) {
|
|
43
|
+
if ((runResult.stderr || '').includes('device not found'))
|
|
44
|
+
runResult.suggestedFixes.push('Ensure device is connected and adb is authorized (adb devices)');
|
|
45
|
+
if ((runResult.stderr || '').includes('No such file or directory'))
|
|
46
|
+
runResult.suggestedFixes.push('Verify ADB_PATH or that adb is installed');
|
|
47
|
+
}
|
|
48
|
+
return { runResult };
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { listAndroidDevices } from "../android/utils.js";
|
|
2
|
+
import { listIOSDevices } from "../ios/utils.js";
|
|
3
|
+
function parseNumericVersion(v) {
|
|
4
|
+
if (!v)
|
|
5
|
+
return 0;
|
|
6
|
+
const m = v.match(/(\d+)(?:[\.\-](\d+))?/);
|
|
7
|
+
if (!m)
|
|
8
|
+
return 0;
|
|
9
|
+
const major = parseInt(m[1], 10) || 0;
|
|
10
|
+
const minor = parseInt(m[2] || "0", 10) || 0;
|
|
11
|
+
return major + minor / 100;
|
|
12
|
+
}
|
|
13
|
+
export async function listDevices(platform, appId) {
|
|
14
|
+
if (!platform || platform === "android") {
|
|
15
|
+
const android = await listAndroidDevices(appId);
|
|
16
|
+
if (platform === "android")
|
|
17
|
+
return android;
|
|
18
|
+
const ios = await listIOSDevices(appId);
|
|
19
|
+
return [...android, ...ios];
|
|
20
|
+
}
|
|
21
|
+
return listIOSDevices(appId);
|
|
22
|
+
}
|
|
23
|
+
export async function resolveTargetDevice(opts) {
|
|
24
|
+
const { platform, appId, prefer, deviceId } = opts;
|
|
25
|
+
const devices = await listDevices(platform, appId);
|
|
26
|
+
if (deviceId) {
|
|
27
|
+
const found = devices.find(d => d.id === deviceId);
|
|
28
|
+
if (!found)
|
|
29
|
+
throw new Error(`Device '${deviceId}' not found for platform ${platform}`);
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
let candidates = devices.slice();
|
|
33
|
+
if (prefer === "physical")
|
|
34
|
+
candidates = candidates.filter(d => !d.simulator);
|
|
35
|
+
if (prefer === "emulator")
|
|
36
|
+
candidates = candidates.filter(d => d.simulator);
|
|
37
|
+
if (appId) {
|
|
38
|
+
const installed = candidates.filter(d => d.appInstalled);
|
|
39
|
+
if (installed.length > 0)
|
|
40
|
+
candidates = installed;
|
|
41
|
+
}
|
|
42
|
+
if (candidates.length === 1)
|
|
43
|
+
return candidates[0];
|
|
44
|
+
if (candidates.length > 1) {
|
|
45
|
+
if (!prefer) {
|
|
46
|
+
const physical = candidates.filter(d => !d.simulator);
|
|
47
|
+
if (physical.length === 1)
|
|
48
|
+
return physical[0];
|
|
49
|
+
if (physical.length > 1)
|
|
50
|
+
candidates = physical;
|
|
51
|
+
}
|
|
52
|
+
candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion));
|
|
53
|
+
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
54
|
+
return candidates[0];
|
|
55
|
+
}
|
|
56
|
+
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: d.appInstalled }));
|
|
57
|
+
const err = new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`);
|
|
58
|
+
err.devices = list;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`No devices found for platform ${platform}`);
|
|
62
|
+
}
|
package/package.json
CHANGED
package/src/android/manage.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from 'child_process'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from './utils.js'
|
|
6
|
-
import { execAdbWithDiagnostics } from '
|
|
6
|
+
import { execAdbWithDiagnostics } from '../utils/diagnostics.js'
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js'
|
|
8
8
|
import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
9
9
|
|
package/src/ios/manage.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
|
-
import { spawn } from "child_process"
|
|
2
|
+
import { spawn, spawnSync } from "child_process"
|
|
3
3
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, InstallAppResponse } from "../types.js"
|
|
4
4
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "./utils.js"
|
|
5
5
|
import path from "path"
|
|
6
6
|
|
|
7
7
|
export class iOSManage {
|
|
8
|
-
async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string }> {
|
|
8
|
+
async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string, diagnostics?: any }> {
|
|
9
9
|
void _variant
|
|
10
10
|
try {
|
|
11
11
|
// Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
|
|
@@ -34,9 +34,11 @@ export class iOSManage {
|
|
|
34
34
|
return null
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Resolve projectPath to an absolute path to avoid cwd-relative resolution issues
|
|
38
|
+
const absProjectPath = path.resolve(projectPath)
|
|
39
|
+
const projectInfo = await findProject(absProjectPath, 3)
|
|
38
40
|
if (!projectInfo) return { error: 'No Xcode project or workspace found' }
|
|
39
|
-
const projectRootDir = projectInfo.dir ||
|
|
41
|
+
const projectRootDir = projectInfo.dir || absProjectPath
|
|
40
42
|
const workspace = projectInfo.workspace
|
|
41
43
|
const proj = projectInfo.proj
|
|
42
44
|
|
|
@@ -49,14 +51,37 @@ export class iOSManage {
|
|
|
49
51
|
} catch {}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// Determine xcode command early so it can be used when detecting schemes
|
|
55
|
+
const xcodeCmd = process.env.XCODEBUILD_PATH || 'xcodebuild'
|
|
56
|
+
|
|
57
|
+
// Determine available schemes by querying xcodebuild -list rather than guessing
|
|
58
|
+
async function detectScheme(xcodeCmdInner: string, workspacePath?: string, projectPathFull?: string, cwd?: string): Promise<string | null> {
|
|
59
|
+
try {
|
|
60
|
+
const args = workspacePath ? ['-list', '-workspace', workspacePath] : ['-list', '-project', projectPathFull!]
|
|
61
|
+
// Run xcodebuild directly to list schemes
|
|
62
|
+
const res = spawnSync(xcodeCmdInner, args, { cwd: cwd || projectRootDir, encoding: 'utf8', timeout: 20000 })
|
|
63
|
+
const out = res.stdout || ''
|
|
64
|
+
const schemesMatch = out.match(/Schemes:\s*\n([\s\S]*?)(?:\n\n|$)/m)
|
|
65
|
+
if (schemesMatch) {
|
|
66
|
+
const block = schemesMatch[1]
|
|
67
|
+
const schemes = block.split(/\n/).map(s => s.trim()).filter(Boolean)
|
|
68
|
+
if (schemes.length) return schemes[0]
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
let buildArgs: string[]
|
|
75
|
+
let chosenScheme: string | null = null
|
|
53
76
|
if (workspace) {
|
|
54
77
|
const workspacePath = path.join(projectRootDir, workspace)
|
|
55
|
-
|
|
78
|
+
chosenScheme = await detectScheme(xcodeCmd, workspacePath, undefined, projectRootDir)
|
|
79
|
+
const scheme = chosenScheme || workspace.replace(/\.xcworkspace$/, '')
|
|
56
80
|
buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build']
|
|
57
81
|
} else {
|
|
58
82
|
const projectPathFull = path.join(projectRootDir, proj!)
|
|
59
|
-
|
|
83
|
+
chosenScheme = await detectScheme(xcodeCmd, undefined, projectPathFull, projectRootDir)
|
|
84
|
+
const scheme = chosenScheme || proj!.replace(/\.xcodeproj$/, '')
|
|
60
85
|
buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build']
|
|
61
86
|
}
|
|
62
87
|
|
|
@@ -73,7 +98,6 @@ export class iOSManage {
|
|
|
73
98
|
// Skip specifying -resultBundlePath to avoid platform-specific collisions; rely on stdout/stderr logs
|
|
74
99
|
|
|
75
100
|
|
|
76
|
-
const xcodeCmd = process.env.XCODEBUILD_PATH || 'xcodebuild'
|
|
77
101
|
const XCODEBUILD_TIMEOUT = parseInt(process.env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000 // default 3 minutes
|
|
78
102
|
const MAX_RETRIES = parseInt(process.env.MCP_XCODEBUILD_RETRIES || '', 10) || 1
|
|
79
103
|
|
|
@@ -85,7 +109,7 @@ export class iOSManage {
|
|
|
85
109
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
86
110
|
// Run xcodebuild with a watchdog
|
|
87
111
|
const res = await new Promise<{ code: number | null, stdout: string, stderr: string, killedByWatchdog?: boolean }>((resolve) => {
|
|
88
|
-
const proc = spawn(xcodeCmd, buildArgs, { cwd:
|
|
112
|
+
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir })
|
|
89
113
|
let stdout = ''
|
|
90
114
|
let stderr = ''
|
|
91
115
|
|
|
@@ -119,6 +143,10 @@ export class iOSManage {
|
|
|
119
143
|
|
|
120
144
|
// record the failure for reporting
|
|
121
145
|
lastErr = new Error(res.stderr || `xcodebuild failed with code ${res.code}`)
|
|
146
|
+
// Attach exit code and watchdog info so diagnostics can include them
|
|
147
|
+
;(lastErr as any).code = res.code
|
|
148
|
+
;(lastErr as any).exitCode = res.code
|
|
149
|
+
;(lastErr as any).killedByWatchdog = !!res.killedByWatchdog
|
|
122
150
|
|
|
123
151
|
// write logs for diagnostics (helpful whether killed or not)
|
|
124
152
|
try {
|
|
@@ -136,8 +164,10 @@ export class iOSManage {
|
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
if (lastErr) {
|
|
139
|
-
// Include diagnostics and result bundle path when available
|
|
140
|
-
|
|
167
|
+
// Include diagnostics and result bundle path when available; provide structured info useful for agents
|
|
168
|
+
const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`
|
|
169
|
+
const envSnapshot = { PATH: process.env.PATH }
|
|
170
|
+
return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: (lastErr as any).code || null, invokedCommand, cwd: projectRootDir, envSnapshot } }
|
|
141
171
|
}
|
|
142
172
|
|
|
143
173
|
// Try to locate built .app. First search project tree, then DerivedData if necessary
|
package/src/tools/interact.ts
CHANGED
package/src/tools/manage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { resolveTargetDevice, listDevices } from '../resolve-device.js'
|
|
3
|
+
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js'
|
|
4
4
|
import { AndroidManage } from '../android/manage.js'
|
|
5
5
|
import { iOSManage } from '../ios/manage.js'
|
|
6
6
|
import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
package/src/tools/observe.ts
CHANGED
package/src/utils/diagnostics.ts
CHANGED
|
@@ -34,3 +34,27 @@ export class DiagnosticError extends Error {
|
|
|
34
34
|
this.runResult = runResult
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
// Exec ADB with diagnostics — moved from src/android/diagnostics.ts
|
|
39
|
+
import { spawnSync } from 'child_process'
|
|
40
|
+
import { getAdbCmd } from '../android/utils.js'
|
|
41
|
+
|
|
42
|
+
export function execAdbWithDiagnostics(args: string[], deviceId?: string) {
|
|
43
|
+
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args
|
|
44
|
+
const timeout = 120000
|
|
45
|
+
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout })
|
|
46
|
+
const runResult: RunResult = {
|
|
47
|
+
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
48
|
+
stdout: res.stdout || '',
|
|
49
|
+
stderr: res.stderr || '',
|
|
50
|
+
envSnapshot: makeEnvSnapshot(['PATH','ADB_PATH','HOME','JAVA_HOME']),
|
|
51
|
+
command: getAdbCmd(),
|
|
52
|
+
args: adbArgs,
|
|
53
|
+
suggestedFixes: []
|
|
54
|
+
}
|
|
55
|
+
if (res.status !== 0) {
|
|
56
|
+
if ((runResult.stderr || '').includes('device not found')) runResult.suggestedFixes!.push('Ensure device is connected and adb is authorized (adb devices)')
|
|
57
|
+
if ((runResult.stderr || '').includes('No such file or directory')) runResult.suggestedFixes!.push('Verify ADB_PATH or that adb is installed')
|
|
58
|
+
}
|
|
59
|
+
return { runResult }
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DeviceInfo } from "
|
|
2
|
-
import { listAndroidDevices } from "
|
|
3
|
-
import { listIOSDevices } from "
|
|
1
|
+
import { DeviceInfo } from "../types.js"
|
|
2
|
+
import { listAndroidDevices } from "../android/utils.js"
|
|
3
|
+
import { listIOSDevices } from "../ios/utils.js"
|
|
4
4
|
|
|
5
5
|
export interface ResolveOptions {
|
|
6
6
|
platform: "android" | "ios"
|
|
@@ -11,7 +11,6 @@ export interface ResolveOptions {
|
|
|
11
11
|
|
|
12
12
|
function parseNumericVersion(v: string): number {
|
|
13
13
|
if (!v) return 0
|
|
14
|
-
// extract first number groups like 17.0 -> 17.0 or Android 12 -> 12
|
|
15
14
|
const m = v.match(/(\d+)(?:[\.\-](\d+))?/)
|
|
16
15
|
if (!m) return 0
|
|
17
16
|
const major = parseInt(m[1], 10) || 0
|
|
@@ -23,7 +22,6 @@ export async function listDevices(platform?: "android" | "ios", appId?: string):
|
|
|
23
22
|
if (!platform || platform === "android") {
|
|
24
23
|
const android = await listAndroidDevices(appId)
|
|
25
24
|
if (platform === "android") return android
|
|
26
|
-
// if no platform specified, merge with ios below
|
|
27
25
|
const ios = await listIOSDevices(appId)
|
|
28
26
|
return [...android, ...ios]
|
|
29
27
|
}
|
|
@@ -42,11 +40,9 @@ export async function resolveTargetDevice(opts: ResolveOptions): Promise<DeviceI
|
|
|
42
40
|
|
|
43
41
|
let candidates = devices.slice()
|
|
44
42
|
|
|
45
|
-
// Apply prefer filter
|
|
46
43
|
if (prefer === "physical") candidates = candidates.filter(d => !d.simulator)
|
|
47
44
|
if (prefer === "emulator") candidates = candidates.filter(d => d.simulator)
|
|
48
45
|
|
|
49
|
-
// If appId provided, prefer devices with appInstalled
|
|
50
46
|
if (appId) {
|
|
51
47
|
const installed = candidates.filter(d => (d as any).appInstalled)
|
|
52
48
|
if (installed.length > 0) candidates = installed
|
|
@@ -55,21 +51,17 @@ export async function resolveTargetDevice(opts: ResolveOptions): Promise<DeviceI
|
|
|
55
51
|
if (candidates.length === 1) return candidates[0]
|
|
56
52
|
|
|
57
53
|
if (candidates.length > 1) {
|
|
58
|
-
// Prefer physical over emulator unless prefer=emulator
|
|
59
54
|
if (!prefer) {
|
|
60
55
|
const physical = candidates.filter(d => !d.simulator)
|
|
61
56
|
if (physical.length === 1) return physical[0]
|
|
62
57
|
if (physical.length > 1) candidates = physical
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
// Pick highest OS version
|
|
66
60
|
candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion))
|
|
67
|
-
// If top is unique (numeric differs), return it
|
|
68
61
|
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
69
62
|
return candidates[0]
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
// Ambiguous: throw an error with candidate list so caller (agent) can present choices
|
|
73
65
|
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: (d as any).appInstalled }))
|
|
74
66
|
const err = new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`)
|
|
75
67
|
;(err as any).devices = list
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process'
|
|
2
|
-
import { getAdbCmd } from './utils.js'
|
|
3
|
-
import { RunResult, makeEnvSnapshot } from '../utils/diagnostics.js'
|
|
4
|
-
|
|
5
|
-
export function execAdbWithDiagnostics(args: string[], deviceId?: string) {
|
|
6
|
-
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args
|
|
7
|
-
const timeout = 120000
|
|
8
|
-
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout }) as any
|
|
9
|
-
const runResult: RunResult = {
|
|
10
|
-
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
11
|
-
stdout: res.stdout || '',
|
|
12
|
-
stderr: res.stderr || '',
|
|
13
|
-
envSnapshot: makeEnvSnapshot(['PATH','ADB_PATH','HOME','JAVA_HOME']),
|
|
14
|
-
command: getAdbCmd(),
|
|
15
|
-
args: adbArgs,
|
|
16
|
-
suggestedFixes: []
|
|
17
|
-
}
|
|
18
|
-
if (res.status !== 0) {
|
|
19
|
-
if ((runResult.stderr || '').includes('device not found')) runResult.suggestedFixes!.push('Ensure device is connected and adb is authorized (adb devices)')
|
|
20
|
-
if ((runResult.stderr || '').includes('No such file or directory')) runResult.suggestedFixes!.push('Verify ADB_PATH or that adb is installed')
|
|
21
|
-
}
|
|
22
|
-
return { runResult }
|
|
23
|
-
}
|