@irsprs/mobwright 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,796 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/doctor.ts
27
+ var import_node_child_process = require("child_process");
28
+ var import_node_util = require("util");
29
+ var import_node_fs = require("fs");
30
+
31
+ // src/cli/colors.ts
32
+ var enabled = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
33
+ function paint(code, text) {
34
+ return enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
35
+ }
36
+ var green = (s) => paint(32, s);
37
+ var yellow = (s) => paint(33, s);
38
+ var red = (s) => paint(31, s);
39
+ var dim = (s) => paint(2, s);
40
+ var bold = (s) => paint(1, s);
41
+
42
+ // src/cli/doctor.ts
43
+ var exec = (0, import_node_util.promisify)(import_node_child_process.exec);
44
+ async function tryExec(cmd, args = []) {
45
+ try {
46
+ const { stdout } = await exec([cmd, ...args].join(" "), { timeout: 5e3 });
47
+ return stdout.split("\n")[0]?.trim() ?? "";
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ async function httpProbe(url, timeoutMs = 1500) {
53
+ try {
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
56
+ const res = await fetch(url, { signal: controller.signal });
57
+ clearTimeout(timer);
58
+ return res.ok || res.status === 404;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ async function checkSystem() {
64
+ const results = [];
65
+ const nodeVersion = process.versions.node;
66
+ const major = parseInt(nodeVersion.split(".")[0], 10);
67
+ results.push({
68
+ status: major >= 18 ? "ok" : "fail",
69
+ label: `Node ${nodeVersion}`,
70
+ detail: major >= 18 ? void 0 : "Node 18.18 or higher required"
71
+ });
72
+ const pnpmVersion = await tryExec("pnpm", ["--version"]);
73
+ if (pnpmVersion) {
74
+ results.push({ status: "ok", label: `pnpm ${pnpmVersion}` });
75
+ }
76
+ return { title: "System", results };
77
+ }
78
+ async function checkAndroid() {
79
+ const results = [];
80
+ const androidHome = process.env.ANDROID_HOME;
81
+ if (!androidHome) {
82
+ results.push({
83
+ status: "warn",
84
+ label: "ANDROID_HOME not set",
85
+ detail: "Required for Android testing. Set to your Android SDK path."
86
+ });
87
+ return { title: "Android", results };
88
+ }
89
+ results.push({
90
+ status: "ok",
91
+ label: `ANDROID_HOME=${androidHome}`
92
+ });
93
+ if (!(0, import_node_fs.existsSync)(androidHome)) {
94
+ results.push({
95
+ status: "fail",
96
+ label: `Directory does not exist: ${androidHome}`
97
+ });
98
+ return { title: "Android", results };
99
+ }
100
+ const adb = await tryExec("adb", ["--version"]);
101
+ results.push({
102
+ status: adb ? "ok" : "warn",
103
+ label: "adb",
104
+ detail: adb ?? "Not in PATH. Add $ANDROID_HOME/platform-tools."
105
+ });
106
+ const emulatorHelp = await tryExec("emulator", ["-help-version"]);
107
+ results.push({
108
+ status: emulatorHelp !== null ? "ok" : "warn",
109
+ label: "emulator",
110
+ detail: emulatorHelp !== null ? void 0 : "Not in PATH. Add $ANDROID_HOME/emulator."
111
+ });
112
+ if (adb) {
113
+ const devices = await tryExec("adb", ["devices"]);
114
+ const lines = (devices ?? "").split("\n").slice(1).filter((l) => l.includes(" device"));
115
+ if (lines.length > 0) {
116
+ results.push({
117
+ status: "info",
118
+ label: `${lines.length} emulator${lines.length > 1 ? "s" : ""} booted`
119
+ });
120
+ }
121
+ }
122
+ return { title: "Android", results };
123
+ }
124
+ async function checkIOS() {
125
+ const results = [];
126
+ if (process.platform !== "darwin") {
127
+ results.push({
128
+ status: "info",
129
+ label: "iOS checks skipped (not macOS)"
130
+ });
131
+ return { title: "iOS", results };
132
+ }
133
+ const xcodeVersion = await tryExec("xcodebuild", ["-version"]);
134
+ results.push({
135
+ status: xcodeVersion ? "ok" : "warn",
136
+ label: xcodeVersion ?? "Xcode not found"
137
+ });
138
+ const xcrun = await tryExec("xcrun", ["--version"]);
139
+ results.push({
140
+ status: xcrun ? "ok" : "warn",
141
+ label: xcrun ? "xcrun" : "xcrun not in PATH"
142
+ });
143
+ const sims = await tryExec("xcrun", ["simctl", "list", "devices", "booted"]);
144
+ if (sims) {
145
+ const lines = sims.split("\n").filter((l) => l.includes("(Booted)"));
146
+ if (lines.length > 0) {
147
+ results.push({
148
+ status: "info",
149
+ label: `${lines.length} simulator${lines.length > 1 ? "s" : ""} booted`
150
+ });
151
+ }
152
+ }
153
+ return { title: "iOS", results };
154
+ }
155
+ async function checkAppium() {
156
+ const results = [];
157
+ const appiumVersion = await tryExec("appium", ["--version"]);
158
+ if (!appiumVersion) {
159
+ results.push({
160
+ status: "fail",
161
+ label: "Appium not installed",
162
+ detail: "npm install -g appium"
163
+ });
164
+ return { title: "Appium", results };
165
+ }
166
+ results.push({ status: "ok", label: `Appium ${appiumVersion}` });
167
+ const driverList = await tryExec("appium", ["driver", "list", "--installed"]);
168
+ if (driverList) {
169
+ const hasUiAutomator2 = driverList.includes("uiautomator2");
170
+ const hasXCUITest = driverList.includes("xcuitest");
171
+ results.push({
172
+ status: hasUiAutomator2 ? "ok" : "warn",
173
+ label: "uiautomator2 driver",
174
+ detail: hasUiAutomator2 ? void 0 : "appium driver install uiautomator2"
175
+ });
176
+ if (process.platform === "darwin") {
177
+ results.push({
178
+ status: hasXCUITest ? "ok" : "warn",
179
+ label: "xcuitest driver",
180
+ detail: hasXCUITest ? void 0 : "appium driver install xcuitest"
181
+ });
182
+ }
183
+ }
184
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
185
+ const reachable = await httpProbe(`http://localhost:${port}/status`);
186
+ results.push({
187
+ status: reachable ? "ok" : "warn",
188
+ label: `Server reachable at localhost:${port}`,
189
+ detail: reachable ? void 0 : "Start with `appium` in another terminal."
190
+ });
191
+ return { title: "Appium", results };
192
+ }
193
+ function checkEnvVars() {
194
+ const results = [];
195
+ const required = ["MOBWRIGHT_APP_PATH", "MOBWRIGHT_ANDROID_AVD"];
196
+ for (const key of required) {
197
+ const value = process.env[key];
198
+ results.push({
199
+ status: value ? "ok" : "warn",
200
+ label: `${key}${value ? " set" : " not set"}`,
201
+ detail: value ? void 0 : "Required for Android testing."
202
+ });
203
+ }
204
+ const aiProvider = process.env.MOBWRIGHT_AI_PROVIDER;
205
+ const aiKey = process.env.MOBWRIGHT_AI_API_KEY;
206
+ if (!aiProvider || !aiKey) {
207
+ results.push({
208
+ status: "warn",
209
+ label: "MOBWRIGHT_AI_API_KEY not set",
210
+ detail: "AI features (device.ai) unavailable until set."
211
+ });
212
+ } else {
213
+ results.push({
214
+ status: "ok",
215
+ label: `AI provider: ${aiProvider}`
216
+ });
217
+ }
218
+ return { title: "Environment variables", results };
219
+ }
220
+ function symbol(status) {
221
+ switch (status) {
222
+ case "ok":
223
+ return green("\u2714");
224
+ case "info":
225
+ return green("\u2713");
226
+ case "warn":
227
+ return yellow("\u2717");
228
+ case "fail":
229
+ return red("\u2717");
230
+ }
231
+ }
232
+ function renderSection(section) {
233
+ console.log("\n" + bold(section.title));
234
+ for (const r of section.results) {
235
+ const indent = " ";
236
+ console.log(`${indent}${symbol(r.status)} ${r.label}`);
237
+ if (r.detail) {
238
+ console.log(`${indent} ${dim(r.detail)}`);
239
+ }
240
+ }
241
+ }
242
+ async function runDoctor() {
243
+ console.log(bold("\u{1F50D} Mobwright environment check"));
244
+ const sections = await Promise.all([
245
+ checkSystem(),
246
+ checkAndroid(),
247
+ checkIOS(),
248
+ checkAppium()
249
+ ]);
250
+ sections.push(checkEnvVars());
251
+ for (const section of sections) {
252
+ renderSection(section);
253
+ }
254
+ const all = sections.flatMap((s) => s.results);
255
+ const fails = all.filter((r) => r.status === "fail").length;
256
+ const warns = all.filter((r) => r.status === "warn").length;
257
+ console.log("");
258
+ if (fails > 0) {
259
+ console.log(red(bold(`Result: ${fails} error${fails > 1 ? "s" : ""}, ${warns} warning${warns !== 1 ? "s" : ""}.`)));
260
+ console.log(dim("Fix errors above before running tests."));
261
+ return 1;
262
+ }
263
+ if (warns > 0) {
264
+ console.log(yellow(bold(`Result: ${warns} warning${warns > 1 ? "s" : ""}.`)));
265
+ console.log(dim("Tests should run, but some features may be limited."));
266
+ return 0;
267
+ }
268
+ console.log(green(bold("Result: All checks passed. \u2728")));
269
+ return 0;
270
+ }
271
+
272
+ // src/cli/init.ts
273
+ var import_promises2 = require("fs/promises");
274
+ var import_node_fs2 = require("fs");
275
+ var import_node_path = __toESM(require("path"), 1);
276
+ var import_node_child_process2 = require("child_process");
277
+ var import_node_util2 = require("util");
278
+
279
+ // src/cli/prompt.ts
280
+ var import_promises = require("readline/promises");
281
+ var rl = null;
282
+ function getInterface() {
283
+ if (!rl) {
284
+ rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
285
+ }
286
+ return rl;
287
+ }
288
+ function closePrompt() {
289
+ if (rl) {
290
+ rl.close();
291
+ rl = null;
292
+ }
293
+ }
294
+ async function ask(question, defaultValue) {
295
+ const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
296
+ const answer = (await getInterface().question(`${question}${suffix}: `)).trim();
297
+ return answer || defaultValue || "";
298
+ }
299
+ async function confirm(question, defaultYes = true) {
300
+ const hint = defaultYes ? "Y/n" : "y/N";
301
+ const answer = (await getInterface().question(`${question} (${hint}): `)).trim().toLowerCase();
302
+ if (!answer) return defaultYes;
303
+ return answer === "y" || answer === "yes";
304
+ }
305
+ async function pickOne(question, options, defaultIndex = 0) {
306
+ console.log(question);
307
+ options.forEach((opt, idx) => {
308
+ const marker = idx === defaultIndex ? "\u203A" : " ";
309
+ console.log(` ${marker} ${idx + 1}. ${opt}`);
310
+ });
311
+ const raw = (await getInterface().question(`Choose (1-${options.length}, default ${defaultIndex + 1}): `)).trim();
312
+ const choice = raw ? parseInt(raw, 10) - 1 : defaultIndex;
313
+ if (Number.isNaN(choice) || choice < 0 || choice >= options.length) {
314
+ return options[defaultIndex];
315
+ }
316
+ return options[choice];
317
+ }
318
+
319
+ // src/cli/templates.ts
320
+ var PACKAGE_JSON = (opts) => JSON.stringify(
321
+ {
322
+ name: opts.name,
323
+ version: "0.1.0",
324
+ private: true,
325
+ type: "module",
326
+ scripts: {
327
+ test: "mobwright test",
328
+ "test:android": "mobwright test --project=android",
329
+ "test:ios": "mobwright test --project=ios",
330
+ doctor: "mobwright doctor"
331
+ },
332
+ devDependencies: {
333
+ "@playwright/test": "^1.48.0",
334
+ mobwright: "^0.1.0",
335
+ dotenv: "^16.4.0",
336
+ typescript: "^5.4.0"
337
+ }
338
+ },
339
+ null,
340
+ 2
341
+ ) + "\n";
342
+ var TSCONFIG = () => JSON.stringify(
343
+ {
344
+ compilerOptions: {
345
+ target: "ES2022",
346
+ module: "NodeNext",
347
+ moduleResolution: "NodeNext",
348
+ strict: true,
349
+ esModuleInterop: true,
350
+ skipLibCheck: true
351
+ },
352
+ include: ["tests/**/*", "playwright.config.ts"]
353
+ },
354
+ null,
355
+ 2
356
+ ) + "\n";
357
+ var PLAYWRIGHT_CONFIG = (opts) => {
358
+ const projects = opts.platforms.map(
359
+ (p) => ` {
360
+ name: '${p}',
361
+ testMatch: ['**/shared/**', '**/${p}/**'],
362
+ },`
363
+ ).join("\n");
364
+ return `import { defineConfig } from '@playwright/test';
365
+ import dotenv from 'dotenv';
366
+ import path from 'node:path';
367
+ import { fileURLToPath } from 'node:url';
368
+
369
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
370
+ dotenv.config({ path: path.join(__dirname, '.env') });
371
+
372
+ export default defineConfig({
373
+ testDir: './tests',
374
+ fullyParallel: false,
375
+ retries: 0,
376
+ workers: 1,
377
+ reporter: 'list',
378
+ timeout: 120_000,
379
+ projects: [
380
+ ${projects}
381
+ ],
382
+ });
383
+ `;
384
+ };
385
+ var ENV_EXAMPLE = (opts) => {
386
+ const lines = ["# Mobwright local config \u2014 copy to .env and fill in.", ""];
387
+ if (opts.platforms.includes("android")) {
388
+ lines.push(
389
+ "# Android",
390
+ "MOBWRIGHT_APP_PATH=/absolute/path/to/your/app.apk",
391
+ "MOBWRIGHT_ANDROID_AVD=Pixel_6_API_34",
392
+ "# MOBWRIGHT_ANDROID_APP_PACKAGE=com.example",
393
+ "# MOBWRIGHT_ANDROID_APP_ACTIVITY=com.example.MainActivity",
394
+ ""
395
+ );
396
+ }
397
+ if (opts.platforms.includes("ios")) {
398
+ lines.push(
399
+ "# iOS",
400
+ "MOBWRIGHT_IOS_DEVICE=iPhone 15",
401
+ "MOBWRIGHT_IOS_BUNDLE_ID=com.example.app",
402
+ "# MOBWRIGHT_IOS_APP_PATH=/absolute/path/to/My.app",
403
+ "# MOBWRIGHT_IOS_UDID=",
404
+ ""
405
+ );
406
+ }
407
+ if (opts.aiProvider !== "none") {
408
+ lines.push(
409
+ "# AI provider",
410
+ `MOBWRIGHT_AI_PROVIDER=${opts.aiProvider}`,
411
+ "MOBWRIGHT_AI_API_KEY=sk-...",
412
+ ""
413
+ );
414
+ } else {
415
+ lines.push(
416
+ "# AI provider (optional)",
417
+ "# MOBWRIGHT_AI_PROVIDER=deepseek",
418
+ "# MOBWRIGHT_AI_API_KEY=sk-...",
419
+ ""
420
+ );
421
+ }
422
+ return lines.join("\n");
423
+ };
424
+ var GITIGNORE = () => `node_modules/
425
+ .env
426
+ .env.local
427
+ test-results/
428
+ playwright-report/
429
+ *.log
430
+ .DS_Store
431
+ `;
432
+ var SANITY_SPEC = () => `import { test, expect } from 'mobwright';
433
+
434
+ test('session boots cleanly', async ({ device }) => {
435
+ expect(device.browser.sessionId).toBeTruthy();
436
+ console.log(\`\u2713 \${device.project} session\`);
437
+ });
438
+
439
+ test('can take a screenshot', async ({ device }) => {
440
+ const png = await device.screenshot();
441
+ expect(png.length).toBeGreaterThan(1000);
442
+ });
443
+ `;
444
+ var PLATFORM_EXAMPLE = (platform) => `import { test, expect } from 'mobwright';
445
+
446
+ test('${platform}: example test', async ({ device }) => {
447
+ // Replace with a real selector from your app
448
+ const greeting = device.locator('~welcome');
449
+
450
+ // Auto-waits up to 5 seconds for the element to appear
451
+ await expect(greeting).toBeVisible();
452
+ });
453
+ `;
454
+ var README = (opts) => `# ${opts.name}
455
+
456
+ Mobile e2e tests powered by [mobwright](https://www.npmjs.com/package/mobwright).
457
+
458
+ ## Setup
459
+
460
+ 1. Copy \`.env.example\` to \`.env\` and fill in your device info:
461
+ \`\`\`bash
462
+ cp .env.example .env
463
+ \`\`\`
464
+
465
+ 2. Verify your environment:
466
+ \`\`\`bash
467
+ npx mobwright doctor
468
+ \`\`\`
469
+
470
+ 3. Boot a device (Android emulator or iOS simulator).
471
+
472
+ 4. Start the Appium server in a separate terminal:
473
+ \`\`\`bash
474
+ appium
475
+ \`\`\`
476
+
477
+ 5. Run the tests:
478
+ \`\`\`bash
479
+ npm test # both platforms
480
+ npm run test:android # Android only${opts.platforms.includes("ios") ? `
481
+ npm run test:ios # iOS only` : ""}
482
+ \`\`\`
483
+
484
+ ## Project structure
485
+
486
+ \`\`\`
487
+ tests/
488
+ \u251C\u2500\u2500 shared/ # Tests that run on both platforms
489
+ ${opts.platforms.includes("android") ? "\u251C\u2500\u2500 android/ # Android-only tests\n" : ""}${opts.platforms.includes("ios") ? "\u2514\u2500\u2500 ios/ # iOS-only tests\n" : ""}\`\`\`
490
+
491
+ ## Learn more
492
+
493
+ - [Mobwright docs](https://github.com/yourname/mobwright)
494
+ - [Playwright test runner](https://playwright.dev/docs/test-intro)
495
+ `;
496
+
497
+ // src/cli/init.ts
498
+ var exec2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
499
+ async function runInit(flags = {}) {
500
+ console.log(bold("\u{1FA84} Mobwright project setup\n"));
501
+ try {
502
+ const dirInput = flags.dir ?? (flags.yes ? "./my-mobile-tests" : await ask("Project directory", "./my-mobile-tests"));
503
+ const targetDir = import_node_path.default.resolve(dirInput);
504
+ if ((0, import_node_fs2.existsSync)(targetDir)) {
505
+ const contents = await (0, import_promises2.readdir)(targetDir);
506
+ const nonHidden = contents.filter((f) => !f.startsWith("."));
507
+ if (nonHidden.length > 0 && !flags.force) {
508
+ console.error(red(`\u2717 Directory ${targetDir} is not empty.`));
509
+ console.error(dim(" Pass --force to overwrite, or pick another directory."));
510
+ return 1;
511
+ }
512
+ }
513
+ let platforms;
514
+ if (flags.platforms) {
515
+ platforms = parsePlatformsFlag(flags.platforms);
516
+ } else if (flags.yes) {
517
+ platforms = ["android", "ios"];
518
+ } else {
519
+ const choice = await pickOne(
520
+ "Which platforms?",
521
+ ["Both Android and iOS", "Android only", "iOS only"]
522
+ );
523
+ platforms = choice === "Android only" ? ["android"] : choice === "iOS only" ? ["ios"] : ["android", "ios"];
524
+ }
525
+ const aiProvider = flags.ai ? parseAIFlag(flags.ai) : flags.yes ? "none" : await pickOne(
526
+ "AI provider (optional)?",
527
+ ["none", "deepseek", "openai", "anthropic"]
528
+ );
529
+ const shouldInstall = flags.skipInstall ? false : flags.yes ? true : await confirm("Install dependencies now?", true);
530
+ const projectName = import_node_path.default.basename(targetDir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
531
+ const opts = {
532
+ name: projectName,
533
+ platforms,
534
+ aiProvider
535
+ };
536
+ console.log("");
537
+ console.log(`Creating project at ${dim(targetDir)}...`);
538
+ await (0, import_promises2.mkdir)(targetDir, { recursive: true });
539
+ await writeFiles(targetDir, opts);
540
+ if (shouldInstall) {
541
+ console.log(" " + dim("Installing dependencies (this may take a minute)..."));
542
+ try {
543
+ await exec2("npm install", { cwd: targetDir });
544
+ console.log(` ${green("\u2714")} Dependencies installed`);
545
+ } catch (err) {
546
+ console.log(` ${yellow("\u2717")} Install failed. Run \`npm install\` manually in ${projectName}/.`);
547
+ }
548
+ }
549
+ printNextSteps(projectName, shouldInstall);
550
+ return 0;
551
+ } catch (err) {
552
+ if (err.code === "ERR_USE_AFTER_CLOSE") {
553
+ console.log("\n" + yellow("Cancelled."));
554
+ return 130;
555
+ }
556
+ console.error(red(`\u2717 ${err.message}`));
557
+ return 1;
558
+ } finally {
559
+ closePrompt();
560
+ }
561
+ }
562
+ async function writeFiles(targetDir, opts) {
563
+ const files = [
564
+ ["package.json", PACKAGE_JSON(opts)],
565
+ ["tsconfig.json", TSCONFIG()],
566
+ ["playwright.config.ts", PLAYWRIGHT_CONFIG(opts)],
567
+ [".env.example", ENV_EXAMPLE(opts)],
568
+ [".gitignore", GITIGNORE()],
569
+ ["README.md", README(opts)],
570
+ ["tests/shared/sanity.spec.ts", SANITY_SPEC()]
571
+ ];
572
+ for (const platform of opts.platforms) {
573
+ files.push([`tests/${platform}/example.spec.ts`, PLATFORM_EXAMPLE(platform)]);
574
+ }
575
+ for (const [relPath, content] of files) {
576
+ const fullPath = import_node_path.default.join(targetDir, relPath);
577
+ await (0, import_promises2.mkdir)(import_node_path.default.dirname(fullPath), { recursive: true });
578
+ await (0, import_promises2.writeFile)(fullPath, content, "utf8");
579
+ console.log(` ${green("\u2714")} ${relPath}`);
580
+ }
581
+ }
582
+ function parsePlatformsFlag(value) {
583
+ const parts = value.split(",").map((p) => p.trim().toLowerCase());
584
+ const result = [];
585
+ for (const p of parts) {
586
+ if (p === "android" || p === "ios") {
587
+ result.push(p);
588
+ } else {
589
+ throw new Error(`Unknown platform: ${p} (use 'android' or 'ios')`);
590
+ }
591
+ }
592
+ if (result.length === 0) {
593
+ throw new Error("At least one platform required");
594
+ }
595
+ return result;
596
+ }
597
+ function parseAIFlag(value) {
598
+ const v = value.trim().toLowerCase();
599
+ if (v === "none" || v === "anthropic" || v === "openai" || v === "deepseek") {
600
+ return v;
601
+ }
602
+ throw new Error(`Unknown AI provider: ${value} (use 'none', 'anthropic', 'openai', 'deepseek')`);
603
+ }
604
+ function printNextSteps(projectName, installed) {
605
+ console.log("");
606
+ console.log(green(bold("\u2728 Done!")) + " Next steps:\n");
607
+ console.log(` ${dim("# enter your new project")}`);
608
+ console.log(` cd ${projectName}
609
+ `);
610
+ if (!installed) {
611
+ console.log(` ${dim("# install dependencies")}`);
612
+ console.log(` npm install
613
+ `);
614
+ }
615
+ console.log(` ${dim("# configure your devices")}`);
616
+ console.log(` cp .env.example .env
617
+ `);
618
+ console.log(` ${dim("# verify your environment")}`);
619
+ console.log(` npx mobwright doctor
620
+ `);
621
+ console.log(` ${dim("# boot a device + run Appium in another terminal, then:")}`);
622
+ console.log(` npx playwright test`);
623
+ console.log("");
624
+ }
625
+
626
+ // src/cli/test.ts
627
+ var import_node_child_process3 = require("child_process");
628
+ async function runTest(flags) {
629
+ console.log(bold("\u{1F3AF} Mobwright \u2014 running tests") + "\n");
630
+ if (!flags.noPreflight) {
631
+ const ok = await checkAppium2();
632
+ if (!ok) {
633
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
634
+ console.error(red("\u2717") + ` Appium server not reachable at http://localhost:${port}.`);
635
+ console.error(dim(" Start it in another terminal: ") + bold("appium"));
636
+ console.error(dim(" Or skip this check: ") + bold("npx mobwright test --no-preflight"));
637
+ console.error("");
638
+ return 1;
639
+ }
640
+ console.log(green("\u2714") + " Appium reachable");
641
+ console.log("");
642
+ }
643
+ return await forwardToPlaywright(flags.rest);
644
+ }
645
+ async function checkAppium2() {
646
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
647
+ const url = `http://localhost:${port}/status`;
648
+ try {
649
+ const controller = new AbortController();
650
+ const timer = setTimeout(() => controller.abort(), 1500);
651
+ const res = await fetch(url, { signal: controller.signal });
652
+ clearTimeout(timer);
653
+ return res.ok;
654
+ } catch {
655
+ return false;
656
+ }
657
+ }
658
+ function forwardToPlaywright(args) {
659
+ return new Promise((resolve) => {
660
+ const child = (0, import_node_child_process3.spawn)("npx", ["playwright", "test", ...args], {
661
+ stdio: "inherit",
662
+ shell: process.platform === "win32"
663
+ // Windows needs shell=true for npx
664
+ });
665
+ child.on("exit", (code, signal) => {
666
+ if (signal) {
667
+ resolve(128);
668
+ return;
669
+ }
670
+ resolve(code ?? 0);
671
+ });
672
+ child.on("error", (err) => {
673
+ console.error(red("\u2717") + ` Failed to spawn playwright: ${err.message}`);
674
+ console.error(dim(" Is @playwright/test installed?"));
675
+ resolve(1);
676
+ });
677
+ });
678
+ }
679
+
680
+ // src/cli/index.ts
681
+ async function main() {
682
+ const args = process.argv.slice(2);
683
+ const command = args[0];
684
+ switch (command) {
685
+ case "doctor": {
686
+ const exitCode = await runDoctor();
687
+ process.exit(exitCode);
688
+ }
689
+ case "init": {
690
+ const flags = parseInitFlags(args.slice(1));
691
+ const exitCode = await runInit(flags);
692
+ process.exit(exitCode);
693
+ }
694
+ case "test": {
695
+ const flags = parseTestFlags(args.slice(1));
696
+ const exitCode = await runTest(flags);
697
+ process.exit(exitCode);
698
+ }
699
+ case void 0:
700
+ case "-h":
701
+ case "--help":
702
+ case "help": {
703
+ printHelp();
704
+ process.exit(0);
705
+ }
706
+ case "-v":
707
+ case "--version": {
708
+ console.log("mobwright 0.1.0");
709
+ process.exit(0);
710
+ }
711
+ default: {
712
+ console.error(`Unknown command: ${command}`);
713
+ console.error("");
714
+ printHelp();
715
+ process.exit(2);
716
+ }
717
+ }
718
+ }
719
+ function parseInitFlags(args) {
720
+ const flags = {};
721
+ for (let i = 0; i < args.length; i++) {
722
+ const arg = args[i];
723
+ if (arg === "-y" || arg === "--yes") {
724
+ flags.yes = true;
725
+ } else if (arg === "--force") {
726
+ flags.force = true;
727
+ } else if (arg === "--skip-install") {
728
+ flags.skipInstall = true;
729
+ } else if (arg === "--dir") {
730
+ flags.dir = args[++i];
731
+ } else if (arg.startsWith("--dir=")) {
732
+ flags.dir = arg.slice("--dir=".length);
733
+ } else if (arg === "--platforms") {
734
+ flags.platforms = args[++i];
735
+ } else if (arg.startsWith("--platforms=")) {
736
+ flags.platforms = arg.slice("--platforms=".length);
737
+ } else if (arg === "--ai") {
738
+ flags.ai = args[++i];
739
+ } else if (arg.startsWith("--ai=")) {
740
+ flags.ai = arg.slice("--ai=".length);
741
+ } else if (!arg.startsWith("-") && !flags.dir) {
742
+ flags.dir = arg;
743
+ }
744
+ }
745
+ return flags;
746
+ }
747
+ function parseTestFlags(args) {
748
+ const flags = { rest: [] };
749
+ for (let i = 0; i < args.length; i++) {
750
+ const arg = args[i];
751
+ if (arg === "--no-preflight") {
752
+ flags.noPreflight = true;
753
+ } else {
754
+ flags.rest.push(arg);
755
+ }
756
+ }
757
+ return flags;
758
+ }
759
+ function printHelp() {
760
+ console.log(`mobwright \u2014 mobile e2e testing with AI
761
+
762
+ Usage:
763
+ mobwright <command> [options]
764
+
765
+ Commands:
766
+ init [dir] Scaffold a new mobwright project
767
+ test Run your test suite
768
+ doctor Check your environment for required tools and config
769
+ help Show this help
770
+
771
+ init options:
772
+ --yes, -y Accept all defaults (no prompts)
773
+ --dir <path> Project directory (default: ./my-mobile-tests)
774
+ --platforms <list> Comma-separated: android,ios (default: both)
775
+ --ai <provider> none, deepseek, openai, anthropic (default: none)
776
+ --skip-install Don't run npm install
777
+ --force Overwrite non-empty target directory
778
+
779
+ test options:
780
+ --no-preflight Skip Appium reachability check
781
+ All other args are forwarded to \`playwright test\`.
782
+
783
+ Examples:
784
+ mobwright init
785
+ mobwright test --project=android
786
+ mobwright test --grep "login" --workers=2
787
+ mobwright doctor
788
+
789
+ Docs: https://github.com/yourname/mobwright
790
+ `);
791
+ }
792
+ main().catch((err) => {
793
+ console.error("Unexpected error:", err);
794
+ process.exit(1);
795
+ });
796
+ //# sourceMappingURL=index.cjs.map