mcp-maestro-mobile-ai 1.1.0 → 1.3.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,390 @@
1
+ /**
2
+ * Runtime Prerequisites Validation
3
+ *
4
+ * This module validates prerequisites when the MCP server starts.
5
+ * Critical missing dependencies will prevent server startup.
6
+ */
7
+
8
+ import { execSync } from "child_process";
9
+ import path from "path";
10
+ import { logger } from "./logger.js";
11
+
12
+ /**
13
+ * Execute a command safely and return result
14
+ */
15
+ function execCommand(command, options = {}) {
16
+ try {
17
+ return {
18
+ success: true,
19
+ output: execSync(command, {
20
+ encoding: "utf8",
21
+ timeout: options.timeout || 10000,
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ ...options,
24
+ }).trim(),
25
+ };
26
+ } catch (error) {
27
+ return {
28
+ success: false,
29
+ error: error.message,
30
+ };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Parse version string to extract major version number
36
+ */
37
+ function parseVersion(versionString) {
38
+ if (!versionString) return null;
39
+ const match = versionString.match(/(\d+)(?:\.(\d+))?/);
40
+ if (match) {
41
+ return {
42
+ major: parseInt(match[1], 10),
43
+ minor: match[2] ? parseInt(match[2], 10) : 0,
44
+ };
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Check Node.js version (should always pass since we're running in Node)
51
+ */
52
+ function checkNodeJs() {
53
+ const version = process.version;
54
+ const parsed = parseVersion(version);
55
+
56
+ if (!parsed || parsed.major < 18) {
57
+ return {
58
+ name: "Node.js",
59
+ status: "error",
60
+ installed: true,
61
+ version: version,
62
+ required: "18+",
63
+ message: `Node.js ${version} is outdated. Requires 18+.`,
64
+ hint: "Upgrade Node.js from https://nodejs.org/",
65
+ };
66
+ }
67
+
68
+ return {
69
+ name: "Node.js",
70
+ status: "ok",
71
+ installed: true,
72
+ version: version,
73
+ message: `Node.js ${version}`,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Check Java version
79
+ */
80
+ function checkJava() {
81
+ // Try java --version first (Java 9+)
82
+ let result = execCommand("java --version");
83
+
84
+ // Fall back to java -version
85
+ if (!result.success) {
86
+ result = execCommand("java -version 2>&1");
87
+ }
88
+
89
+ if (!result.success) {
90
+ return {
91
+ name: "Java",
92
+ status: "error",
93
+ installed: false,
94
+ required: "17+",
95
+ message: "Java is not installed or not in PATH.",
96
+ hint: "Install Java 17+ from https://adoptium.net/",
97
+ };
98
+ }
99
+
100
+ // Parse version
101
+ const versionMatch = result.output.match(
102
+ /(?:java|openjdk)\s+(?:version\s+)?["']?(\d+)/i
103
+ );
104
+
105
+ if (versionMatch) {
106
+ const major = parseInt(versionMatch[1], 10);
107
+
108
+ if (major < 17) {
109
+ return {
110
+ name: "Java",
111
+ status: "error",
112
+ installed: true,
113
+ version: `${major}`,
114
+ required: "17+",
115
+ message: `Java ${major} is outdated. Requires 17+.`,
116
+ hint: "Upgrade to Java 17+ from https://adoptium.net/",
117
+ };
118
+ }
119
+
120
+ return {
121
+ name: "Java",
122
+ status: "ok",
123
+ installed: true,
124
+ version: `${major}`,
125
+ message: `Java ${major}`,
126
+ };
127
+ }
128
+
129
+ // Can't determine version but Java is installed
130
+ return {
131
+ name: "Java",
132
+ status: "warning",
133
+ installed: true,
134
+ version: "unknown",
135
+ message: "Java installed (version unknown)",
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Check Maestro CLI
141
+ */
142
+ function checkMaestro() {
143
+ const result = execCommand("maestro --version");
144
+
145
+ if (!result.success) {
146
+ return {
147
+ name: "Maestro CLI",
148
+ status: "error",
149
+ installed: false,
150
+ message: "Maestro CLI is not installed or not in PATH.",
151
+ hint: "Install Maestro: curl -Ls https://get.maestro.mobile.dev | bash",
152
+ hintWindows: "Install Maestro: iwr https://get.maestro.mobile.dev | iex",
153
+ };
154
+ }
155
+
156
+ return {
157
+ name: "Maestro CLI",
158
+ status: "ok",
159
+ installed: true,
160
+ version: result.output,
161
+ message: `Maestro ${result.output}`,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Check Android SDK / ADB
167
+ */
168
+ function checkAndroidSdk() {
169
+ const androidHome = process.env.ANDROID_HOME;
170
+
171
+ if (!androidHome) {
172
+ return {
173
+ name: "Android SDK",
174
+ status: "warning",
175
+ installed: false,
176
+ message: "ANDROID_HOME environment variable is not set.",
177
+ hint: "Set ANDROID_HOME to your Android SDK path for reliable ADB access.",
178
+ };
179
+ }
180
+
181
+ // Check if ADB is accessible
182
+ const adbPath = path.join(androidHome, "platform-tools", "adb");
183
+ const result = execCommand(`"${adbPath}" --version`);
184
+
185
+ if (!result.success) {
186
+ // Try system PATH
187
+ const pathResult = execCommand("adb --version");
188
+ if (pathResult.success) {
189
+ return {
190
+ name: "Android SDK",
191
+ status: "ok",
192
+ installed: true,
193
+ message: `Android SDK (ADB available via PATH)`,
194
+ };
195
+ }
196
+
197
+ return {
198
+ name: "Android SDK",
199
+ status: "warning",
200
+ installed: true,
201
+ message: "ANDROID_HOME set but ADB not accessible.",
202
+ hint: "Ensure platform-tools is installed in Android SDK.",
203
+ };
204
+ }
205
+
206
+ return {
207
+ name: "Android SDK",
208
+ status: "ok",
209
+ installed: true,
210
+ path: androidHome,
211
+ message: `Android SDK configured`,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Check for connected device/emulator
217
+ */
218
+ function checkDevice() {
219
+ const androidHome = process.env.ANDROID_HOME;
220
+ let adbCmd = "adb";
221
+
222
+ if (androidHome) {
223
+ adbCmd = `"${path.join(androidHome, "platform-tools", "adb")}"`;
224
+ }
225
+
226
+ const result = execCommand(`${adbCmd} devices`);
227
+
228
+ if (!result.success) {
229
+ return {
230
+ name: "Device/Emulator",
231
+ status: "warning",
232
+ connected: false,
233
+ message: "Could not check for connected devices.",
234
+ hint: "Ensure ADB is properly configured.",
235
+ };
236
+ }
237
+
238
+ // Parse device list
239
+ const lines = result.output.split("\n").filter((l) => l.trim());
240
+ const devices = lines
241
+ .slice(1)
242
+ .filter((l) => l.includes("device") && !l.includes("offline"))
243
+ .map((l) => l.split("\t")[0]);
244
+
245
+ if (devices.length === 0) {
246
+ return {
247
+ name: "Device/Emulator",
248
+ status: "warning",
249
+ connected: false,
250
+ message: "No Android device or emulator connected.",
251
+ hint: "Start an emulator or connect a device via USB.",
252
+ };
253
+ }
254
+
255
+ return {
256
+ name: "Device/Emulator",
257
+ status: "ok",
258
+ connected: true,
259
+ devices: devices,
260
+ message: `${devices.length} device(s) connected`,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Run all prerequisite checks
266
+ * @param {Object} options - Options
267
+ * @param {boolean} options.exitOnError - Exit process if critical error found
268
+ * @param {boolean} options.checkDevice - Also check for connected device
269
+ * @returns {Object} Results object
270
+ */
271
+ export async function validatePrerequisites(options = {}) {
272
+ const { exitOnError = true, checkDevice: shouldCheckDevice = false } = options;
273
+
274
+ logger.info("Validating prerequisites...");
275
+
276
+ const checks = [
277
+ { fn: checkNodeJs, critical: true },
278
+ { fn: checkJava, critical: true },
279
+ { fn: checkMaestro, critical: true },
280
+ { fn: checkAndroidSdk, critical: false },
281
+ ];
282
+
283
+ if (shouldCheckDevice) {
284
+ checks.push({ fn: checkDevice, critical: false });
285
+ }
286
+
287
+ const results = [];
288
+ let hasErrors = false;
289
+ let hasWarnings = false;
290
+
291
+ for (const { fn, critical } of checks) {
292
+ const result = fn();
293
+ results.push({ ...result, critical });
294
+
295
+ if (result.status === "error") {
296
+ if (critical) {
297
+ hasErrors = true;
298
+ logger.error(`❌ ${result.name}: ${result.message}`);
299
+ } else {
300
+ hasWarnings = true;
301
+ logger.warn(`⚠️ ${result.name}: ${result.message}`);
302
+ }
303
+ } else if (result.status === "warning") {
304
+ hasWarnings = true;
305
+ logger.warn(`⚠️ ${result.name}: ${result.message}`);
306
+ } else {
307
+ logger.info(`✅ ${result.name}: ${result.message}`);
308
+ }
309
+ }
310
+
311
+ // Collect hints for failed checks
312
+ const hints = results
313
+ .filter((r) => r.hint && (r.status === "error" || r.status === "warning"))
314
+ .map((r) => {
315
+ // Use Windows hint if available and on Windows
316
+ if (process.platform === "win32" && r.hintWindows) {
317
+ return r.hintWindows;
318
+ }
319
+ return r.hint;
320
+ });
321
+
322
+ if (hints.length > 0) {
323
+ logger.info("");
324
+ logger.info("💡 To fix issues:");
325
+ hints.forEach((hint) => {
326
+ logger.info(` → ${hint}`);
327
+ });
328
+ }
329
+
330
+ // Handle critical errors
331
+ if (hasErrors && exitOnError) {
332
+ logger.error("");
333
+ logger.error("═══════════════════════════════════════════════════════");
334
+ logger.error(" CRITICAL: Required prerequisites are missing.");
335
+ logger.error(" The MCP server cannot start without them.");
336
+ logger.error("═══════════════════════════════════════════════════════");
337
+ logger.error("");
338
+ process.exit(2); // Exit code 2 for infrastructure errors
339
+ }
340
+
341
+ return {
342
+ success: !hasErrors,
343
+ hasWarnings,
344
+ results,
345
+ hints,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Quick check for critical prerequisites only
351
+ * Used for fast startup validation
352
+ */
353
+ export function quickCheck() {
354
+ const java = checkJava();
355
+ const maestro = checkMaestro();
356
+
357
+ const errors = [];
358
+
359
+ if (java.status === "error") {
360
+ errors.push({
361
+ name: "Java",
362
+ message: java.message,
363
+ hint: java.hint,
364
+ });
365
+ }
366
+
367
+ if (maestro.status === "error") {
368
+ errors.push({
369
+ name: "Maestro CLI",
370
+ message: maestro.message,
371
+ hint: process.platform === "win32" ? maestro.hintWindows : maestro.hint,
372
+ });
373
+ }
374
+
375
+ return {
376
+ success: errors.length === 0,
377
+ errors,
378
+ };
379
+ }
380
+
381
+ export default {
382
+ validatePrerequisites,
383
+ quickCheck,
384
+ checkNodeJs,
385
+ checkJava,
386
+ checkMaestro,
387
+ checkAndroidSdk,
388
+ checkDevice,
389
+ };
390
+