@teardown/cli 2.0.79 → 2.0.82
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/package.json +2 -2
- package/src/cli/commands/run.ts +368 -292
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teardown/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.82",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@biomejs/biome": "2.3.11",
|
|
79
|
-
"@teardown/tsconfig": "2.0.
|
|
79
|
+
"@teardown/tsconfig": "2.0.82",
|
|
80
80
|
"@types/bun": "1.3.5",
|
|
81
81
|
"@types/ejs": "^3.1.5",
|
|
82
82
|
"typescript": "5.9.3"
|
package/src/cli/commands/run.ts
CHANGED
|
@@ -97,6 +97,73 @@ function hasPodsInstalled(): boolean {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Parse CocoaPods output line to extract activity status
|
|
102
|
+
*/
|
|
103
|
+
function parsePodOutputLine(line: string): string | null {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (!trimmed) return null;
|
|
106
|
+
|
|
107
|
+
if (trimmed.startsWith("Analyzing dependencies")) {
|
|
108
|
+
return "analyzing dependencies";
|
|
109
|
+
}
|
|
110
|
+
if (trimmed.startsWith("Downloading dependencies")) {
|
|
111
|
+
return "downloading dependencies";
|
|
112
|
+
}
|
|
113
|
+
if (trimmed.startsWith("Installing")) {
|
|
114
|
+
const match = trimmed.match(/Installing (\S+)/);
|
|
115
|
+
return match ? `installing ${match[1]}` : null;
|
|
116
|
+
}
|
|
117
|
+
if (trimmed.startsWith("Generating Pods project")) {
|
|
118
|
+
return "generating Pods project";
|
|
119
|
+
}
|
|
120
|
+
if (trimmed.startsWith("Integrating client project")) {
|
|
121
|
+
return "integrating project";
|
|
122
|
+
}
|
|
123
|
+
if (trimmed.includes("Pod installation complete")) {
|
|
124
|
+
return "completing";
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get spinner text based on elapsed time
|
|
131
|
+
*/
|
|
132
|
+
function getPodSpinnerText(elapsed: number, lastActivity: string): string {
|
|
133
|
+
const activity = lastActivity ? ` - ${lastActivity}` : "";
|
|
134
|
+
const base = `Installing CocoaPods dependencies... (${elapsed}s)`;
|
|
135
|
+
|
|
136
|
+
if (elapsed === 60) {
|
|
137
|
+
return `${base} - still working, this can take a while`;
|
|
138
|
+
}
|
|
139
|
+
if (elapsed === 120) {
|
|
140
|
+
return `${base} - updating pod repos`;
|
|
141
|
+
}
|
|
142
|
+
if (elapsed === 180) {
|
|
143
|
+
return `${base} - this is taking longer than usual`;
|
|
144
|
+
}
|
|
145
|
+
return `${base}${activity}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle pod install failure output
|
|
150
|
+
*/
|
|
151
|
+
function handlePodInstallFailure(stderr: string, stdout: string, isTimeout: boolean): void {
|
|
152
|
+
if (isTimeout) {
|
|
153
|
+
console.log(chalk.yellow("\nThis usually means:"));
|
|
154
|
+
console.log(chalk.gray(" - Network connection issues"));
|
|
155
|
+
console.log(chalk.gray(" - Slow CocoaPods CDN"));
|
|
156
|
+
console.log(chalk.gray(" - Large number of pods to install"));
|
|
157
|
+
} else {
|
|
158
|
+
console.error(chalk.red(stderr));
|
|
159
|
+
}
|
|
160
|
+
if (stdout) {
|
|
161
|
+
console.log(chalk.gray("\nLast output:"), stdout.slice(-500));
|
|
162
|
+
}
|
|
163
|
+
console.log(chalk.yellow("\nTry running manually:"));
|
|
164
|
+
console.log(chalk.gray(" cd ios && pod install --repo-update"));
|
|
165
|
+
}
|
|
166
|
+
|
|
100
167
|
/**
|
|
101
168
|
* Run pod install manually
|
|
102
169
|
*/
|
|
@@ -120,48 +187,19 @@ async function runPodInstall(): Promise<boolean> {
|
|
|
120
187
|
let stderr = "";
|
|
121
188
|
let stdout = "";
|
|
122
189
|
|
|
123
|
-
// Update spinner with elapsed time and activity
|
|
124
190
|
const progressInterval = setInterval(() => {
|
|
125
191
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
126
|
-
|
|
127
|
-
spinner.text = `Installing CocoaPods dependencies... (${elapsed}s)${activity}`;
|
|
128
|
-
|
|
129
|
-
// Warn if taking too long
|
|
130
|
-
if (elapsed === 60) {
|
|
131
|
-
spinner.text = `Installing CocoaPods dependencies... (${elapsed}s) - still working, this can take a while`;
|
|
132
|
-
} else if (elapsed === 120) {
|
|
133
|
-
spinner.text = `Installing CocoaPods dependencies... (${elapsed}s) - updating pod repos`;
|
|
134
|
-
} else if (elapsed === 180) {
|
|
135
|
-
spinner.text = `Installing CocoaPods dependencies... (${elapsed}s) - this is taking longer than usual`;
|
|
136
|
-
}
|
|
192
|
+
spinner.text = getPodSpinnerText(elapsed, lastActivity);
|
|
137
193
|
}, 1000);
|
|
138
194
|
|
|
139
195
|
proc.stdout?.on("data", (data) => {
|
|
140
196
|
const chunk = data.toString();
|
|
141
197
|
stdout += chunk;
|
|
142
198
|
|
|
143
|
-
// Parse CocoaPods output for status
|
|
144
199
|
for (const line of chunk.split("\n")) {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
// Extract useful status from pod output
|
|
149
|
-
if (trimmed.startsWith("Analyzing dependencies")) {
|
|
150
|
-
lastActivity = "analyzing dependencies";
|
|
151
|
-
} else if (trimmed.startsWith("Downloading dependencies")) {
|
|
152
|
-
lastActivity = "downloading dependencies";
|
|
153
|
-
} else if (trimmed.startsWith("Installing")) {
|
|
154
|
-
// Extract pod name: "Installing PodName (1.0.0)"
|
|
155
|
-
const match = trimmed.match(/Installing (\S+)/);
|
|
156
|
-
if (match) {
|
|
157
|
-
lastActivity = `installing ${match[1]}`;
|
|
158
|
-
}
|
|
159
|
-
} else if (trimmed.startsWith("Generating Pods project")) {
|
|
160
|
-
lastActivity = "generating Pods project";
|
|
161
|
-
} else if (trimmed.startsWith("Integrating client project")) {
|
|
162
|
-
lastActivity = "integrating project";
|
|
163
|
-
} else if (trimmed.includes("Pod installation complete")) {
|
|
164
|
-
lastActivity = "completing";
|
|
200
|
+
const activity = parsePodOutputLine(line);
|
|
201
|
+
if (activity) {
|
|
202
|
+
lastActivity = activity;
|
|
165
203
|
}
|
|
166
204
|
}
|
|
167
205
|
});
|
|
@@ -179,12 +217,7 @@ async function runPodInstall(): Promise<boolean> {
|
|
|
179
217
|
resolve(true);
|
|
180
218
|
} else {
|
|
181
219
|
spinner.fail(`Failed to install CocoaPods dependencies after ${elapsed}s`);
|
|
182
|
-
|
|
183
|
-
if (stdout) {
|
|
184
|
-
console.log(chalk.gray("Last output:"), stdout.slice(-500));
|
|
185
|
-
}
|
|
186
|
-
console.log(chalk.yellow("\nTry running manually:"));
|
|
187
|
-
console.log(chalk.gray(" cd ios && pod install --repo-update"));
|
|
220
|
+
handlePodInstallFailure(stderr, stdout, false);
|
|
188
221
|
resolve(false);
|
|
189
222
|
}
|
|
190
223
|
});
|
|
@@ -195,21 +228,13 @@ async function runPodInstall(): Promise<boolean> {
|
|
|
195
228
|
resolve(false);
|
|
196
229
|
});
|
|
197
230
|
|
|
198
|
-
// Timeout after 10 minutes (pod install can be slow)
|
|
199
231
|
const timeout = setTimeout(
|
|
200
232
|
() => {
|
|
201
233
|
clearInterval(progressInterval);
|
|
202
234
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
203
235
|
proc.kill("SIGTERM");
|
|
204
236
|
spinner.fail(`pod install timed out after ${elapsed}s`);
|
|
205
|
-
|
|
206
|
-
console.log(chalk.gray(" - Network connection issues"));
|
|
207
|
-
console.log(chalk.gray(" - Slow CocoaPods CDN"));
|
|
208
|
-
console.log(chalk.gray(" - Large number of pods to install"));
|
|
209
|
-
console.log(chalk.gray("\nTry running manually: cd ios && pod install --repo-update"));
|
|
210
|
-
if (stdout) {
|
|
211
|
-
console.log(chalk.gray("\nLast output:"), stdout.slice(-500));
|
|
212
|
-
}
|
|
237
|
+
handlePodInstallFailure(stderr, stdout, true);
|
|
213
238
|
resolve(false);
|
|
214
239
|
},
|
|
215
240
|
10 * 60 * 1000
|
|
@@ -293,6 +318,289 @@ interface RunOptions {
|
|
|
293
318
|
clean?: boolean;
|
|
294
319
|
}
|
|
295
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Pre-generate routes if navigation-metro is installed
|
|
323
|
+
*/
|
|
324
|
+
function preGenerateRoutes(projectRoot: string): void {
|
|
325
|
+
const navigationMetroPath = join(projectRoot, "node_modules/@teardown/navigation-metro");
|
|
326
|
+
const navConfig = getNavigationConfig(projectRoot);
|
|
327
|
+
|
|
328
|
+
if (!existsSync(navigationMetroPath) || !navConfig) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const routesDir = resolve(projectRoot, navConfig.routesDir);
|
|
333
|
+
const generatedDir = resolve(projectRoot, navConfig.generatedDir);
|
|
334
|
+
|
|
335
|
+
if (!existsSync(routesDir)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
// Dynamic require from user's node_modules
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
342
|
+
const { generateAllRouteFiles } = require(navigationMetroPath) as {
|
|
343
|
+
generateAllRouteFiles: (opts: {
|
|
344
|
+
routesDir: string;
|
|
345
|
+
generatedDir: string;
|
|
346
|
+
prefixes: string[];
|
|
347
|
+
verbose: boolean;
|
|
348
|
+
}) => void;
|
|
349
|
+
};
|
|
350
|
+
const slug = getAppScheme(projectRoot) || "app";
|
|
351
|
+
generateAllRouteFiles({
|
|
352
|
+
routesDir,
|
|
353
|
+
generatedDir,
|
|
354
|
+
prefixes: [`${slug}://`],
|
|
355
|
+
verbose: false,
|
|
356
|
+
});
|
|
357
|
+
} catch {
|
|
358
|
+
// Ignore - Metro will regenerate
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Select iOS device based on options
|
|
364
|
+
*/
|
|
365
|
+
async function selectIOSDevice(devices: iOSDevice[], options: RunOptions): Promise<iOSDevice> {
|
|
366
|
+
if (options.device) {
|
|
367
|
+
const found = devices.find(
|
|
368
|
+
(d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.udid === options.device
|
|
369
|
+
);
|
|
370
|
+
if (!found) {
|
|
371
|
+
console.error(chalk.red(`Device not found: ${options.device}`));
|
|
372
|
+
console.log(chalk.gray("\nAvailable devices:"));
|
|
373
|
+
for (const d of devices) {
|
|
374
|
+
console.log(chalk.gray(` - ${d.name} (${d.state})`));
|
|
375
|
+
}
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
return found;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!options.picker) {
|
|
382
|
+
return devices.find((d) => d.state === "Booted") || devices[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return pickDevice(devices, "iOS");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Build iOS command arguments
|
|
390
|
+
*/
|
|
391
|
+
function buildIOSArgs(options: RunOptions, deviceName: string): string[] {
|
|
392
|
+
const args = ["react-native", "run-ios", "--simulator", deviceName, "--port", options.port, "--no-packager"];
|
|
393
|
+
|
|
394
|
+
const configuration = options.configuration || (options.release ? "Release" : "Debug");
|
|
395
|
+
args.push("--mode", configuration);
|
|
396
|
+
|
|
397
|
+
if (options.scheme) {
|
|
398
|
+
args.push("--scheme", options.scheme);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return args;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Build Android command arguments
|
|
406
|
+
*/
|
|
407
|
+
function buildAndroidArgs(
|
|
408
|
+
device: AndroidDevice,
|
|
409
|
+
options: { release?: boolean; port: string; variant?: string }
|
|
410
|
+
): string[] {
|
|
411
|
+
const args = ["react-native", "run-android", "--port", options.port, "--no-packager"];
|
|
412
|
+
|
|
413
|
+
if (device.type === "device") {
|
|
414
|
+
args.push("--deviceId", device.id);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (options.variant) {
|
|
418
|
+
args.push("--variant", options.variant);
|
|
419
|
+
} else if (options.release) {
|
|
420
|
+
args.push("--mode", "release");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return args;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Clean build directory if requested
|
|
428
|
+
*/
|
|
429
|
+
async function cleanBuildIfNeeded(platform: "ios" | "android", shouldClean?: boolean): Promise<void> {
|
|
430
|
+
if (!shouldClean) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (platform === "ios") {
|
|
435
|
+
console.log(chalk.blue("Cleaning iOS build..."));
|
|
436
|
+
try {
|
|
437
|
+
await execAsync("xcodebuild clean", { cwd: join(process.cwd(), "ios") });
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore clean errors
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
console.log(chalk.blue("Cleaning Android build..."));
|
|
443
|
+
try {
|
|
444
|
+
await execAsync("./gradlew clean", { cwd: join(process.cwd(), "android") });
|
|
445
|
+
} catch {
|
|
446
|
+
// Ignore clean errors
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Launch app with bundler connection via deep link
|
|
453
|
+
*/
|
|
454
|
+
async function launchWithBundlerIfNeeded(
|
|
455
|
+
platform: "ios" | "android",
|
|
456
|
+
metroProcess: ChildProcess | null,
|
|
457
|
+
options: { release?: boolean; port: string },
|
|
458
|
+
deviceId: string,
|
|
459
|
+
deviceType?: "emulator" | "device"
|
|
460
|
+
): Promise<void> {
|
|
461
|
+
if (!metroProcess || options.release) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const scheme = getAppScheme(process.cwd());
|
|
466
|
+
if (!scheme) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let bundlerUrl: string;
|
|
471
|
+
if (platform === "android" && deviceType === "emulator") {
|
|
472
|
+
bundlerUrl = `http://10.0.2.2:${options.port}`;
|
|
473
|
+
} else {
|
|
474
|
+
bundlerUrl = `http://localhost:${options.port}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log(chalk.blue("\nLaunching app with bundler connection..."));
|
|
478
|
+
try {
|
|
479
|
+
await launchAppWithBundler(platform, scheme, bundlerUrl, deviceId);
|
|
480
|
+
console.log(chalk.green(`✓ App launched with bundler: ${bundlerUrl}`));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.log(
|
|
483
|
+
chalk.yellow(`Note: Deep link launch skipped - ${error instanceof Error ? error.message : "unknown error"}`)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Select Android device based on options
|
|
490
|
+
*/
|
|
491
|
+
async function selectAndroidDevice(devices: AndroidDevice[], options: RunOptions): Promise<AndroidDevice> {
|
|
492
|
+
if (options.device) {
|
|
493
|
+
const found = devices.find(
|
|
494
|
+
(d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.id === options.device
|
|
495
|
+
);
|
|
496
|
+
if (!found) {
|
|
497
|
+
console.error(chalk.red(`Device not found: ${options.device}`));
|
|
498
|
+
console.log(chalk.gray("\nAvailable devices:"));
|
|
499
|
+
for (const d of devices) {
|
|
500
|
+
console.log(chalk.gray(` - ${d.name} (${d.type})`));
|
|
501
|
+
}
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!options.picker || devices.length === 1) {
|
|
508
|
+
return devices[0];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return pickAndroidDevice(devices);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Handle case when no Android devices are running but AVDs are available
|
|
516
|
+
* Returns true if an emulator was started and app should be launched
|
|
517
|
+
*/
|
|
518
|
+
async function handleNoDevicesWithAVDs(options: RunOptions, metroProcess: ChildProcess | null): Promise<boolean> {
|
|
519
|
+
const avds = await getAndroidAVDs();
|
|
520
|
+
if (avds.length === 0) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(chalk.gray("\nAvailable emulators (not running):"));
|
|
525
|
+
for (const avd of avds) {
|
|
526
|
+
console.log(chalk.gray(` - ${avd}`));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const selectedAVD = await pickAVD(avds);
|
|
530
|
+
if (!selectedAVD) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const bootSpinner = ora(`Starting ${selectedAVD}...`).start();
|
|
535
|
+
await startAndroidEmulator(selectedAVD);
|
|
536
|
+
bootSpinner.succeed(`${selectedAVD} is starting`);
|
|
537
|
+
|
|
538
|
+
await waitForAndroidDevice();
|
|
539
|
+
|
|
540
|
+
const newDevices = await getAndroidDevices();
|
|
541
|
+
if (newDevices.length === 0) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
await launchAndroidApp(
|
|
546
|
+
newDevices[0],
|
|
547
|
+
{
|
|
548
|
+
release: options.release,
|
|
549
|
+
port: options.port,
|
|
550
|
+
variant: options.variant,
|
|
551
|
+
clean: options.clean,
|
|
552
|
+
},
|
|
553
|
+
metroProcess
|
|
554
|
+
);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Execute the run command for a platform
|
|
560
|
+
*/
|
|
561
|
+
async function executeRunCommand(platform: "ios" | "android", options: RunOptions): Promise<void> {
|
|
562
|
+
let metroProcess: ChildProcess | null = null;
|
|
563
|
+
|
|
564
|
+
const cleanup = () => {
|
|
565
|
+
if (metroProcess) {
|
|
566
|
+
metroProcess.kill("SIGTERM");
|
|
567
|
+
}
|
|
568
|
+
process.exit(0);
|
|
569
|
+
};
|
|
570
|
+
process.on("SIGINT", cleanup);
|
|
571
|
+
process.on("SIGTERM", cleanup);
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const hasProject = await ensureNativeProject(platform);
|
|
575
|
+
if (!hasProject) {
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (options.bundler && !options.release) {
|
|
580
|
+
preGenerateRoutes(process.cwd());
|
|
581
|
+
metroProcess = await startMetroAndWait(options.port, platform);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (platform === "ios") {
|
|
585
|
+
await runIOS(options, metroProcess);
|
|
586
|
+
} else {
|
|
587
|
+
await runAndroid(options, metroProcess);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (metroProcess) {
|
|
591
|
+
console.log(chalk.blue("\n─".repeat(50)));
|
|
592
|
+
console.log(chalk.blue("Metro bundler running. Press Ctrl+C to stop.\n"));
|
|
593
|
+
attachBundlerToForeground(metroProcess);
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (metroProcess) {
|
|
597
|
+
metroProcess.kill("SIGTERM");
|
|
598
|
+
}
|
|
599
|
+
console.error(chalk.red(`Failed to run app: ${error instanceof Error ? error.message : error}`));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
296
604
|
/**
|
|
297
605
|
* Create the run command
|
|
298
606
|
*/
|
|
@@ -314,87 +622,7 @@ export function createRunCommand(): Command {
|
|
|
314
622
|
console.error(chalk.red(`Invalid platform: ${platform}. Use 'ios' or 'android'.`));
|
|
315
623
|
process.exit(1);
|
|
316
624
|
}
|
|
317
|
-
|
|
318
|
-
// Track Metro process for cleanup
|
|
319
|
-
let metroProcess: ChildProcess | null = null;
|
|
320
|
-
|
|
321
|
-
// Handle SIGINT (Ctrl+C) to clean up Metro
|
|
322
|
-
const cleanup = () => {
|
|
323
|
-
if (metroProcess) {
|
|
324
|
-
metroProcess.kill("SIGTERM");
|
|
325
|
-
}
|
|
326
|
-
process.exit(0);
|
|
327
|
-
};
|
|
328
|
-
process.on("SIGINT", cleanup);
|
|
329
|
-
process.on("SIGTERM", cleanup);
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
// Ensure native project exists (auto-prebuild if needed)
|
|
333
|
-
const hasProject = await ensureNativeProject(platform);
|
|
334
|
-
if (!hasProject) {
|
|
335
|
-
process.exit(1);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Start Metro bundler FIRST (unless --no-bundler or --release)
|
|
339
|
-
if (options.bundler && !options.release) {
|
|
340
|
-
// Pre-generate routes if navigation-metro is installed
|
|
341
|
-
const projectRoot = process.cwd();
|
|
342
|
-
const navigationMetroPath = join(projectRoot, "node_modules/@teardown/navigation-metro");
|
|
343
|
-
const navConfig = getNavigationConfig(projectRoot);
|
|
344
|
-
|
|
345
|
-
if (existsSync(navigationMetroPath) && navConfig) {
|
|
346
|
-
const routesDir = resolve(projectRoot, navConfig.routesDir);
|
|
347
|
-
const generatedDir = resolve(projectRoot, navConfig.generatedDir);
|
|
348
|
-
|
|
349
|
-
if (existsSync(routesDir)) {
|
|
350
|
-
try {
|
|
351
|
-
// Dynamic require from user's node_modules
|
|
352
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
353
|
-
const { generateAllRouteFiles } = require(navigationMetroPath) as {
|
|
354
|
-
generateAllRouteFiles: (opts: {
|
|
355
|
-
routesDir: string;
|
|
356
|
-
generatedDir: string;
|
|
357
|
-
prefixes: string[];
|
|
358
|
-
verbose: boolean;
|
|
359
|
-
}) => void;
|
|
360
|
-
};
|
|
361
|
-
const slug = getAppScheme(projectRoot) || "app";
|
|
362
|
-
generateAllRouteFiles({
|
|
363
|
-
routesDir,
|
|
364
|
-
generatedDir,
|
|
365
|
-
prefixes: [`${slug}://`],
|
|
366
|
-
verbose: false,
|
|
367
|
-
});
|
|
368
|
-
} catch {
|
|
369
|
-
// Ignore - Metro will regenerate
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
metroProcess = await startMetroAndWait(options.port, platform);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Run platform-specific build and launch
|
|
378
|
-
if (platform === "ios") {
|
|
379
|
-
await runIOS(options, metroProcess);
|
|
380
|
-
} else {
|
|
381
|
-
await runAndroid(options, metroProcess);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// If Metro is running, attach to foreground
|
|
385
|
-
if (metroProcess) {
|
|
386
|
-
console.log(chalk.blue("\n─".repeat(50)));
|
|
387
|
-
console.log(chalk.blue("Metro bundler running. Press Ctrl+C to stop.\n"));
|
|
388
|
-
attachBundlerToForeground(metroProcess);
|
|
389
|
-
}
|
|
390
|
-
} catch (error) {
|
|
391
|
-
// Clean up Metro on error
|
|
392
|
-
if (metroProcess) {
|
|
393
|
-
metroProcess.kill("SIGTERM");
|
|
394
|
-
}
|
|
395
|
-
console.error(chalk.red(`Failed to run app: ${error instanceof Error ? error.message : error}`));
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
625
|
+
await executeRunCommand(platform, options);
|
|
398
626
|
});
|
|
399
627
|
|
|
400
628
|
return run;
|
|
@@ -452,30 +680,7 @@ async function runIOS(options: RunOptions, metroProcess: ChildProcess | null): P
|
|
|
452
680
|
process.exit(1);
|
|
453
681
|
}
|
|
454
682
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (options.device) {
|
|
458
|
-
// Find device by name or udid
|
|
459
|
-
const found = devices.find(
|
|
460
|
-
(d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.udid === options.device
|
|
461
|
-
);
|
|
462
|
-
if (!found) {
|
|
463
|
-
console.error(chalk.red(`Device not found: ${options.device}`));
|
|
464
|
-
console.log(chalk.gray("\nAvailable devices:"));
|
|
465
|
-
for (const d of devices) {
|
|
466
|
-
console.log(chalk.gray(` - ${d.name} (${d.state})`));
|
|
467
|
-
}
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
470
|
-
selectedDevice = found;
|
|
471
|
-
} else if (!options.picker) {
|
|
472
|
-
// Use first booted device, or first available
|
|
473
|
-
selectedDevice = devices.find((d) => d.state === "Booted") || devices[0];
|
|
474
|
-
} else {
|
|
475
|
-
// Show device picker
|
|
476
|
-
selectedDevice = await pickDevice(devices, "iOS");
|
|
477
|
-
}
|
|
478
|
-
|
|
683
|
+
const selectedDevice = await selectIOSDevice(devices, options);
|
|
479
684
|
console.log(chalk.blue(`\nSelected device: ${selectedDevice.name}`));
|
|
480
685
|
|
|
481
686
|
// Boot device if needed
|
|
@@ -488,54 +693,12 @@ async function runIOS(options: RunOptions, metroProcess: ChildProcess | null): P
|
|
|
488
693
|
// Build the app
|
|
489
694
|
console.log(chalk.blue("\nBuilding iOS app...\n"));
|
|
490
695
|
|
|
491
|
-
|
|
492
|
-
"react-native",
|
|
493
|
-
"run-ios",
|
|
494
|
-
"--simulator",
|
|
495
|
-
selectedDevice.name,
|
|
496
|
-
"--port",
|
|
497
|
-
options.port,
|
|
498
|
-
"--no-packager",
|
|
499
|
-
];
|
|
500
|
-
|
|
501
|
-
// Handle configuration/release mode
|
|
502
|
-
const configuration = options.configuration || (options.release ? "Release" : "Debug");
|
|
503
|
-
args.push("--mode", configuration);
|
|
504
|
-
|
|
505
|
-
// Handle scheme if specified
|
|
506
|
-
if (options.scheme) {
|
|
507
|
-
args.push("--scheme", options.scheme);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Handle clean build
|
|
511
|
-
if (options.clean) {
|
|
512
|
-
console.log(chalk.blue("Cleaning iOS build..."));
|
|
513
|
-
try {
|
|
514
|
-
await execAsync("xcodebuild clean", { cwd: join(process.cwd(), "ios") });
|
|
515
|
-
} catch {
|
|
516
|
-
// Ignore clean errors
|
|
517
|
-
}
|
|
518
|
-
}
|
|
696
|
+
await cleanBuildIfNeeded("ios", options.clean);
|
|
519
697
|
|
|
698
|
+
const args = buildIOSArgs(options, selectedDevice.name);
|
|
520
699
|
await runCommand("npx", args);
|
|
521
700
|
|
|
522
|
-
|
|
523
|
-
if (metroProcess && !options.release) {
|
|
524
|
-
const scheme = getAppScheme(process.cwd());
|
|
525
|
-
if (scheme) {
|
|
526
|
-
const bundlerUrl = `http://localhost:${options.port}`;
|
|
527
|
-
console.log(chalk.blue("\nLaunching app with bundler connection..."));
|
|
528
|
-
try {
|
|
529
|
-
await launchAppWithBundler("ios", scheme, bundlerUrl, selectedDevice.udid);
|
|
530
|
-
console.log(chalk.green(`✓ App launched with bundler: ${bundlerUrl}`));
|
|
531
|
-
} catch (error) {
|
|
532
|
-
// Deep link launch failed, but app should still work
|
|
533
|
-
console.log(
|
|
534
|
-
chalk.yellow(`Note: Deep link launch skipped - ${error instanceof Error ? error.message : "unknown error"}`)
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
701
|
+
await launchWithBundlerIfNeeded("ios", metroProcess, options, selectedDevice.udid);
|
|
539
702
|
} catch (error) {
|
|
540
703
|
spinner.fail("Failed to detect iOS devices");
|
|
541
704
|
throw error;
|
|
@@ -555,65 +718,16 @@ async function runAndroid(options: RunOptions, metroProcess: ChildProcess | null
|
|
|
555
718
|
if (devices.length === 0) {
|
|
556
719
|
console.log(chalk.yellow("\nNo Android devices or emulators found."));
|
|
557
720
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
console.log(chalk.gray("\nAvailable emulators (not running):"));
|
|
562
|
-
for (const avd of avds) {
|
|
563
|
-
console.log(chalk.gray(` - ${avd}`));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const selectedAVD = await pickAVD(avds);
|
|
567
|
-
if (selectedAVD) {
|
|
568
|
-
const bootSpinner = ora(`Starting ${selectedAVD}...`).start();
|
|
569
|
-
await startAndroidEmulator(selectedAVD);
|
|
570
|
-
bootSpinner.succeed(`${selectedAVD} is starting`);
|
|
571
|
-
|
|
572
|
-
// Wait for device to be ready
|
|
573
|
-
await waitForAndroidDevice();
|
|
574
|
-
|
|
575
|
-
// Re-fetch devices
|
|
576
|
-
const newDevices = await getAndroidDevices();
|
|
577
|
-
if (newDevices.length > 0) {
|
|
578
|
-
await launchAndroidApp(
|
|
579
|
-
newDevices[0],
|
|
580
|
-
{
|
|
581
|
-
release: options.release,
|
|
582
|
-
port: options.port,
|
|
583
|
-
variant: options.variant,
|
|
584
|
-
clean: options.clean,
|
|
585
|
-
},
|
|
586
|
-
metroProcess
|
|
587
|
-
);
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
721
|
+
const handled = await handleNoDevicesWithAVDs(options, metroProcess);
|
|
722
|
+
if (handled) {
|
|
723
|
+
return;
|
|
591
724
|
}
|
|
592
725
|
|
|
593
726
|
console.log(chalk.gray("\nTo create an emulator, run: Android Studio > Tools > Device Manager"));
|
|
594
727
|
process.exit(1);
|
|
595
728
|
}
|
|
596
729
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (options.device) {
|
|
600
|
-
const found = devices.find(
|
|
601
|
-
(d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.id === options.device
|
|
602
|
-
);
|
|
603
|
-
if (!found) {
|
|
604
|
-
console.error(chalk.red(`Device not found: ${options.device}`));
|
|
605
|
-
console.log(chalk.gray("\nAvailable devices:"));
|
|
606
|
-
for (const d of devices) {
|
|
607
|
-
console.log(chalk.gray(` - ${d.name} (${d.type})`));
|
|
608
|
-
}
|
|
609
|
-
process.exit(1);
|
|
610
|
-
}
|
|
611
|
-
selectedDevice = found;
|
|
612
|
-
} else if (!options.picker || devices.length === 1) {
|
|
613
|
-
selectedDevice = devices[0];
|
|
614
|
-
} else {
|
|
615
|
-
selectedDevice = await pickAndroidDevice(devices);
|
|
616
|
-
}
|
|
730
|
+
const selectedDevice = await selectAndroidDevice(devices, options);
|
|
617
731
|
|
|
618
732
|
await launchAndroidApp(
|
|
619
733
|
selectedDevice,
|
|
@@ -642,50 +756,12 @@ async function launchAndroidApp(
|
|
|
642
756
|
console.log(chalk.blue(`\nSelected device: ${device.name} (${device.type})`));
|
|
643
757
|
console.log(chalk.blue("\nBuilding Android app...\n"));
|
|
644
758
|
|
|
645
|
-
|
|
646
|
-
if (options.clean) {
|
|
647
|
-
console.log(chalk.blue("Cleaning Android build..."));
|
|
648
|
-
try {
|
|
649
|
-
await execAsync("./gradlew clean", { cwd: join(process.cwd(), "android") });
|
|
650
|
-
} catch {
|
|
651
|
-
// Ignore clean errors
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const args = ["react-native", "run-android", "--port", options.port, "--no-packager"];
|
|
656
|
-
|
|
657
|
-
if (device.type === "device") {
|
|
658
|
-
args.push("--deviceId", device.id);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Handle variant or release mode
|
|
662
|
-
if (options.variant) {
|
|
663
|
-
args.push("--variant", options.variant);
|
|
664
|
-
} else if (options.release) {
|
|
665
|
-
args.push("--mode", "release");
|
|
666
|
-
}
|
|
759
|
+
await cleanBuildIfNeeded("android", options.clean);
|
|
667
760
|
|
|
761
|
+
const args = buildAndroidArgs(device, options);
|
|
668
762
|
await runCommand("npx", args);
|
|
669
763
|
|
|
670
|
-
|
|
671
|
-
if (metroProcess && !options.release) {
|
|
672
|
-
const scheme = getAppScheme(process.cwd());
|
|
673
|
-
if (scheme) {
|
|
674
|
-
// Android emulator uses 10.0.2.2 to reach host machine
|
|
675
|
-
const bundlerUrl =
|
|
676
|
-
device.type === "emulator" ? `http://10.0.2.2:${options.port}` : `http://localhost:${options.port}`;
|
|
677
|
-
console.log(chalk.blue("\nLaunching app with bundler connection..."));
|
|
678
|
-
try {
|
|
679
|
-
await launchAppWithBundler("android", scheme, bundlerUrl, device.id);
|
|
680
|
-
console.log(chalk.green(`✓ App launched with bundler: ${bundlerUrl}`));
|
|
681
|
-
} catch (error) {
|
|
682
|
-
// Deep link launch failed, but app should still work
|
|
683
|
-
console.log(
|
|
684
|
-
chalk.yellow(`Note: Deep link launch skipped - ${error instanceof Error ? error.message : "unknown error"}`)
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
764
|
+
await launchWithBundlerIfNeeded("android", metroProcess, options, device.id, device.type);
|
|
689
765
|
}
|
|
690
766
|
|
|
691
767
|
/**
|